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     */
018    package org.apache.hadoop.util;
019    
020    import java.util.Comparator;
021    import java.util.Iterator;
022    import java.util.PriorityQueue;
023    
024    import org.apache.hadoop.HadoopIllegalArgumentException;
025    import org.apache.hadoop.classification.InterfaceAudience;
026    
027    import com.google.common.annotations.VisibleForTesting;
028    import 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
054    public 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      /** A clock for measuring time so that it can be mocked in unit tests. */
081      static class Clock {
082        /** @return the current time. */
083        long currentTime() {
084          return System.nanoTime();
085        }
086      }
087      
088      private static int updateRecommendedLength(int recommendedLength,
089          int sizeLimit) {
090        return sizeLimit > 0 && sizeLimit < recommendedLength?
091            (sizeLimit/4*3) // 0.75 load factor
092            : recommendedLength;
093      }
094    
095      /*
096       * The memory footprint for java.util.PriorityQueue is low but the
097       * remove(Object) method runs in linear time. We may improve it by using a
098       * balanced tree. However, we do not yet have a low memory footprint balanced
099       * tree implementation.
100       */
101      private final PriorityQueue<Entry> queue;
102      private final long creationExpirationPeriod;
103      private final long accessExpirationPeriod;
104      private final int sizeLimit;
105      private final Clock clock;
106    
107      /**
108       * @param recommendedLength Recommended size of the internal array.
109       * @param sizeLimit the limit of the size of the cache.
110       *            The limit is disabled if it is <= 0.
111       * @param creationExpirationPeriod the time period C > 0 in nanoseconds that
112       *            the creation of an entry is expired if it is added to the cache
113       *            longer than C.
114       * @param accessExpirationPeriod the time period A >= 0 in nanoseconds that
115       *            the access of an entry is expired if it is not accessed
116       *            longer than A. 
117       */
118      public LightWeightCache(final int recommendedLength,
119          final int sizeLimit,
120          final long creationExpirationPeriod,
121          final long accessExpirationPeriod) {
122        this(recommendedLength, sizeLimit,
123            creationExpirationPeriod, accessExpirationPeriod, new Clock());
124      }
125    
126      @VisibleForTesting
127      LightWeightCache(final int recommendedLength,
128          final int sizeLimit,
129          final long creationExpirationPeriod,
130          final long accessExpirationPeriod,
131          final Clock clock) {
132        super(updateRecommendedLength(recommendedLength, sizeLimit));
133    
134        this.sizeLimit = sizeLimit;
135    
136        if (creationExpirationPeriod <= 0) {
137          throw new IllegalArgumentException("creationExpirationPeriod = "
138              + creationExpirationPeriod + " <= 0");
139        }
140        this.creationExpirationPeriod = creationExpirationPeriod;
141    
142        if (accessExpirationPeriod < 0) {
143          throw new IllegalArgumentException("accessExpirationPeriod = "
144              + accessExpirationPeriod + " < 0");
145        }
146        this.accessExpirationPeriod = accessExpirationPeriod;
147    
148        this.queue = new PriorityQueue<Entry>(
149            sizeLimit > 0? sizeLimit + 1: 1 << 10, expirationTimeComparator);
150        this.clock = clock;
151      }
152    
153      void setExpirationTime(final Entry e, final long expirationPeriod) {
154        e.setExpirationTime(clock.currentTime() + expirationPeriod);
155      }
156    
157      boolean isExpired(final Entry e, final long now) {
158        return now > e.getExpirationTime();
159      }
160    
161      private E evict() {
162        @SuppressWarnings("unchecked")
163        final E polled = (E)queue.poll();
164        final E removed = super.remove(polled);
165        Preconditions.checkState(removed == polled);
166        return polled;
167      }
168    
169      /** Evict expired entries. */
170      private void evictExpiredEntries() {
171        final long now = clock.currentTime();
172        for(int i = 0; i < EVICTION_LIMIT; i++) {
173          final Entry peeked = queue.peek();
174          if (peeked == null || !isExpired(peeked, now)) {
175            return;
176          }
177    
178          final E evicted = evict();
179          Preconditions.checkState(evicted == peeked);
180        }
181      }
182    
183      /** Evict entries in order to enforce the size limit of the cache. */
184      private void evictEntries() {
185        if (sizeLimit > 0) {
186          for(int i = size(); i > sizeLimit; i--) {
187            evict();
188          }
189        }
190      }
191      
192      @Override
193      public E get(K key) {
194        final E entry = super.get(key);
195        if (entry != null) {
196          if (accessExpirationPeriod > 0) {
197            // update expiration time
198            final Entry existing = (Entry)entry;
199            Preconditions.checkState(queue.remove(existing));
200            setExpirationTime(existing, accessExpirationPeriod);
201            queue.offer(existing);
202          }
203        }
204        return entry;
205      }
206    
207      @Override
208      public E put(final E entry) {
209        if (!(entry instanceof Entry)) {
210          throw new HadoopIllegalArgumentException(
211              "!(entry instanceof Entry), entry.getClass()=" + entry.getClass());
212        }
213    
214        evictExpiredEntries();
215    
216        final E existing = super.put(entry);
217        if (existing != null) {
218          queue.remove(existing);
219        }
220    
221        final Entry e = (Entry)entry;
222        setExpirationTime(e, creationExpirationPeriod);
223        queue.offer(e);
224        
225        evictEntries();
226        return existing;
227      }
228    
229      @Override
230      public E remove(K key) {
231        evictExpiredEntries();
232    
233        final E removed = super.remove(key);
234        if (removed != null) {
235          Preconditions.checkState(queue.remove(removed));
236        }
237        return removed;
238      }
239    
240      @Override
241      public Iterator<E> iterator() {
242        final Iterator<E> iter = super.iterator();
243        return new Iterator<E>() {
244          @Override
245          public boolean hasNext() {
246            return iter.hasNext();
247          }
248    
249          @Override
250          public E next() {
251            return iter.next();
252          }
253    
254          @Override
255          public void remove() {
256            // It would be tricky to support this because LightWeightCache#remove
257            // may evict multiple elements via evictExpiredEntries.
258            throw new UnsupportedOperationException("Remove via iterator is " +
259                "not supported for LightWeightCache");
260          }
261        };
262      }
263    }