All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fiolino.common.util.Cached Maven / Gradle / Ivy

Go to download

General structure to easily create dynamic logic via MethodHandles and others.

There is a newer version: 1.0.10
Show newest version
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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy