001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.lang3.concurrent;
018
019import java.util.concurrent.CancellationException;
020import java.util.concurrent.ConcurrentHashMap;
021import java.util.concurrent.ConcurrentMap;
022import java.util.concurrent.ExecutionException;
023import java.util.concurrent.Future;
024import java.util.concurrent.FutureTask;
025import java.util.function.Function;
026
027import org.apache.commons.lang3.exception.ExceptionUtils;
028
029/**
030 * Definition of an interface for a wrapper around a calculation that takes a single parameter and returns a result. The
031 * results for the calculation will be cached for future requests.
032 *
033 * <p>
034 * This is not a fully functional cache: it is unbounded, and there is no way of limiting or removing results once they
035 * have been generated. In particular, note the exception-caching default: unless the {@code recalculate} constructor
036 * option is set to {@code true}, the <em>first</em> exception thrown by a calculation for a given parameter is cached
037 * and rethrown for every future call with that parameter for the lifetime of this instance - a single transient
038 * failure permanently poisons that key. Set {@code recalculate} to {@code true} to retry failed calculations on
039 * subsequent calls instead.
040 * </p>
041 * <p>
042 * Thanks go to Brian Goetz, Tim Peierls and the members of JCP JSR-166 Expert Group for coming up with the
043 * original implementation of the class. It was also published within Java Concurrency in Practice as a sample.
044 * </p>
045 *
046 * @param <I> The type of the input to the calculation
047 * @param <O> The type of the output of the calculation
048 * @since 3.6
049 */
050public class Memoizer<I, O> implements Computable<I, O> {
051
052    private final ConcurrentMap<I, Future<O>> cache = new ConcurrentHashMap<>();
053    private final Function<? super I, FutureTask<O>> mappingFunction;
054    private final boolean recalculate;
055
056    /**
057     * Constructs a Memoizer for the provided Computable calculation.
058     *
059     * <p>
060     * If a calculation throws an exception for any reason, this exception will be cached and returned for all future
061     * calls with the provided parameter.
062     * </p>
063     *
064     * @param computable The computation whose results should be memorized
065     */
066    public Memoizer(final Computable<I, O> computable) {
067        this(computable, false);
068    }
069
070    /**
071     * Constructs a Memoizer for the provided Computable calculation, with the option of whether a Computation that
072     * experiences an error should recalculate on subsequent calls or return the same cached exception.
073     *
074     * @param computable The computation whose results should be memorized
075     * @param recalculate determines whether the computation should be recalculated on subsequent calls if the previous call
076     *        failed
077     */
078    public Memoizer(final Computable<I, O> computable, final boolean recalculate) {
079        this.recalculate = recalculate;
080        this.mappingFunction = k -> new FutureTask<>(() -> computable.compute(k));
081    }
082
083    /**
084     * Constructs a Memoizer for the provided Function calculation.
085     *
086     * <p>
087     * If a calculation throws an exception for any reason, this exception will be cached and returned for all future
088     * calls with the provided parameter.
089     * </p>
090     *
091     * @param function The function whose results should be memorized
092     * @since 2.13.0
093     */
094    public Memoizer(final Function<I, O> function) {
095        this(function, false);
096    }
097
098    /**
099     * Constructs a Memoizer for the provided Function calculation, with the option of whether a Function that
100     * experiences an error should recalculate on subsequent calls or return the same cached exception.
101     *
102     * @param function The computation whose results should be memorized
103     * @param recalculate determines whether the computation should be recalculated on subsequent calls if the previous call
104     *        failed
105     * @since 2.13.0
106     */
107     public Memoizer(final Function<I, O> function, final boolean recalculate) {
108        this.recalculate = recalculate;
109        this.mappingFunction = k -> new FutureTask<>(() -> function.apply(k));
110    }
111
112    /**
113     * This method will return the result of the calculation and cache it, if it has not previously been calculated.
114     *
115     * <p>
116     * This cache will also cache exceptions that occur during the computation if the {@code recalculate} parameter in the
117     * constructor was set to {@code false}, or not set: the first exception thrown for a given argument is rethrown for
118     * every future call with that argument. Otherwise, if an exception happened on the previous calculation,
119     * the method will attempt again to generate a value.
120     * </p>
121     * <p>
122     * The calculation for a given argument runs at most once per cached entry and executes <em>outside</em> any internal
123     * lock of the backing map (the pattern published in <em>Java Concurrency in Practice</em>): a slow calculation for
124     * one key does not block calls for unrelated keys, and a calculation may itself use this Memoizer without
125     * deadlocking. Concurrent callers for the same argument wait on the same {@link Future}.
126     * </p>
127     *
128     * @param arg The argument for the calculation
129     * @return The result of the calculation
130     * @throws InterruptedException Thrown if the calculation is interrupted.
131     */
132    @Override
133    public O compute(final I arg) throws InterruptedException {
134        while (true) {
135            Future<O> future = cache.get(arg);
136            if (future == null) {
137                final FutureTask<O> futureTask = mappingFunction.apply(arg);
138                future = cache.putIfAbsent(arg, futureTask);
139                if (future == null) {
140                    // This thread won the race to install the task: run the user computation here,
141                    // outside the ConcurrentHashMap's internal locks. Losing threads (and later
142                    // callers) block on futureTask.get() instead of on a map bin lock.
143                    future = futureTask;
144                    futureTask.run();
145                }
146            }
147            try {
148                return future.get();
149            } catch (final CancellationException e) {
150                cache.remove(arg, future);
151            } catch (final ExecutionException e) {
152                if (recalculate) {
153                    cache.remove(arg, future);
154                }
155                throw launderException(e.getCause());
156            }
157        }
158    }
159
160    /**
161     * Always throws an unchecked exception or error, rethrowing a {@link RuntimeException} or {@link Error} unchanged
162     * and wrapping any other throwable in an {@link IllegalStateException}.
163     *
164     * @param throwable The throwable to rethrow or wrap.
165     * @return Never returns normally.
166     */
167    private RuntimeException launderException(final Throwable throwable) {
168        throw new IllegalStateException("Unchecked exception", ExceptionUtils.throwUnchecked(throwable));
169    }
170}