com.yahoo.yolean.concurrent.Memoized Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vespajlib Show documentation
Show all versions of vespajlib Show documentation
Library for use in Java components of Vespa. Shared code which do
not fit anywhere else.
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.yolean.concurrent;
import com.yahoo.api.annotations.Beta;
import java.util.function.Function;
import java.util.function.Supplier;
import static java.util.Objects.requireNonNull;
/**
* Wraps a lazily initialised resource which needs to be shut down.
* The wrapped supplier may not return {@code null}, and should be retryable on failure.
* If it throws, it will be retried if {@link #get} is retried. A supplier that fails to
* clean up partial state on failure may cause a resource leak.
*
* @author jonmv
*/
@Beta
public class Memoized implements Supplier, AutoCloseable {
/**
* Provides a tighter bound on the thrown exception type.
*/
@FunctionalInterface
public interface Closer {
void close(T t) throws E;
}
private final Object monitor = new Object();
private final Closer closer;
private volatile T wrapped;
private Supplier factory;
/** Returns a new Memoized which has no close method. */
public Memoized(Supplier factory) {
this(factory, __ -> { });
}
/** Returns a new Memoized with the given factory and closer. */
public Memoized(Supplier factory, Closer closer) {
this.factory = requireNonNull(factory);
this.closer = requireNonNull(closer);
}
/** Returns a generic AutoCloseable Memoized with the given AutoCloseable-supplier. */
public static Memoized of(Supplier factory) {
return new Memoized<>(factory, AutoCloseable::close);
}
/** Composes the given memoized with a function taking its output as an argument to produce a new Memoized, with the given closer. */
public static Memoized combine(Memoized inner, Function outer, Closer closer) {
return new Memoized<>(() -> outer.apply(inner.get()), compose(closer, inner::close));
}
@Override
public T get() {
// Double-checked locking: try the _volatile_ variable, and if not initialized, try to initialize it.
if (wrapped == null) synchronized (monitor) {
// Ensure the factory is called only once, by clearing it once successfully called.
if (factory != null) wrapped = requireNonNull(factory.get());
factory = null;
// If we found the factory, we won the initialization race, and return normally; otherwise
// if wrapped is non-null, we lost the race, wrapped was set by the winner, and we return; otherwise
// we tried to initialise because wrapped was cleared by closing this, and we fail.
if (wrapped == null) throw new IllegalStateException("already closed");
}
return wrapped;
}
@Override
public void close() throws E {
// Alter state only when synchronized with calls to get().
synchronized (monitor) {
// Ensure we only try to close the generated resource once, by clearing it after picking it up here.
T maybe = wrapped;
wrapped = null;
// Clear the factory, to signal this has been closed.
factory = null;
if (maybe != null) closer.close(maybe);
}
}
private interface Thrower { void call() throws E; }
private static Closer compose(Closer outer, Thrower extends E> inner) {
return parent -> {
Exception thrown = null;
try {
outer.close(parent);
}
catch (Exception e) {
thrown = e;
}
try {
inner.call();
}
catch (Exception e) {
if (thrown != null) thrown.addSuppressed(e);
else thrown = e;
}
@SuppressWarnings("unchecked")
E e = (E) thrown;
if (e != null) throw e;
};
}
}