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.PriorityQueue;
022    
023    import org.apache.hadoop.HadoopIllegalArgumentException;
024    import org.apache.hadoop.classification.InterfaceAudience;
025    
026    import com.google.common.annotations.VisibleForTesting;
027    import com.google.common.base.Preconditions;
028    
029    /**
030     * A low memory footprint Cache which extends {@link LightWeightGSet}.
031     * An entry in the cache is expired if
032     * (1) it is added to the cache longer than the creation-expiration period, and
033     * (2) it is not accessed for the access-expiration period.
034     * When an entry is expired, it may be evicted from the cache.
035     * When the size limit of the cache is set, the cache will evict the entries
036     * with earliest expiration time, even if they are not expired.
037     * 
038     * It is guaranteed that number of entries in the cache is less than or equal
039     * to the size limit.  However, It is not guaranteed that expired entries are
040     * evicted from the cache. An expired entry may possibly be accessed after its
041     * expiration time. In such case, the expiration time may be updated.
042     *
043     * This class does not support null entry.
044     *
045     * This class is not thread safe.
046     *
047     * @param <K> Key type for looking up the entries
048     * @param <E> Entry type, which must be
049     *       (1) a subclass of K, and
050     *       (2) implementing {@link Entry} interface, and
051     */
052    @InterfaceAudience.Private
053    public class LightWeightCache<K, E extends K> extends LightWeightGSet<K, E> {
054      /** Limit the number of entries in each eviction. */
055      private static final int EVICTION_LIMIT = 1 << 16;
056    
057      /**
058       * Entries of {@link LightWeightCache}.
059       */
060      public static interface Entry extends LinkedElement {
061        /** Set the expiration time. */
062        public void setExpirationTime(long timeNano);
063    
064        /** Get the expiration time. */
065        public long getExpirationTime();
066      }
067    
068      /** Comparator for sorting entries by expiration time in ascending order. */
069      private static final Comparator<Entry> expirationTimeComparator
070          = new Comparator<Entry>() {
071        @Override
072        public int compare(Entry left, Entry right) {
073          final long l = left.getExpirationTime();
074          final long r = right.getExpirationTime();
075          return l > r? 1: l < r? -1: 0;
076        }
077      };
078    
079      /** A clock for measuring time so that it can be mocked in unit tests. */
080      static class Clock {
081        /** @return the current time. */
082        long currentTime() {
083          return System.nanoTime();
084        }
085      }
086      
087      private static int updateRecommendedLength(int recommendedLength,
088          int sizeLimit) {
089        return sizeLimit > 0 && sizeLimit < recommendedLength?
090            (sizeLimit/4*3) // 0.75 load factor
091            : recommendedLength;
092      }
093    
094      /*
095       * The memory footprint for java.util.PriorityQueue is low but the
096       * remove(Object) method runs in linear time. We may improve it by using a
097       * balanced tree. However, we do not yet have a low memory footprint balanced
098       * tree implementation.
099       */
100      private final PriorityQueue<Entry> queue;
101      private final long creationExpirationPeriod;
102      private final long accessExpirationPeriod;
103      private final int sizeLimit;
104      private final Clock clock;
105    
106      /**
107       * @param recommendedLength Recommended size of the internal array.
108       * @param sizeLimit the limit of the size of the cache.
109       *            The limit is disabled if it is <= 0.
110       * @param creationExpirationPeriod the time period C > 0 in nanoseconds that
111       *            the creation of an entry is expired if it is added to the cache
112       *            longer than C.
113       * @param accessExpirationPeriod the time period A >= 0 in nanoseconds that
114       *            the access of an entry is expired if it is not accessed
115       *            longer than A. 
116       */
117      public LightWeightCache(final int recommendedLength,
118          final int sizeLimit,
119          final long creationExpirationPeriod,
120          final long accessExpirationPeriod) {
121        this(recommendedLength, sizeLimit,
122            creationExpirationPeriod, accessExpirationPeriod, new Clock());
123      }
124    
125      @VisibleForTesting
126      LightWeightCache(final int recommendedLength,
127          final int sizeLimit,
128          final long creationExpirationPeriod,
129          final long accessExpirationPeriod,
130          final Clock clock) {
131        super(updateRecommendedLength(recommendedLength, sizeLimit));
132    
133        this.sizeLimit = sizeLimit;
134    
135        if (creationExpirationPeriod <= 0) {
136          throw new IllegalArgumentException("creationExpirationPeriod = "
137              + creationExpirationPeriod + " <= 0");
138        }
139        this.creationExpirationPeriod = creationExpirationPeriod;
140    
141        if (accessExpirationPeriod < 0) {
142          throw new IllegalArgumentException("accessExpirationPeriod = "
143              + accessExpirationPeriod + " < 0");
144        }
145        this.accessExpirationPeriod = accessExpirationPeriod;
146    
147        this.queue = new PriorityQueue<Entry>(
148            sizeLimit > 0? sizeLimit + 1: 1 << 10, expirationTimeComparator);
149        this.clock = clock;
150      }
151    
152      void setExpirationTime(final Entry e, final long expirationPeriod) {
153        e.setExpirationTime(clock.currentTime() + expirationPeriod);
154      }
155    
156      boolean isExpired(final Entry e, final long now) {
157        return now > e.getExpirationTime();
158      }
159    
160      private E evict() {
161        @SuppressWarnings("unchecked")
162        final E polled = (E)queue.poll();
163        final E removed = super.remove(polled);
164        Preconditions.checkState(removed == polled);
165        return polled;
166      }
167    
168      /** Evict expired entries. */
169      private void evictExpiredEntries() {
170        final long now = clock.currentTime();
171        for(int i = 0; i < EVICTION_LIMIT; i++) {
172          final Entry peeked = queue.peek();
173          if (peeked == null || !isExpired(peeked, now)) {
174            return;
175          }
176    
177          final E evicted = evict();
178          Preconditions.checkState(evicted == peeked);
179        }
180      }
181    
182      /** Evict entries in order to enforce the size limit of the cache. */
183      private void evictEntries() {
184        if (sizeLimit > 0) {
185          for(int i = size(); i > sizeLimit; i--) {
186            evict();
187          }
188        }
189      }
190      
191      @Override
192      public E get(K key) {
193        final E entry = super.get(key);
194        if (entry != null) {
195          if (accessExpirationPeriod > 0) {
196            // update expiration time
197            final Entry existing = (Entry)entry;
198            Preconditions.checkState(queue.remove(existing));
199            setExpirationTime(existing, accessExpirationPeriod);
200            queue.offer(existing);
201          }
202        }
203        return entry;
204      }
205    
206      @Override
207      public E put(final E entry) {
208        if (!(entry instanceof Entry)) {
209          throw new HadoopIllegalArgumentException(
210              "!(entry instanceof Entry), entry.getClass()=" + entry.getClass());
211        }
212    
213        evictExpiredEntries();
214    
215        final E existing = super.put(entry);
216        if (existing != null) {
217          queue.remove(existing);
218        }
219    
220        final Entry e = (Entry)entry;
221        setExpirationTime(e, creationExpirationPeriod);
222        queue.offer(e);
223        
224        evictEntries();
225        return existing;
226      }
227    
228      @Override
229      public E remove(K key) {
230        evictExpiredEntries();
231    
232        final E removed = super.remove(key);
233        if (removed != null) {
234          Preconditions.checkState(queue.remove(removed));
235        }
236        return removed;
237      }
238    }