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 }