001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2016 ForgeRock AS. 015 */ 016 017package org.forgerock.util; 018 019import static org.forgerock.util.Reject.checkNotNull; 020import static org.forgerock.util.promise.Promises.newResultPromise; 021 022import java.util.concurrent.Callable; 023import java.util.concurrent.ConcurrentHashMap; 024import java.util.concurrent.ConcurrentMap; 025import java.util.concurrent.ExecutionException; 026import java.util.concurrent.Future; 027import java.util.concurrent.FutureTask; 028import java.util.concurrent.ScheduledExecutorService; 029import java.util.concurrent.ScheduledFuture; 030 031import org.forgerock.util.promise.Promise; 032import org.forgerock.util.promise.ResultHandler; 033import org.forgerock.util.time.Duration; 034 035/** 036 * PerItemEvictionStrategyCache is a thread-safe write-through cache. 037 * <p> 038 * Instead of storing directly the value in the backing Map, it requires the 039 * consumer to provide a value factory (a Callable). A new FutureTask 040 * encapsulates the callable, is executed and is placed inside a 041 * ConcurrentHashMap if absent. 042 * <p> 043 * The final behavior is that, even if two concurrent Threads are borrowing an 044 * object from the cache, given that they provide an equivalent value factory, 045 * the first one will compute the value while the other will get the result from 046 * the Future (and will wait until the result is computed or a timeout occurs). 047 * 048 * @param <K> 049 * Type of the key 050 * @param <V> 051 * Type of the value 052 */ 053public class PerItemEvictionStrategyCache<K, V> { 054 055 // @Checkstyle:off (automatic formatting to 16 but Checkstyle expects 8 or 12) 056 private static final Function<Exception, Duration, Exception> ON_EXCEPTION_NO_TIMEOUT = 057 new Function<Exception, Duration, Exception>() { 058 @Override 059 public Duration apply(Exception e) throws Exception { 060 return Duration.ZERO; 061 } 062 }; 063 // @Checkstyle:on 064 065 private final ScheduledExecutorService executorService; 066 private final ConcurrentMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>(); 067 private final AsyncFunction<V, Duration, Exception> defaultTimeoutFunction; 068 private Duration maxTimeout; 069 070 /** 071 * Build a new {@link PerItemEvictionStrategyCache} using the given scheduled executor. 072 * 073 * @param executorService 074 * scheduled executor for registering expiration callbacks. 075 * @param defaultTimeout 076 * the default cache entry timeout 077 */ 078 public PerItemEvictionStrategyCache(final ScheduledExecutorService executorService, final Duration defaultTimeout) { 079 this(executorService, new AsyncFunction<V, Duration, Exception>() { 080 @Override 081 public Promise<Duration, Exception> apply(V value) { 082 return newResultPromise(defaultTimeout); 083 } 084 }); 085 } 086 087 /** 088 * Build a new {@link PerItemEvictionStrategyCache} using the given scheduled executor. 089 * 090 * @param executorService 091 * scheduled executor for registering expiration callbacks. 092 * @param defaultTimeoutFunction 093 * the function that will compute the cache entry timeout (must not be {@literal null}) 094 * the default timeout to cache the entries 095 */ 096 public PerItemEvictionStrategyCache(final ScheduledExecutorService executorService, 097 final AsyncFunction<V, Duration, Exception> defaultTimeoutFunction) { 098 this.executorService = checkNotNull(executorService); 099 this.defaultTimeoutFunction = checkNotNull(defaultTimeoutFunction); 100 } 101 102 /** 103 * Borrow (and create before hand if absent) a cache entry. If another 104 * Thread has created (or the creation is undergoing) the value, this 105 * method waits indefinitely for the value to be available. 106 * 107 * @param key 108 * entry key 109 * @param callable 110 * cached value factory 111 * @return the cached value 112 * @throws InterruptedException 113 * if the current thread was interrupted while waiting 114 * @throws ExecutionException 115 * if the cached value computation threw an exception 116 */ 117 public V getValue(final K key, final Callable<V> callable) throws InterruptedException, 118 ExecutionException { 119 return getValue(key, callable, defaultTimeoutFunction); 120 } 121 122 /** 123 * Borrow (and create before hand if absent) a cache entry. If another 124 * Thread has created (or the creation is undergoing) the value, this 125 * method waits indefinitely for the value to be available. 126 * 127 * @param key 128 * entry key 129 * @param callable 130 * cached value factory 131 * @param expire 132 * function to override the global cache's timeout 133 * @return the cached value 134 * @throws InterruptedException 135 * if the current thread was interrupted while waiting 136 * @throws ExecutionException 137 * if the cached value computation threw an exception 138 */ 139 public V getValue(final K key, final Callable<V> callable, final AsyncFunction<V, Duration, Exception> expire) 140 throws InterruptedException, ExecutionException { 141 try { 142 return createIfAbsent(key, callable, expire).get(); 143 } catch (InterruptedException | RuntimeException | ExecutionException e) { 144 evict(key); 145 throw e; 146 } 147 } 148 149 private Future<V> createIfAbsent(final K key, final Callable<V> callable, 150 final AsyncFunction<V, Duration, Exception> timeoutFunction) 151 throws InterruptedException, ExecutionException { 152 // See the javadoc of the class for the intent of the Future and FutureTask. 153 CacheEntry<V> cacheEntry = cache.get(key); 154 if (cacheEntry == null) { 155 // First call: no value cached for that key 156 final FutureTask<V> futureTask = new FutureTask<>(callable); 157 final CacheEntry<V> futureCacheEntry = new CacheEntry<>(futureTask); 158 cacheEntry = cache.putIfAbsent(key, futureCacheEntry); 159 if (cacheEntry == null) { 160 // after the double check, it seems we are still the first to want to cache that value. 161 cacheEntry = futureCacheEntry; 162 163 // Compute the value 164 futureTask.run(); 165 166 scheduleEviction(key, futureCacheEntry, timeoutFunction); 167 } 168 } 169 return cacheEntry.getFutureTask(); 170 } 171 172 private void scheduleEviction(final K key, final CacheEntry<V> cacheEntry, 173 final AsyncFunction<V, Duration, Exception> timeoutFunction) 174 throws ExecutionException, InterruptedException { 175 newResultPromise(cacheEntry.getFutureTask().get()) 176 .thenAsync(timeoutFunction) 177 .thenCatch(ON_EXCEPTION_NO_TIMEOUT) 178 .thenCatchRuntimeException(ON_EXCEPTION_NO_TIMEOUT) 179 .thenOnResult(new ResultHandler<Duration>() { 180 @Override 181 public void handleResult(Duration timeout) { 182 Runnable eviction = new Runnable() { 183 @Override 184 public void run() { 185 // The cache can be cleared and another entry for the same key can be created 186 // before the eviction is really scheduled : so ensure that we remove the expected 187 // cache entry 188 if (cache.remove(key, cacheEntry)) { 189 cacheEntry.cancelExpiration(); 190 } 191 } 192 }; 193 194 if (timeout == null || timeout.isZero()) { 195 // Fast path : no need to schedule, evict it now 196 // Do not do "executorService.execute(eviction);" as we have no real guarantee that it will 197 // be executed now 198 eviction.run(); 199 } else { 200 // Cap the timeout if requested 201 if (maxTimeout != null) { 202 timeout = timeout.compareTo(maxTimeout) < 0 ? timeout : maxTimeout; 203 } 204 205 if (!timeout.isUnlimited()) { 206 // Schedule the eviction 207 ScheduledFuture<?> scheduledFuture = executorService.schedule(eviction, 208 timeout.getValue(), timeout.getUnit()); 209 cacheEntry.setScheduledHandler(scheduledFuture); 210 } 211 } 212 } 213 }); 214 } 215 216 /** 217 * Clean-up the cache entries. 218 */ 219 public void clear() { 220 for (K key : cache.keySet()) { 221 evict(key); 222 } 223 } 224 225 /** 226 * Returns the number of cached values. 227 * 228 * @return the number of cached values 229 */ 230 public int size() { 231 return cache.size(); 232 } 233 234 /** 235 * Returns whether this cache is empty or not. 236 * 237 * @return {@literal true} if the cache does not contain any values, {@literal false} otherwise. 238 */ 239 public boolean isEmpty() { 240 return cache.isEmpty(); 241 } 242 243 /** 244 * Evict a cached value from the cache. 245 * 246 * @param key 247 * the entry key 248 */ 249 public void evict(K key) { 250 CacheEntry<V> entry = cache.remove(key); 251 if (entry != null) { 252 entry.cancelExpiration(); 253 } 254 } 255 256 /** 257 * Gets the maximum timeout (can be {@literal null}). 258 * 259 * @return the maximum timeout 260 */ 261 public Duration getMaxTimeout() { 262 return maxTimeout; 263 } 264 265 /** 266 * Sets the maximum timeout. If the timeout returned by the {@literal timeoutFunction} is greater than this 267 * specified maximum timeout, then the maximum timeout is used instead of the returned one to cache the entry. 268 * 269 * @param maxTimeout 270 * the maximum timeout to use. 271 */ 272 public void setMaxTimeout(Duration maxTimeout) { 273 this.maxTimeout = maxTimeout; 274 } 275 276 private static class CacheEntry<V> { 277 private final FutureTask<V> futureTask; 278 private ScheduledFuture<?> scheduledHandler; 279 280 CacheEntry(FutureTask<V> futureTask) { 281 this.futureTask = futureTask; 282 } 283 284 void setScheduledHandler(ScheduledFuture<?> scheduledHandler) { 285 this.scheduledHandler = scheduledHandler; 286 } 287 288 FutureTask<V> getFutureTask() { 289 return futureTask; 290 } 291 292 void cancelExpiration() { 293 if (scheduledHandler != null) { 294 scheduledHandler.cancel(false); 295 } 296 } 297 } 298}