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}