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 }