net.jodah.failsafe.FailsafeFuture Maven / Gradle / Ivy
/*
* Copyright 2016 the original author or authors.
*
* 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 net.jodah.failsafe;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import net.jodah.failsafe.function.CheckedBiFunction;
import net.jodah.failsafe.internal.util.Assert;
import net.jodah.failsafe.internal.util.ReentrantCircuit;
/**
* The future result of an asynchronous Failsafe execution.
*
* @author Jonathan Halterman
* @param result type
*/
public class FailsafeFuture implements Future {
private final ReentrantCircuit circuit = new ReentrantCircuit();
private final FailsafeConfig config;
private ExecutionContext execution;
private java.util.concurrent.CompletableFuture completableFuture;
// Mutable state
private volatile Future delegate;
private volatile boolean done;
private volatile boolean cancelled;
private volatile T result;
private volatile Throwable failure;
FailsafeFuture(FailsafeConfig config) {
this.config = config;
circuit.open();
}
/**
* Attempts to cancel this execution. This attempt will fail if the execution has already completed, has already been
* cancelled, or could not be cancelled for some other reason. If successful, and this execution has not started when
* {@code cancel} is called, this execution should never run. If the execution has already started, then the
* {@code mayInterruptIfRunning} parameter determines whether the thread executing this task should be interrupted in
* an attempt to stop the execution.
*
*
* After this method returns, subsequent calls to {@link #isDone} will always return {@code true}. Subsequent calls to
* {@link #isCancelled} will always return {@code true} if this method returned {@code true}.
*
* @param mayInterruptIfRunning {@code true} if the thread executing this execution should be interrupted; otherwise,
* in-progress executions are allowed to complete
* @return {@code false} if the execution could not be cancelled, typically because it has already completed normally;
* {@code true} otherwise
*/
@Override
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
if (done)
return false;
boolean cancelResult = delegate.cancel(mayInterruptIfRunning);
failure = new CancellationException();
cancelled = true;
config.handleComplete(null, failure, execution, false);
complete(null, failure, config.fallback, false);
return cancelResult;
}
/**
* Waits if necessary for the execution to complete, and then returns its result.
*
* @return the execution result
* @throws CancellationException if the execution was cancelled
* @throws ExecutionException if the execution threw an exception
* @throws InterruptedException if the current thread was interrupted while waiting
*/
@Override
public T get() throws InterruptedException, ExecutionException {
circuit.await();
if (failure != null) {
if (failure instanceof CancellationException)
throw (CancellationException) failure;
throw new ExecutionException(failure);
}
return result;
}
/**
* Waits if necessary for at most the given time for the execution to complete, and then returns its result, if
* available.
*
* @param timeout the maximum time to wait
* @param unit the time unit of the timeout argument
* @return the execution result
* @throws CancellationException if the execution was cancelled
* @throws ExecutionException if the execution threw an exception
* @throws InterruptedException if the current thread was interrupted while waiting
* @throws TimeoutException if the wait timed out
* @throws NullPointerException if {@code unit} is null
* @throws IllegalArgumentException if {@code timeout} is < 0
*/
@Override
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
Assert.isTrue(timeout >= 0, "timeout cannot be negative");
if (!circuit.await(timeout, Assert.notNull(unit, "unit")))
throw new TimeoutException();
if (failure != null)
throw new ExecutionException(failure);
return result;
}
/**
* Returns {@code true} if this execution was cancelled before it completed normally.
*
* @return {@code true} if this execution was cancelled before it completed
*/
@Override
public boolean isCancelled() {
return cancelled;
}
/**
* Returns {@code true} if this execution completed.
*
* Completion may be due to normal termination, an exception, or cancellation -- in all of these cases, this method
* will return {@code true}.
*
* @return {@code true} if this execution completed
*/
@Override
public boolean isDone() {
return done;
}
synchronized void complete(T result, Throwable failure, CheckedBiFunction fallback,
boolean success) {
if (done)
return;
if (success || fallback == null) {
this.result = result;
this.failure = failure;
} else {
try {
this.result = fallback.apply(result, failure);
} catch (Throwable fallbackFailure) {
this.failure = fallbackFailure;
}
}
done = true;
if (completableFuture != null)
completeFuture();
circuit.close();
}
void inject(java.util.concurrent.CompletableFuture completableFuture) {
this.completableFuture = completableFuture;
}
void inject(Future delegate) {
this.delegate = delegate;
}
void inject(ExecutionContext execution) {
this.execution = execution;
}
private void completeFuture() {
if (failure == null)
completableFuture.complete(result);
else
completableFuture.completeExceptionally(failure);
}
}