/*
* Copyright (C) 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.concurrency;
import org.joda.time.DateTimeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.AbstractMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import com.facebook.collections.TranslatingIterator;
import com.facebook.collectionsbase.Mapper;
import com.facebook.util.exceptions.ExceptionHandler;
public class ExpiringConcurrentCache<K, V, E extends Exception>
implements ConcurrentCache<K, V, E> {
private static final Logger LOG = LoggerFactory.getLogger(ExpiringConcurrentCache.class);
private final ConcurrentCache<K, CacheEntry<V, E>, E> baseCache;
private final long maxAgeMillis;
private final ExecutorService executor;
// track the last time a prune operation was performed. Objects
// will only be pruned after maxAgeMillis has passed
private final AtomicLong lastPrune = new AtomicLong(
DateTimeUtils.currentTimeMillis()
);
private final AtomicBoolean pruning = new AtomicBoolean(false);
// an EvictionListener provides a memory efficient way for clients of this
// object to receive information about both the key and value evicted.
private final EvictionListener<K, V> evictionListener;
public ExpiringConcurrentCache(
ValueFactory<K, V, E> valueFactory,
long maxAge,
TimeUnit maxAgeUnit,
EvictionListener<K, V> evictionListener,
ExceptionHandler<E> exceptionHandler,
ExecutorService executor
) {
this.evictionListener = evictionListener;
this.baseCache =
new CoreConcurrentCache<>(
new CacheEntryValueFactory(valueFactory),
exceptionHandler
);
this.maxAgeMillis = maxAgeUnit.toMillis(maxAge);
this.executor = executor;
}
public ExpiringConcurrentCache(
ValueFactory<K, V, E> valueFactory,
long maxAge,
TimeUnit maxAgeUnit,
EvictionListener<K, V> evictionListener,
ExceptionHandler<E> exceptionHandler
) {
this(
valueFactory,
maxAge,
maxAgeUnit,
evictionListener,
exceptionHandler,
Executors.newSingleThreadExecutor()
);
}
/**
* compatibility function for use with legacy implementations that use
* Reapable to be notified of evictions
*
* @param valueFactory
* @param maxAge
* @param maxAgeUnit
* @param exceptionHandler
* @param executor
* @param <K>
* @param <V>
* @param <E>
* @return
*/
public static <K, V extends Reapable<? extends Exception>, E extends Exception>
ExpiringConcurrentCache<K, V, E> createWithReapableValue(
ValueFactory<K, V, E> valueFactory,
long maxAge,
TimeUnit maxAgeUnit,
ExceptionHandler<E> exceptionHandler,
ExecutorService executor
) {
return new ExpiringConcurrentCache<>(
valueFactory,
maxAge,
maxAgeUnit,
(key, value) -> {
try {
value.shutdown();
} catch (Throwable t) {
LOG.error("error shutting down reapable", t);
}
},
exceptionHandler,
executor
);
}
@Override
public V get(K key) throws E {
CacheEntry<V, E> cacheEntry = baseCache.get(key);
CallableSnapshot<V, E> snapshot = cacheEntry.touch();
// prune after getting the value
pruneIfNeeded();
return snapshot.get();
}
@Override
public V put(K key, V value) throws E {
pruneIfNeeded();
CacheEntry<V, E> cacheEntry = new CacheEntry<>(value);
CacheEntry<V, E> existingCacheEntry = baseCache.put(key, cacheEntry);
return existingCacheEntry == null ? null : existingCacheEntry.getSnapshot().get();
}
@Override
public V remove(K key) throws E {
pruneIfNeeded();
CacheEntry<V, E> cacheEntry = baseCache.remove(key);
return cacheEntry == null ? null : cacheEntry.getSnapshot().get();
}
@Override
public boolean removeIfError(K key) {
return baseCache.removeIfError(key);
}
@Override
public void clear() {
baseCache.clear();
}
@Override
public void prune() throws E {
pruneIfNeeded();
}
@Override
public int size() {
return baseCache.size();
}
/**
* non-blocking, thread-safe prune operation that only enters pruning block
* after enough time has elapsed
*
* @throws E
*/
private void pruneIfNeeded() {
// only prune if sufficient time has elapsed and another thread isn't
// already pruning
if (DateTimeUtils.currentTimeMillis() - lastPrune.get() >= maxAgeMillis &&
pruning.compareAndSet(false, true)) {
try {
Iterator<Map.Entry<K, CallableSnapshot<CacheEntry<V, E>, E>>> iterator =
baseCache.iterator();
while (iterator.hasNext()) {
final K key;
final CacheEntry<V, E> cacheEntry;
try {
Map.Entry<K, CallableSnapshot<CacheEntry<V, E>, E>> entry =
iterator.next();
key = entry.getKey();
cacheEntry = entry.getValue().get();
} catch (Exception e) {
// We control the creation process, so should not get an exception
throw new RuntimeException("CacheEntry create should not fail");
}
if (cacheEntry.hasExpired(maxAgeMillis)) {
// remove the item from the cache
iterator.remove();
// do any shutdown() tasks asynchronously so we don't block access
// to the cache
executor.execute(
() -> {
// now reap the entry
try {
V value = cacheEntry.getSnapshot().get();
try {
evictionListener.evicted(key, value);
} catch (Throwable t) {
LOG.error(
"Error reaping cache element-- may not be properly closed",
t
);
}
} catch (Exception e) {
LOG.info(
"Unable to get cache value for key " + key
);
// still notify that key is evicted
evictionListener.evicted(key, null);
}
}
);
}
}
} finally {
lastPrune.set(DateTimeUtils.currentTimeMillis());
pruning.set(false);
}
}
}
@Override
public Iterator<Map.Entry<K, CallableSnapshot<V, E>>> iterator() {
return new TranslatingIterator<>(
new ValueMapper(), baseCache.iterator()
);
}
@Override
public CallableSnapshot<V, E> getIfPresent(K key) {
pruneIfNeeded();
CallableSnapshot<CacheEntry<V, E>, E> snapshot =
baseCache.getIfPresent(key);
if (snapshot == null) {
return null;
} else {
try {
return snapshot.get().getSnapshot();
} catch (Exception e) {
throw new RuntimeException("this shouldn't happen", e);
}
}
}
private class ValueMapper implements
Mapper<
Map.Entry<K, CallableSnapshot<CacheEntry<V, E>, E>>,
Map.Entry<K, CallableSnapshot<V, E>>
> {
@Override
public Map.Entry<K, CallableSnapshot<V, E>> map(
Map.Entry<K, CallableSnapshot<CacheEntry<V, E>, E>> input
) {
CallableSnapshot<V, E> snapshot;
try {
snapshot = input.getValue().get().touch();
} catch (Exception e) {
// We control the creation process, so we should not get an exception
throw new RuntimeException("CacheEntry create should not fail");
}
return new AbstractMap.SimpleImmutableEntry<>(
input.getKey(),
snapshot
);
}
}
private class CacheEntryValueFactory
implements ValueFactory<K, CacheEntry<V, E>, E> {
CallableSnapshotFunction<K, V, E> snapshotFunction;
private CacheEntryValueFactory(ValueFactory<K, V, E> valueFactory) {
snapshotFunction =
new PrivateCallableSnapshotFunction<>(valueFactory);
}
@Override
public CacheEntry<V, E> create(K input) {
return new CacheEntry<>(snapshotFunction.apply(input));
}
}
/**
* a cache entry is a value and it's last accessed time (create, read).
* The last accessed is used for expiring entire older than a configured
* TTL by the cache
*
* @param <V> value type
* @param <E> exception type
*/
@SuppressWarnings({"unchecked"})
private static class CacheEntry<V, E extends Exception> {
// mtime guarded by this
private long mtime = DateTimeUtils.currentTimeMillis();
private volatile Object snapshotOrValue;
private CacheEntry(V value) {
this.snapshotOrValue = value;
}
private CacheEntry(CallableSnapshot<V, E> snapshot) {
// if the snapshot indicates no error, store just the value. This
// will save us about 24 bytes on a 64-bit box: 2 x 8 byte ptr and
// the 8-byte overhead java adds for each object (saved by not
// having the CallableSnapshot)
if (snapshot.getException() == null) {
try {
snapshotOrValue = snapshot.get();
} catch (Exception e) {
LOG.error("this should NEVER be seen", e);
snapshotOrValue = snapshot;
}
} else {
snapshotOrValue = snapshot;
}
}
public CallableSnapshot<V, E> getSnapshot() throws E {
return getCallableSnapshot();
}
public synchronized CallableSnapshot<V, E> touch() {
mtime = DateTimeUtils.currentTimeMillis();
return getCallableSnapshot();
}
public synchronized boolean hasExpired(long maxAgeMillis) {
return DateTimeUtils.currentTimeMillis() - mtime >= maxAgeMillis;
}
/**
* we store either the result of the Callable if there is no
* exception (saves memory). Otherewise, we keep the whole CallableSnapshot
*
* @return
*/
private CallableSnapshot<V, E> getCallableSnapshot() {
if (snapshotOrValue instanceof PrivateCallableSnapshot) {
return (CallableSnapshot<V, E>) snapshotOrValue;
} else {
// we can use NullExceptionHandler since we know
// this is a value
return new PrivateCallableSnapshot<>(
new FixedValueCallable<>((V) snapshotOrValue),
new NullExceptionHandler<>()
);
}
}
}
// this private class is using it's class type as a boolean flag (to save
// memory). If the value stored is of this type in the CacheEntry,
// then it means we couldn't store the value and need to call
// CallableSnapshot.get(). By making it private, we guarantee that O
// cannot be this type
private static class PrivateCallableSnapshotFunction
<I, O, E extends Exception>
implements CallableSnapshotFunction<I, O, E> {
private final ValueFactory<I, O, E> valueFactory;
private final ExceptionHandler<E> exceptionHandler;
private PrivateCallableSnapshotFunction(
ValueFactory<I, O, E> valueFactory, ExceptionHandler<E> exceptionHandler
) {
this.valueFactory = valueFactory;
this.exceptionHandler = exceptionHandler;
}
private PrivateCallableSnapshotFunction(ValueFactory<I, O, E> valueFactory) {
// We can cast exceptions because the value factory declares which type
// of exceptions it can throw on creation
this(valueFactory, new CastingExceptionHandler<>());
}
@Override
public CallableSnapshot<O, E> apply(final I input) {
return new PrivateCallableSnapshot<>(
() -> valueFactory.create(input),
exceptionHandler
);
}
}
private static class PrivateCallableSnapshot<V, E extends Exception>
extends CallableSnapshot<V, E> {
private PrivateCallableSnapshot(
Callable<V> callable,
ExceptionHandler<E> exceptionHandler
) {
super(callable, exceptionHandler);
}
}
}