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}