Skip to content

Commit 6371f6d

Browse files
committed
add minimal, non-blocking, structured concurrency wrapper for CompletableFuture.
1 parent 3da1f1d commit 6371f6d

14 files changed

Lines changed: 1623 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.temporal.common;
2+
3+
import java.util.concurrent.CancellationException;
4+
import java.util.concurrent.CompletableFuture;
5+
6+
/** Token that allows asynchronous code to observe cancellation requests. */
7+
@Experimental
8+
public interface CancellationToken {
9+
CancellationToken NONE =
10+
new CancellationToken() {
11+
@Override
12+
public boolean isCancellationRequested() {
13+
return false;
14+
}
15+
16+
@Override
17+
public void throwIfCancellationRequested() throws CancellationException {}
18+
19+
@Override
20+
public Registration onCancel(Runnable callback) {
21+
return () -> {};
22+
}
23+
};
24+
25+
/** Returns true after cancellation has been requested. */
26+
boolean isCancellationRequested();
27+
28+
/** Throws {@link CancellationException} if cancellation has been requested. */
29+
void throwIfCancellationRequested() throws CancellationException;
30+
31+
/**
32+
* Future that completes normally when cancellation has been requested.
33+
*
34+
* <p>Code waiting on external work can attach a callback to this future to abort in-flight
35+
* requests when cancellation is requested.
36+
*/
37+
default CompletableFuture<Void> getCancellationFuture() {
38+
CompletableFuture<Void> result = new CompletableFuture<>();
39+
Registration registration = onCancel(() -> result.complete(null));
40+
result.whenComplete((ignored, error) -> registration.close());
41+
return result;
42+
}
43+
44+
/**
45+
* Registers a callback to run when cancellation is requested, or immediately if already
46+
* cancelled.
47+
*
48+
* @return a handle that removes the callback if cancellation has not happened yet.
49+
*/
50+
Registration onCancel(Runnable callback);
51+
52+
/** Handle for removing a previously registered cancellation callback. */
53+
interface Registration extends AutoCloseable {
54+
@Override
55+
void close();
56+
}
57+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.temporal.internal.common;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.List;
6+
7+
public final class ListUtils {
8+
9+
private ListUtils() {}
10+
11+
public static <T> List<T> flatten(Collection<? extends Collection<? extends T>> collections) {
12+
List<T> result = new ArrayList<>();
13+
for (Collection<? extends T> collection : collections) {
14+
result.addAll(collection);
15+
}
16+
return result;
17+
}
18+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.temporal.internal.concurrent.structured;
2+
3+
import io.temporal.common.CancellationToken;
4+
import java.util.concurrent.CompletableFuture;
5+
import java.util.function.BiConsumer;
6+
import java.util.function.Consumer;
7+
import java.util.function.Function;
8+
9+
/**
10+
* Internal task handle over a {@link CompletableFuture} that manages cancellation
11+
* and tracks derived tasks.
12+
*
13+
* <p><strong>Continuation style.</strong> {@link #map}, {@link #recover}, and {@link #whenSettled}
14+
* chain follow-on work, mirroring {@code thenApply}/{@code exceptionally}/{@code whenComplete}.
15+
*
16+
* <p><strong>Cancellation is downstream by default.</strong> {@link #cancel()} settles this task
17+
* and every task derived from it (its {@code map}/{@code recover} children). It does <em>not</em>
18+
* cancel the task this one was derived <em>from</em>.
19+
*
20+
* @param <T> the result type
21+
*/
22+
interface AsyncTask<T> extends TaskChain<T> {
23+
24+
@Override
25+
<R> AsyncTask<R> map(Function<? super T, ? extends R> fn);
26+
27+
@Override
28+
AsyncTask<T> recover(Function<? super Throwable, ? extends T> fn);
29+
30+
@Override
31+
default AsyncTask<Void> thenAccept(Consumer<? super T> fn) {
32+
return map(
33+
value -> {
34+
fn.accept(value);
35+
return null;
36+
});
37+
}
38+
39+
/**
40+
* Runs a side effect when this task settles. Unlike {@code CompletableFuture.whenComplete}, the
41+
* callback receives the unwrapped throwable ({@code null} on success).
42+
*/
43+
AsyncTask<T> whenSettled(BiConsumer<? super T, ? super Throwable> cb);
44+
45+
/**
46+
* Cancels this task and everything derived from it.
47+
*
48+
* @return {@code true} if this call initiated cancellation (the task had not already settled).
49+
*/
50+
boolean cancel();
51+
52+
boolean isDone();
53+
54+
boolean isCancelled();
55+
56+
/**
57+
* Blocks for the value; throws on failure, or {@link java.util.concurrent.CancellationException}
58+
* on cancel.
59+
*/
60+
T join();
61+
62+
/** Blocks until settled and returns the outcome as a {@link Result}; never throws. */
63+
Result<T> joinSettled();
64+
65+
/**
66+
* @return the read-only cancellation token for this task.
67+
*/
68+
CancellationToken token();
69+
70+
/** Escape hatch to the underlying future for interop with existing APIs. */
71+
CompletableFuture<T> toCompletableFuture();
72+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.temporal.internal.concurrent.structured;
2+
3+
import io.temporal.common.CancellationToken;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
7+
/**
8+
* The <em>write</em> side of cancellation. Whoever holds the {@code CancelSource} can
9+
* request cancellation while everyone else observes using {@link #token()}.
10+
*/
11+
public final class CancelSource {
12+
13+
private final Object lock = new Object();
14+
private volatile boolean cancelled = false;
15+
16+
/** Pending callbacks. Set to {@code null} by {@link #cancel()} once it takes ownership. */
17+
private List<Runnable> callbacks = new ArrayList<>();
18+
19+
private final CancellationToken token =
20+
new CancellationToken() {
21+
@Override
22+
public boolean isCancellationRequested() {
23+
return cancelled;
24+
}
25+
26+
@Override
27+
public void throwIfCancellationRequested() {
28+
if (cancelled) throw new java.util.concurrent.CancellationException();
29+
}
30+
31+
@Override
32+
public Registration onCancel(Runnable cb) {
33+
synchronized (lock) {
34+
if (!cancelled) {
35+
callbacks.add(cb);
36+
return () -> {
37+
synchronized (lock) {
38+
if (callbacks != null) {
39+
callbacks.remove(cb);
40+
}
41+
}
42+
};
43+
}
44+
}
45+
runSafely(cb);
46+
return () -> {};
47+
}
48+
};
49+
50+
/** The read-only token to hand to code that observes cancellation. */
51+
public CancellationToken token() {
52+
return token;
53+
}
54+
55+
public boolean isCancelled() {
56+
return cancelled;
57+
}
58+
59+
/** Requests cancellation. Idempotent; fires each registered callback exactly once. */
60+
public void cancel() {
61+
List<Runnable> toRun;
62+
synchronized (lock) {
63+
if (cancelled) {
64+
return;
65+
}
66+
cancelled = true;
67+
toRun = callbacks;
68+
callbacks = null;
69+
}
70+
for (Runnable cb : toRun) {
71+
runSafely(cb);
72+
}
73+
}
74+
75+
private static void runSafely(Runnable cb) {
76+
try {
77+
cb.run();
78+
} catch (Throwable ignored) {
79+
/* a bad callback must not block others */
80+
}
81+
}
82+
83+
/** Creates a source whose token is cancelled whenever any {@code parent} token is cancelled. */
84+
public static CancelSource linkedTo(CancellationToken... parents) {
85+
CancelSource s = new CancelSource();
86+
for (CancellationToken p : parents) p.onCancel(s::cancel);
87+
return s;
88+
}
89+
}

0 commit comments

Comments
 (0)