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 }