
net.moznion.auto_refresh_cache.AutoRefreshCache Maven / Gradle / Ivy
package net.moznion.auto_refresh_cache;
import lombok.extern.slf4j.Slf4j;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.function.Supplier;
/**
* Cached object that can be refreshed automatically when cache is expired.
*
* It holds the same object until cache is expired,
* and it refreshes the object by given supplier automatically when cache is expired.
*
* @param Type of cache object.
*/
@Slf4j
public class AutoRefreshCache {
private final long discardIntervalSec;
private final Supplier supplier;
private final Semaphore semaphore;
private final boolean useBeforeCacheOnException;
private volatile long expiresAt;
private volatile T cached;
/**
* A constructor.
*
* @param discardIntervalSec Lifetime of cache object (seconds). If this interval is over, cache object will be refreshed by given supplier.
* @param supplier Supplier of cache object. This supplier is used when making a cache object and refreshing one.
* @param useBeforeCacheOnException If true, it returns already cached object and suppresses exception when supplier raises some exception. Otherwise, it throws exception as it is.
*/
public AutoRefreshCache(final long discardIntervalSec, final boolean useBeforeCacheOnException, final Supplier supplier) {
this(supplier.get(), discardIntervalSec, useBeforeCacheOnException, supplier);
}
/**
* A constructor with initial cached object.
*
* @param init the initial cached object.
* @param discardIntervalSec Lifetime of cache object (seconds). If this interval is over, cache object will be refreshed by given supplier.
* @param supplier Supplier of cache object. This supplier is used when making a cache object and refreshing one.
* @param useBeforeCacheOnException If true, it returns already cached object and suppresses exception when supplier raises some exception. Otherwise, it throws exception as it is.
*/
public AutoRefreshCache(final T init, final long discardIntervalSec, final boolean useBeforeCacheOnException, final Supplier supplier) {
this.discardIntervalSec = discardIntervalSec;
this.supplier = supplier;
this.useBeforeCacheOnException = useBeforeCacheOnException;
cached = init;
expiresAt = getExpiresAt(discardIntervalSec);
semaphore = new Semaphore(1);
}
/**
* Get cached or refreshed object.
*
* @return When cache is alive, it returns a cached object. Otherwise, it returns an object that is refreshed by supplier.
*/
public T get() {
return get(getCurrentEpoch(), false);
}
/**
* Get cached object. And schedule to refresh cache if cache is expired.
*
* This method doesn't get refreshed cache object even if cache is expired; refreshed cache object will be available from the next calling.
* When cache is expired, this method delegates to refresh processing to another thread.
* It means refreshing processing is executed as asynchronous on other thread.
*
* To describe in other words, this method retrieves always already cached object.
* And schedules a task to refresh cache when cache is expired.
*
* @return Already cached object.
*/
public T getWithRefreshScheduling() {
return get(getCurrentEpoch(), true);
}
/**
* Get refreshed object.
*
* It returns always refreshed object and extends lifetime.
* But if other thread is attempting to refresh, this method returns the cached object that is not refreshed.
*
* This method runs as exclusive between threads to ensure atomicity of updating cached object and lifetime.
*
* @return Refreshed object.
*/
public T forceGet() {
return forceGet(false);
}
/**
* Get cached object with scheduling to refresh cache always.
*
* This method doesn't get refreshed cache object; refreshed cache object will be available from the next calling.
* This method delegates to refresh processing to another thread.
* It means refreshing processing is executed as asynchronous on other thread.
*
* To describe in other words, this method retrieves always already cached object.
* And schedules a task to refresh cache.
*
* @return Already cached object.
*/
public T forceGetWithRefreshScheduling() {
return forceGet(true);
}
T get(final long currentEpoch, final boolean isDelayed) {
if (expiresAt < currentEpoch) {
// Expired. Fill new instance
return forceGet(isDelayed);
}
return cached;
}
T forceGet(final boolean isScheduledRefreshing) {
if (!semaphore.tryAcquire()) {
// If attempt to get cached object while refreshing that by other thread, current thread returns old cache.
return cached;
}
final Runnable refresher = () -> {
try {
this.cached = supplier.get();
} catch (RuntimeException e) {
if (!useBeforeCacheOnException) {
semaphore.release();
throw e;
}
log.warn("Failed to refresh cache so use old cache", e);
}
expiresAt = getExpiresAt(discardIntervalSec);
semaphore.release();
};
if (isScheduledRefreshing) {
final T currentCached = cached;
final ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(refresher);
pool.shutdown();
return currentCached;
}
refresher.run();
return cached;
}
private static long getCurrentEpoch() {
return Instant.now().getEpochSecond();
}
private static long getExpiresAt(final long discardIntervalSec) {
return getCurrentEpoch() + discardIntervalSec;
}
}