| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.hadoop.util; |
| |
| import java.util.Comparator; |
| import java.util.PriorityQueue; |
| |
| import org.apache.hadoop.HadoopIllegalArgumentException; |
| import org.apache.hadoop.classification.InterfaceAudience; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| |
| /** |
| * A low memory footprint Cache which extends {@link LightWeightGSet}. |
| * An entry in the cache is expired if |
| * (1) it is added to the cache longer than the creation-expiration period, and |
| * (2) it is not accessed for the access-expiration period. |
| * When an entry is expired, it may be evicted from the cache. |
| * When the size limit of the cache is set, the cache will evict the entries |
| * with earliest expiration time, even if they are not expired. |
| * |
| * It is guaranteed that number of entries in the cache is less than or equal |
| * to the size limit. However, It is not guaranteed that expired entries are |
| * evicted from the cache. An expired entry may possibly be accessed after its |
| * expiration time. In such case, the expiration time may be updated. |
| * |
| * This class does not support null entry. |
| * |
| * This class is not thread safe. |
| * |
| * @param <K> Key type for looking up the entries |
| * @param <E> Entry type, which must be |
| * (1) a subclass of K, and |
| * (2) implementing {@link Entry} interface, and |
| */ |
| @InterfaceAudience.Private |
| public class LightWeightCache<K, E extends K> extends LightWeightGSet<K, E> { |
| /** Limit the number of entries in each eviction. */ |
| private static final int EVICTION_LIMIT = 1 << 16; |
| |
| /** |
| * Entries of {@link LightWeightCache}. |
| */ |
| public static interface Entry extends LinkedElement { |
| /** Set the expiration time. */ |
| public void setExpirationTime(long timeNano); |
| |
| /** Get the expiration time. */ |
| public long getExpirationTime(); |
| } |
| |
| /** Comparator for sorting entries by expiration time in ascending order. */ |
| private static final Comparator<Entry> expirationTimeComparator |
| = new Comparator<Entry>() { |
| @Override |
| public int compare(Entry left, Entry right) { |
| final long l = left.getExpirationTime(); |
| final long r = right.getExpirationTime(); |
| return l > r? 1: l < r? -1: 0; |
| } |
| }; |
| |
| /** A clock for measuring time so that it can be mocked in unit tests. */ |
| static class Clock { |
| /** @return the current time. */ |
| long currentTime() { |
| return System.nanoTime(); |
| } |
| } |
| |
| private static int updateRecommendedLength(int recommendedLength, |
| int sizeLimit) { |
| return sizeLimit > 0 && sizeLimit < recommendedLength? |
| (sizeLimit/4*3) // 0.75 load factor |
| : recommendedLength; |
| } |
| |
| /* |
| * The memory footprint for java.util.PriorityQueue is low but the |
| * remove(Object) method runs in linear time. We may improve it by using a |
| * balanced tree. However, we do not yet have a low memory footprint balanced |
| * tree implementation. |
| */ |
| private final PriorityQueue<Entry> queue; |
| private final long creationExpirationPeriod; |
| private final long accessExpirationPeriod; |
| private final int sizeLimit; |
| private final Clock clock; |
| |
| /** |
| * @param recommendedLength Recommended size of the internal array. |
| * @param sizeLimit the limit of the size of the cache. |
| * The limit is disabled if it is <= 0. |
| * @param creationExpirationPeriod the time period C > 0 in nanoseconds that |
| * the creation of an entry is expired if it is added to the cache |
| * longer than C. |
| * @param accessExpirationPeriod the time period A >= 0 in nanoseconds that |
| * the access of an entry is expired if it is not accessed |
| * longer than A. |
| */ |
| public LightWeightCache(final int recommendedLength, |
| final int sizeLimit, |
| final long creationExpirationPeriod, |
| final long accessExpirationPeriod) { |
| this(recommendedLength, sizeLimit, |
| creationExpirationPeriod, accessExpirationPeriod, new Clock()); |
| } |
| |
| @VisibleForTesting |
| LightWeightCache(final int recommendedLength, |
| final int sizeLimit, |
| final long creationExpirationPeriod, |
| final long accessExpirationPeriod, |
| final Clock clock) { |
| super(updateRecommendedLength(recommendedLength, sizeLimit)); |
| |
| this.sizeLimit = sizeLimit; |
| |
| if (creationExpirationPeriod <= 0) { |
| throw new IllegalArgumentException("creationExpirationPeriod = " |
| + creationExpirationPeriod + " <= 0"); |
| } |
| this.creationExpirationPeriod = creationExpirationPeriod; |
| |
| if (accessExpirationPeriod < 0) { |
| throw new IllegalArgumentException("accessExpirationPeriod = " |
| + accessExpirationPeriod + " < 0"); |
| } |
| this.accessExpirationPeriod = accessExpirationPeriod; |
| |
| this.queue = new PriorityQueue<Entry>( |
| sizeLimit > 0? sizeLimit + 1: 1 << 10, expirationTimeComparator); |
| this.clock = clock; |
| } |
| |
| void setExpirationTime(final Entry e, final long expirationPeriod) { |
| e.setExpirationTime(clock.currentTime() + expirationPeriod); |
| } |
| |
| boolean isExpired(final Entry e, final long now) { |
| return now > e.getExpirationTime(); |
| } |
| |
| private E evict() { |
| @SuppressWarnings("unchecked") |
| final E polled = (E)queue.poll(); |
| final E removed = super.remove(polled); |
| Preconditions.checkState(removed == polled); |
| return polled; |
| } |
| |
| /** Evict expired entries. */ |
| private void evictExpiredEntries() { |
| final long now = clock.currentTime(); |
| for(int i = 0; i < EVICTION_LIMIT; i++) { |
| final Entry peeked = queue.peek(); |
| if (peeked == null || !isExpired(peeked, now)) { |
| return; |
| } |
| |
| final E evicted = evict(); |
| Preconditions.checkState(evicted == peeked); |
| } |
| } |
| |
| /** Evict entries in order to enforce the size limit of the cache. */ |
| private void evictEntries() { |
| if (sizeLimit > 0) { |
| for(int i = size(); i > sizeLimit; i--) { |
| evict(); |
| } |
| } |
| } |
| |
| @Override |
| public E get(K key) { |
| final E entry = super.get(key); |
| if (entry != null) { |
| if (accessExpirationPeriod > 0) { |
| // update expiration time |
| final Entry existing = (Entry)entry; |
| Preconditions.checkState(queue.remove(existing)); |
| setExpirationTime(existing, accessExpirationPeriod); |
| queue.offer(existing); |
| } |
| } |
| return entry; |
| } |
| |
| @Override |
| public E put(final E entry) { |
| if (!(entry instanceof Entry)) { |
| throw new HadoopIllegalArgumentException( |
| "!(entry instanceof Entry), entry.getClass()=" + entry.getClass()); |
| } |
| |
| evictExpiredEntries(); |
| |
| final E existing = super.put(entry); |
| if (existing != null) { |
| queue.remove(existing); |
| } |
| |
| final Entry e = (Entry)entry; |
| setExpirationTime(e, creationExpirationPeriod); |
| queue.offer(e); |
| |
| evictEntries(); |
| return existing; |
| } |
| |
| @Override |
| public E remove(K key) { |
| evictExpiredEntries(); |
| |
| final E removed = super.remove(key); |
| if (removed != null) { |
| Preconditions.checkState(queue.remove(removed)); |
| } |
| return removed; |
| } |
| } |