// 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 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 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; }; } }