org.fiolino.common.util.Cached Maven / Gradle / Ivy
Show all versions of commons Show documentation
package org.fiolino.common.util;
import javax.annotation.Nullable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
/**
* This class can be used to hold values that usually don't change,
* but should be frequently updated to accept changes if there are some.
*
* This is best used for tasks that need some resources, but not that many
* that frequent updates would hurt. An example is some data read from the
* file system.
*
* Values are calculated by either a {@link Supplier} or a {@link UnaryOperator}, which gets the old
* cached value as the parameter. This can be useful for costly operations which can check whether
* an update is necessary at all.
*
* Operators are only called once even in concurrent access. They don't need to be thread safe even in concurrent
* environments.
*
* Example:
*
* Cached<DataObject> valueHolder = Cached.updateEvery(5).hours().with(() -> new DataObject(...));
*
*
* @author kuli
*/
public final class Cached implements Supplier {
/**
* This is the starting factory method, followed by a method for the time unit.
*
* @param value How many time units shall the cache keep its value.
*/
public static ExpectUnit updateEvery(long value) {
return new ExpectUnit(value);
}
/**
* This is the factory method which assigns a duration in text form, including the delay and the time unit.
*
* Examples:
* 1 Day
* 5 sec
* 900 millis
*
* The time unit must be a unique start of some {@link TimeUnit} name. If none is given, seconds are assumed.
*/
public static ExpectEvaluator updateEvery(String value) {
TimeUnit u = findFrom(value);
String delay = value.replaceAll("\\D", "");
if (delay.equals("")) {
throw new IllegalArgumentException(value + " is missing the delay value");
}
return new ExpectEvaluator(u.toMillis(Long.parseLong(delay)));
}
/**
* Use this factory to create a cached instance which calls its operator in every call.
*/
public static ExpectEvaluator updateAlways() {
return new ExpectEvaluator(-1);
}
/**
* Use this factory to create a cached instance which calls its operator only for initialization.
*/
public static ExpectEvaluator forever() {
return new ExpectEvaluator(Long.MAX_VALUE);
}
/**
* Shortcut to create a Cached instance that gets initialized by some supplier and then always returns
* that value.
*
* @param eval Computes the initial value
* @param The type
* @return A Cached instance
*/
public static Cached with(Supplier eval) {
return forever().with(eval);
}
public static final class ExpectUnit {
private final long value;
private ExpectUnit(long value) {
this.value = value;
}
/**
* Sets the expiration timeout unit to nano seconds.
*
* This method follows a with().
*/
public ExpectEvaluator nanoseconds() {
return new ExpectEvaluator(TimeUnit.NANOSECONDS.toMillis(value));
}
/**
* Sets the expiration timeout unit to micro seconds.
*
* This method follows a with().
*/
public ExpectEvaluator microseconds() {
return new ExpectEvaluator(TimeUnit.MICROSECONDS.toMillis(value));
}
/**
* Sets the expiration timeout unit to milli seconds.
*
* This method follows a with().
*/
public ExpectEvaluator milliseconds() {
return new ExpectEvaluator(value);
}
/**
* Sets the expiration timeout unit to seconds.
*
* This method follows a with().
*/
public ExpectEvaluator seconds() {
return new ExpectEvaluator(TimeUnit.SECONDS.toMillis(value));
}
/**
* Sets the expiration timeout unit to minutes.
*
* This method follows a with().
*/
public ExpectEvaluator minutes() {
return new ExpectEvaluator(TimeUnit.MINUTES.toMillis(value));
}
/**
* Sets the expiration timeout unit to hours.
*
* This method follows a with().
*/
public ExpectEvaluator hours() {
return new ExpectEvaluator(TimeUnit.HOURS.toMillis(value));
}
/**
* Sets the expiration timeout unit to days.
*
* This method follows a with().
*/
public ExpectEvaluator days() {
return new ExpectEvaluator(TimeUnit.DAYS.toMillis(value));
}
}
private static TimeUnit findFrom(String desc) {
String textOnly = desc.replaceAll("\\W", "").toUpperCase();
if (textOnly.equals("")) {
// No unit given assume seconds
return TimeUnit.SECONDS;
}
TimeUnit found = null;
for (TimeUnit u : TimeUnit.values()) {
if (u.name().startsWith(textOnly)) {
if (found != null) {
throw new IllegalArgumentException(desc + " has ambiguous time unit");
}
found = u;
}
}
if (found == null) {
throw new IllegalArgumentException(desc + " does not describe a time unit");
}
return found;
}
public static final class ExpectEvaluator {
final long milliseconds;
private ExpectEvaluator(long milliseconds) {
this.milliseconds = milliseconds;
}
/**
* Assigns an initial value and an operator that updates any existing value initially and after expiry.
*
* @param initialValue This is used for the first call to the operator.
* @param eval This gets evaluated first and after each timeout
* @param The cached type
* @return The cache instance. This can be used now.
*/
public Cached with(@Nullable T initialValue, UnaryOperator eval) {
return new Cached(milliseconds, initialValue, eval);
}
/**
* Initially and after each expiry, a new value is calculated via this Callable instance.
* Expired values will be discarded completely.
*
* @param eval This gets evaluated first and after each timeout
* @param The cached type
* @return The cache instance. This can be used now.
*/
public Cached with(Supplier eval) {
return with(null, v -> eval.get());
}
}
private volatile boolean isInitialized;
private volatile T instance;
private volatile long lastUpdate;
private final long refreshRate;
private final UnaryOperator evaluator;
private final Semaphore updateResource = new Semaphore(1);
private Cached(long refreshRate, T initialValue, UnaryOperator evaluator) {
this.instance = initialValue;
this.refreshRate = refreshRate;
this.evaluator = evaluator;
}
/**
* Gets the cached value.
*
* Update the cached value, if refresh rate has expired.
*/
@Override
public T get() {
T value;
do {
value = instance;
} while (neededRefresh());
return value;
}
private boolean isValid() {
return System.currentTimeMillis() - lastUpdate <= refreshRate;
}
private boolean neededRefresh() {
// Unsafe.loadFence() -- then lastUpdate could be non-volatile
if (isValid()) {
return false;
}
tryRefresh();
return true;
}
/**
* Refreshes the value to an updated instance.
* This either starts the refresh process immediately, or it waits until another updating thread has finished.
*/
public void refresh() {
isInitialized = false;
if (!tryRefresh()) {
spinWait();
}
}
private boolean tryRefresh() {
if (updateResource.tryAcquire()) {
try {
T value;
try {
value = evaluator.apply(instance);
} catch (RefreshNotPossibleException ex) {
return true;
}
if (value == null) {
throw new NullPointerException("Evaluator " + evaluator + " returned null value");
}
lastUpdate = System.currentTimeMillis();
isInitialized = true;
instance = value;
} finally {
updateResource.release();
}
return true;
} else {
waitIfUninitialized();
}
return false;
}
private void waitIfUninitialized() {
while (!isInitialized) {
spinWait();
}
}
private void spinWait() {
try {
updateResource.acquire();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new CancellationException("Thread is interrupted, " + evaluator + " may return uninitialized null value.");
}
updateResource.release();
}
/**
* Assigned Operators can throw this to indicate that a refresh is not possible yet,
* and the cached value shall remain until it's possible again.
*/
public static class RefreshNotPossibleException extends RuntimeException {
private static final long serialVersionUID = 5734137134481666718L;
}
}