
javaslang.concurrent.FutureImpl Maven / Gradle / Ivy
/* / \____ _ _ ____ ______ / \ ____ __ _______
* / / \/ \ / \/ \ / /\__\/ // \/ \ // /\__\ JΛVΛSLΛNG
* _/ / /\ \ \/ / /\ \\__\\ \ // /\ \ /\\/ \ /__\ \ Copyright 2014-2016 Javaslang, http://javaslang.io
* /___/\_/ \_/\____/\_/ \_/\__\/__/\__\_/ \_// \__/\_____/ Licensed under the Apache License, Version 2.0
*/
package javaslang.concurrent;
import javaslang.collection.Queue;
import javaslang.control.Option;
import javaslang.control.Try;
import javaslang.control.Try.CheckedSupplier;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
/**
* INTERNAL API - This class is subject to change.
*
* {@link Future} implementation, for internal use only.
*
* Lifecycle of a {@code FutureImpl}:
*
* 1) Creation
*
* - {@code value = None}
* - {@code actions = Queue.empty()}
* - {@code job = null}
*
* 2) Run
*
* - {@code value = None}
* - {@code actions = Queue(...)}
* - {@code job = java.util.concurrent.Future}
*
* 3) Complete
*
* - {@code value = Some(Try)}
* - {@code actions = null}
* - {@code job = null}
*
* 4) Cancel
*
* - {@code value = Some(Failure(CancellationException))}
* - {@code actions = null}
* - {@code job = null}
*
*
* @param Result of the computation.
* @author Daniel Dietrich
* @since 2.0.0
*/
final class FutureImpl implements Future {
/**
* Used to start new threads.
*/
private final ExecutorService executorService;
/**
* Used to synchronize state changes.
*/
private final Object lock = new Object();
/**
* Once the Future is completed, the value is defined.
*
* @@GuardedBy("lock")
*/
private volatile Option> value = Option.none();
/**
* The queue of actions is filled when calling onComplete() before the Future is completed or cancelled.
* Otherwise actions = null.
*
* @@GuardedBy("lock")
*/
private Queue>> actions = Queue.empty();
/**
* Once a computation is started via run(), job is defined and used to control the lifecycle of the computation.
*
* @@GuardedBy("lock")
*/
private java.util.concurrent.Future> job = null;
/**
* Creates a Future, {@link #run(CheckedSupplier)} has to be called separately.
*
* @param executorService An {@link ExecutorService} to run and control the computation and to perform the actions.
*/
FutureImpl(ExecutorService executorService) {
Objects.requireNonNull(executorService, "executorService is null");
this.executorService = executorService;
}
@Override
public void await() {
final Object monitor = new Object();
onComplete(ignored -> {
synchronized (monitor) {
monitor.notify();
}
});
synchronized (monitor) {
if (!isCompleted()) {
Try.run(monitor::wait);
}
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
synchronized (lock) {
if (isCompleted()) {
return false;
} else {
return Try.of(() -> job == null || job.cancel(mayInterruptIfRunning)).onSuccess(cancelled -> {
if (cancelled) {
value = Option.some(Try.failure(new CancellationException()));
actions = null;
job = null;
}
}).getOrElse(false);
}
}
}
@Override
public ExecutorService executorService() {
return executorService;
}
@Override
public Option> getValue() {
return value;
}
@Override
public boolean isCompleted() {
return value.isDefined();
}
@Override
public Future onComplete(Consumer super Try> action) {
Objects.requireNonNull(action, "action is null");
if (isCompleted()) {
perform(action);
} else {
synchronized (lock) {
if (isCompleted()) {
perform(action);
} else {
actions = actions.enqueue(action);
}
}
}
return this;
}
/**
* Runs a computation using the underlying ExecutorService.
*
* DEV-NOTE: Internally this method is called by the static {@code Future} factory methods.
*
* @throws IllegalStateException if the Future is pending, completed or cancelled
* @throws NullPointerException if {@code computation} is null.
*/
void run(CheckedSupplier extends T> computation) {
Objects.requireNonNull(computation, "computation is null");
synchronized (lock) {
if (job != null) {
throw new IllegalStateException("The Future is already running.");
}
if (isCompleted()) {
throw new IllegalStateException("The Future is completed.");
}
// if the ExecutorService runs the computation
// - in a different thread, the lock ensures that the job is assigned before the computation completes
// - in the current thread, the job is already completed and the `job` variable remains null
try {
final java.util.concurrent.Future> tmpJob = executorService.submit(() -> complete(Try.of(computation)));
if (!isCompleted()) {
job = tmpJob;
}
} catch (Throwable t) {
if (!isCompleted()) {
complete(Try.failure(t));
}
}
}
}
/**
* Completes this Future with a value.
*
* DEV-NOTE: Internally this method is called by the {@code Future.run()} method and by {@code Promise}.
*
* @param value A Success containing a result or a Failure containing an Exception.
* @return The given {@code value} for convenience purpose.
* @throws IllegalStateException if the Future is already completed or cancelled.
* @throws NullPointerException if the given {@code value} is null.
*/
@SuppressWarnings("unchecked")
Try complete(Try extends T> value) {
Objects.requireNonNull(value, "value is null");
final Queue>> actions;
// it is essential to make the completed state public *before* performing the actions
synchronized (lock) {
if (isCompleted()) {
throw new IllegalStateException("The Future is completed.");
}
actions = this.actions;
this.value = Option.some((Try) value);
this.actions = null;
this.job = null;
}
actions.forEach(this::perform);
return (Try) value;
}
boolean tryComplete(Try extends T> value) {
Objects.requireNonNull(value, "value is null");
synchronized (lock) {
if (isCompleted()) {
return false;
} else {
complete(value);
return true;
}
}
}
private void perform(Consumer super Try> action) {
Try.run(() -> executorService.execute(() -> action.accept(value.get())));
}
// This class is MUTABLE and therefore CANNOT CHANGE DEFAULT equals() and hashCode() behavior.
// See http://stackoverflow.com/questions/4718009/mutable-objects-and-hashcode
@Override
public String toString() {
return stringPrefix() + "(" + value.map(String::valueOf).getOrElse("?") + ")";
}
}