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

com.diffplug.common.util.concurrent.AggregateFuture Maven / Gradle / Ivy

There is a newer version: 1.2.0
Show newest version
/*
 * Original Guava code is copyright (C) 2015 The Guava Authors.
 * Modifications from Guava are copyright (C) 2016 DiffPlug.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.diffplug.common.util.concurrent;

import static com.diffplug.common.base.Preconditions.checkNotNull;
import static com.diffplug.common.base.Preconditions.checkState;
import static com.diffplug.common.util.concurrent.MoreExecutors.directExecutor;
import static com.diffplug.common.util.concurrent.Uninterruptibles.getUninterruptibly;

import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;

import com.google.j2objc.annotations.WeakOuter;

import com.diffplug.common.annotations.GwtCompatible;
import com.diffplug.common.annotations.GwtIncompatible;
import com.diffplug.common.collect.ImmutableCollection;

/**
 * A future made up of a collection of sub-futures.
 *
 * @param  the type of the individual inputs
 * @param  the type of the output (i.e. this) future
 */
@GwtCompatible
abstract class AggregateFuture extends AbstractFuture.TrustedFuture {
	private static final Logger logger = Logger.getLogger(AggregateFuture.class.getName());

	private RunningState runningState;

	@Override
	final void done() {
		super.done();

		// Let go of the memory held by the running state
		this.runningState = null;
	}

	// TODO(cpovirk): Use maybePropagateCancellation() if the performance is OK and the code is clean.
	@Override
	public final boolean cancel(boolean mayInterruptIfRunning) {
		// Must get a reference to the futures before we cancel, as they'll be cleared out.
		RunningState localRunningState = runningState;
		ImmutableCollection> futures = (localRunningState != null) ? localRunningState.futures : null;
		// Cancel all the component futures.
		boolean cancelled = super.cancel(mayInterruptIfRunning);
		// & is faster than the branch required for &&
		if (cancelled & futures != null) {
			for (ListenableFuture future : futures) {
				future.cancel(mayInterruptIfRunning);
			}
		}
		return cancelled;
	}

	@GwtIncompatible("Interruption not supported")
	@Override
	protected final void interruptTask() {
		RunningState localRunningState = runningState;
		if (localRunningState != null) {
			localRunningState.interruptTask();
		}
	}

	/**
	 * Must be called at the end of each sub-class's constructor.
	 */
	final void init(RunningState runningState) {
		this.runningState = runningState;
		runningState.init();
	}

	@WeakOuter
	abstract class RunningState extends AggregateFutureState implements Runnable {
		private ImmutableCollection> futures;
		private final boolean allMustSucceed;
		private final boolean collectsValues;

		RunningState(ImmutableCollection> futures,
				boolean allMustSucceed, boolean collectsValues) {
			super(futures.size());
			this.futures = checkNotNull(futures);
			this.allMustSucceed = allMustSucceed;
			this.collectsValues = collectsValues;
		}

		/* Used in the !allMustSucceed case so we don't have to instantiate a listener. */
		@Override
		public final void run() {
			decrementCountAndMaybeComplete();
		}

		/**
		 * The "real" initialization; we can't put this in the constructor because, in the case where
		 * futures are already complete, we would not initialize the subclass before calling
		 * {@link #handleOneInputDone}. As this is called after the subclass is constructed, we're
		 * guaranteed to have properly initialized the subclass.
		 */
		private void init() {
			// Corner case: List is empty.
			if (futures.isEmpty()) {
				handleAllCompleted();
				return;
			}

			// NOTE: If we ever want to use a custom executor here, have a look at
			// CombinedFuture as we'll need to handle RejectedExecutionException

			if (allMustSucceed) {
				// We need fail fast, so we have to keep track of which future failed so we can propagate
				// the exception immediately

				// Register a listener on each Future in the list to update
				// the state of this future.
				// Note that if all the futures on the list are done prior to completing
				// this loop, the last call to addListener() will callback to
				// setOneValue(), transitively call our cleanup listener, and set
				// this.futures to null.
				// This is not actually a problem, since the foreach only needs
				// this.futures to be non-null at the beginning of the loop.
				int i = 0;
				for (final ListenableFuture listenable : futures) {
					final int index = i++;
					listenable.addListener(new Runnable() {
						@Override
						public void run() {
							try {
								handleOneInputDone(index, listenable);
							} finally {
								decrementCountAndMaybeComplete();
							}
						}
					}, directExecutor());
				}
			} else {
				// We'll only call the callback when all futures complete, regardless of whether some failed
				// Hold off on calling setOneValue until all complete, so we can share the same listener
				for (ListenableFuture listenable : futures) {
					listenable.addListener(this, directExecutor());
				}
			}
		}

		/**
		 * Fails this future with the given Throwable if {@link #allMustSucceed} is
		 * true. Also, logs the throwable if it is an {@link Error} or if
		 * {@link #allMustSucceed} is {@code true}, the throwable did not cause
		 * this future to fail, and it is the first time we've seen that particular Throwable.
		 */
		private void handleException(Throwable throwable) {
			checkNotNull(throwable);

			boolean completedWithFailure = false;
			boolean firstTimeSeeingThisException = true;
			if (allMustSucceed) {
				// As soon as the first one fails, throw the exception up.
				// The result of all other inputs is then ignored.
				completedWithFailure = setException(throwable);
				if (completedWithFailure) {
					releaseResourcesAfterFailure();
				} else {
					// Go up the causal chain to see if we've already seen this cause; if we have,
					// even if it's wrapped by a different exception, don't log it.
					firstTimeSeeingThisException = addCausalChain(getOrInitSeenExceptions(), throwable);
				}
			}

			// | and & used because it's faster than the branch required for || and &&
			if (throwable instanceof Error
					| (allMustSucceed & !completedWithFailure & firstTimeSeeingThisException)) {
				String message = (throwable instanceof Error)
						? "Input Future failed with Error"
						: "Got more than one input Future failure. Logging failures after the first";
				logger.log(Level.SEVERE, message, throwable);
			}
		}

		@Override
		final void addInitialException(Set seen) {
			if (!isCancelled()) {
				addCausalChain(seen, trustedGetException());
			}
		}

		/**
		 * Handles the input at the given index completing.
		 */
		private void handleOneInputDone(int index, Future future) {
			// The only cases in which this Future should already be done are (a) if
			// it was cancelled or (b) if an input failed and we propagated that
			// immediately because of allMustSucceed.
			checkState(allMustSucceed || !isDone() || isCancelled(),
					"Future was done before all dependencies completed");

			try {
				checkState(future.isDone(),
						"Tried to set value from future which is not done");
				if (allMustSucceed) {
					if (future.isCancelled()) {
						// this.cancel propagates the cancellation to children; we use super.cancel
						// to set our own state but let the input futures keep running
						// as some of them may be used elsewhere.
						AggregateFuture.super.cancel(false);
					} else {
						// We always get the result so that we can have fail-fast, even if we don't collect
						InputT result = getUninterruptibly(future);
						if (collectsValues) {
							collectOneValue(allMustSucceed, index, result);
						}
					}
				} else if (collectsValues && !future.isCancelled()) {
					collectOneValue(allMustSucceed, index, getUninterruptibly(future));
				}
			} catch (ExecutionException e) {
				handleException(e.getCause());
			} catch (Throwable t) {
				handleException(t);
			}
		}

		private void decrementCountAndMaybeComplete() {
			int newRemaining = decrementRemainingAndGet();
			checkState(newRemaining >= 0, "Less than 0 remaining futures");
			if (newRemaining == 0) {
				processCompleted();
			}
		}

		private void processCompleted() {
			// Collect the values if (a) our output requires collecting them and (b) we haven't been
			// collecting them as we go. (We've collected them as we go only if we needed to fail fast)
			if (collectsValues & !allMustSucceed) {
				int i = 0;
				for (ListenableFuture listenable : futures) {
					handleOneInputDone(i++, listenable);
				}
			}
			handleAllCompleted();
		}

		/**
		 * Listeners implicitly keep a reference to {@link RunningState} as they're inner classes,
		 * so we free resources here as well for the allMustSucceed=true case (i.e. when a future fails,
		 * we immediately release resources we no longer need); additionally, the future will release
		 * its reference to {@link RunningState}, which should free all associated memory when all the
		 * futures complete & the listeners are released.
		 *
		 * TODO(user): Write tests for memory retention
		 */
		void releaseResourcesAfterFailure() {
			this.futures = null;
		}

		/**
		 * Called only if {@code collectsValues} is true.
		 *
		 * 

If {@code allMustSucceed} is true, called as each future completes; otherwise, * called for each future when all futures complete. */ abstract void collectOneValue(boolean allMustSucceed, int index, @Nullable InputT returnValue); abstract void handleAllCompleted(); void interruptTask() {} } /** Adds the chain to the seen set, and returns whether all the chain was new to us. */ private static boolean addCausalChain(Set seen, Throwable t) { for (; t != null; t = t.getCause()) { boolean firstTimeSeen = seen.add(t); if (!firstTimeSeen) { /* * We've seen this, so we've seen its causes, too. No need to re-add them. (There's one case * where this isn't true, but we ignore it: If we record an exception, then someone calls * initCause() on it, and then we examine it again, we'll conclude that we've seen the whole * chain before when it fact we haven't. But this should be rare.) */ return false; } } return true; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy