001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.util;
019
020import java.util.Comparator;
021import java.util.Iterator;
022import java.util.PriorityQueue;
023
024import org.apache.hadoop.HadoopIllegalArgumentException;
025import org.apache.hadoop.classification.InterfaceAudience;
026
027import com.google.common.annotations.VisibleForTesting;
028import com.google.common.base.Preconditions;
029
030/**
031 * A low memory footprint Cache which extends {@link LightWeightGSet}.
032 * An entry in the cache is expired if
033 * (1) it is added to the cache longer than the creation-expiration period, and
034 * (2) it is not accessed for the access-expiration period.
035 * When an entry is expired, it may be evicted from the cache.
036 * When the size limit of the cache is set, the cache will evict the entries
037 * with earliest expiration time, even if they are not expired.
038 * 
039 * It is guaranteed that number of entries in the cache is less than or equal
040 * to the size limit.  However, It is not guaranteed that expired entries are
041 * evicted from the cache. An expired entry may possibly be accessed after its
042 * expiration time. In such case, the expiration time may be updated.
043 *
044 * This class does not support null entry.
045 *
046 * This class is not thread safe.
047 *
048 * @param <K> Key type for looking up the entries
049 * @param <E> Entry type, which must be
050 *       (1) a subclass of K, and
051 *       (2) implementing {@link Entry} interface, and
052 */
053@InterfaceAudience.Private
054public class LightWeightCache<K, E extends K> extends LightWeightGSet<K, E> {
055  /** Limit the number of entries in each eviction. */
056  private static final int EVICTION_LIMIT = 1 << 16;
057
058  /**
059   * Entries of {@link LightWeightCache}.
060   */
061  public static interface Entry extends LinkedElement {
062    /** Set the expiration time. */
063    public void setExpirationTime(long timeNano);
064
065    /** Get the expiration time. */
066    public long getExpirationTime();
067  }
068
069  /** Comparator for sorting entries by expiration time in ascending order. */
070  private static final Comparator<Entry> expirationTimeComparator
071      = new Comparator<Entry>() {
072    @Override
073    public int compare(Entry left, Entry right) {
074      final long l = left.getExpirationTime();
075      final long r = right.getExpirationTime();
076      return l > r? 1: l < r? -1: 0;
077    }
078  };
079  
080  private static int updateRecommendedLength(int recommendedLength,
081      int sizeLimit) {
082    return sizeLimit > 0 && sizeLimit < recommendedLength?
083        (sizeLimit/4*3) // 0.75 load factor
084        : recommendedLength;
085  }
086
087  /*
088   * The memory footprint for java.util.PriorityQueue is low but the
089   * remove(Object) method runs in linear time. We may improve it by using a
090   * balanced tree. However, we do not yet have a low memory footprint balanced
091   * tree implementation.
092   */
093  private final PriorityQueue<Entry> queue;
094  private final long creationExpirationPeriod;
095  private final long accessExpirationPeriod;
096  private final int sizeLimit;
097  private final Timer timer;
098
099  /**
100   * @param recommendedLength Recommended size of the internal array.
101   * @param sizeLimit the limit of the size of the cache.
102   *            The limit is disabled if it is <= 0.
103   * @param creationExpirationPeriod the time period C > 0 in nanoseconds that
104   *            the creation of an entry is expired if it is added to the cache
105   *            longer than C.
106   * @param accessExpirationPeriod the time period A >= 0 in nanoseconds that
107   *            the access of an entry is expired if it is not accessed
108   *            longer than A. 
109   */
110  public LightWeightCache(final int recommendedLength,
111      final int sizeLimit,
112      final long creationExpirationPeriod,
113      final long accessExpirationPeriod) {
114    this(recommendedLength, sizeLimit,
115        creationExpirationPeriod, accessExpirationPeriod, new Timer());
116  }
117
118  @VisibleForTesting
119  LightWeightCache(final int recommendedLength,
120      final int sizeLimit,
121      final long creationExpirationPeriod,
122      final long accessExpirationPeriod,
123      final Timer timer) {
124    super(updateRecommendedLength(recommendedLength, sizeLimit));
125
126    this.sizeLimit = sizeLimit;
127
128    if (creationExpirationPeriod <= 0) {
129      throw new IllegalArgumentException("creationExpirationPeriod = "
130          + creationExpirationPeriod + " <= 0");
131    }
132    this.creationExpirationPeriod = creationExpirationPeriod;
133
134    if (accessExpirationPeriod < 0) {
135      throw new IllegalArgumentException("accessExpirationPeriod = "
136          + accessExpirationPeriod + " < 0");
137    }
138    this.accessExpirationPeriod = accessExpirationPeriod;
139
140    this.queue = new PriorityQueue<Entry>(
141        sizeLimit > 0? sizeLimit + 1: 1 << 10, expirationTimeComparator);
142    this.timer = timer;
143  }
144
145  void setExpirationTime(final Entry e, final long expirationPeriod) {
146    e.setExpirationTime(timer.monotonicNowNanos() + expirationPeriod);
147  }
148
149  boolean isExpired(final Entry e, final long now) {
150    return now > e.getExpirationTime();
151  }
152
153  private E evict() {
154    @SuppressWarnings("unchecked")
155    final E polled = (E)queue.poll();
156    final E removed = super.remove(polled);
157    Preconditions.checkState(removed == polled);
158    return polled;
159  }
160
161  /** Evict expired entries. */
162  private void evictExpiredEntries() {
163    final long now = timer.monotonicNowNanos();
164    for(int i = 0; i < EVICTION_LIMIT; i++) {
165      final Entry peeked = queue.peek();
166      if (peeked == null || !isExpired(peeked, now)) {
167        return;
168      }
169
170      final E evicted = evict();
171      Preconditions.checkState(evicted == peeked);
172    }
173  }
174
175  /** Evict entries in order to enforce the size limit of the cache. */
176  private void evictEntries() {
177    if (sizeLimit > 0) {
178      for(int i = size(); i > sizeLimit; i--) {
179        evict();
180      }
181    }
182  }
183  
184  @Override
185  public E get(K key) {
186    final E entry = super.get(key);
187    if (entry != null) {
188      if (accessExpirationPeriod > 0) {
189        // update expiration time
190        final Entry existing = (Entry)entry;
191        Preconditions.checkState(queue.remove(existing));
192        setExpirationTime(existing, accessExpirationPeriod);
193        queue.offer(existing);
194      }
195    }
196    return entry;
197  }
198
199  @Override
200  public E put(final E entry) {
201    if (!(entry instanceof Entry)) {
202      throw new HadoopIllegalArgumentException(
203          "!(entry instanceof Entry), entry.getClass()=" + entry.getClass());
204    }
205
206    evictExpiredEntries();
207
208    final E existing = super.put(entry);
209    if (existing != null) {
210      queue.remove(existing);
211    }
212
213    final Entry e = (Entry)entry;
214    setExpirationTime(e, creationExpirationPeriod);
215    queue.offer(e);
216    
217    evictEntries();
218    return existing;
219  }
220
221  @Override
222  public E remove(K key) {
223    evictExpiredEntries();
224
225    final E removed = super.remove(key);
226    if (removed != null) {
227      Preconditions.checkState(queue.remove(removed));
228    }
229    return removed;
230  }
231
232  @Override
233  public Iterator<E> iterator() {
234    final Iterator<E> iter = super.iterator();
235    return new Iterator<E>() {
236      @Override
237      public boolean hasNext() {
238        return iter.hasNext();
239      }
240
241      @Override
242      public E next() {
243        return iter.next();
244      }
245
246      @Override
247      public void remove() {
248        // It would be tricky to support this because LightWeightCache#remove
249        // may evict multiple elements via evictExpiredEntries.
250        throw new UnsupportedOperationException("Remove via iterator is " +
251            "not supported for LightWeightCache");
252      }
253    };
254  }
255}