Add structured concurrency wrapper for CompletableFuture#2939
Add structured concurrency wrapper for CompletableFuture#2939cconstable wants to merge 3 commits into
Conversation
| Throwable unwrapped = unwrap(error); | ||
| if (unwrapped instanceof CancellationException) { | ||
| next.completeExceptionally(unwrapped); | ||
| return; | ||
| } |
There was a problem hiding this comment.
cancellation is an error but it's not something we can "recover from" so we should just immediately surface cancellations (skip the user supplied transform)
b0455e7 to
6371f6d
Compare
|
|
||
| /** Token that allows asynchronous code to observe cancellation requests. */ | ||
| @Experimental | ||
| public interface CancellationToken { |
There was a problem hiding this comment.
- the only "public" part of this PR
- meant to serve as a general purpose cancellation token
- can replace existing bespoke tokens e.g.
ActivityCancellationToken
There was a problem hiding this comment.
I think we should replace ActivityCancellationToken as part of this PR - it's so new probably no one is using it yet and still experimental. I suppose the only issue is that CancellationException is not ActivityCanceledException which the activity token needs to throw.
Maybe we can parameterize this over exception type?
| * Attaches an existing asynchronous task to this scope. Scope cancellation requests cancellation | ||
| * on the attached future. | ||
| */ | ||
| <U> TaskChain<U> attach(CompletableFuture<U> future); |
There was a problem hiding this comment.
This is the main usage point:
scope.attach(someAsyncWork());It lifts a CompletableFuture into the structured scope. It's the bridge from the unstructured CF world into the structured one. We return a TaskChain rather than a CompletableFuture so callers can chain downstream work (via map, recover, etc) without leaving the scope. It also does not expose blocking operations and foot-guns like exceptionally that break cancellation.
| private ListUtils() {} | ||
|
|
||
| /** Concatenates a list of lists into a single list, preserving order. */ | ||
| public static <T> List<T> flatten(List<? extends List<? extends T>> lists) { |
There was a problem hiding this comment.
This isn't used in this PR but it's a natural part of the using this abstraction:
scope.awaitAll(ListUtils::flatten);It's also being used in the branch I have this relies on this branch.
16ea50d to
16a7629
Compare
Sushisource
left a comment
There was a problem hiding this comment.
The readme example is very motivating.
I don't have any particularly strong comments. Just a few minor things / questions
|
|
||
| /** Token that allows asynchronous code to observe cancellation requests. */ | ||
| @Experimental | ||
| public interface CancellationToken { |
There was a problem hiding this comment.
I think we should replace ActivityCancellationToken as part of this PR - it's so new probably no one is using it yet and still experimental. I suppose the only issue is that CancellationException is not ActivityCanceledException which the activity token needs to throw.
Maybe we can parameterize this over exception type?
| * <p>Code waiting on external work can attach a callback to this future to abort in-flight | ||
| * requests when cancellation is requested. | ||
| */ | ||
| default CompletableFuture<Void> getCancellationFuture() { |
There was a problem hiding this comment.
I think probably this should also throw, like was done with the activity version (or, at least, we should have some way for it to work that way)
| * Runs a side effect when this task settles. Unlike {@code CompletableFuture.whenComplete}, the | ||
| * callback receives the unwrapped throwable ({@code null} on success). | ||
| */ | ||
| AsyncTask<T> whenSettled(BiConsumer<? super T, ? super Throwable> cb); |
There was a problem hiding this comment.
Can only one of these parameters be non-null? IE: Either it completed T or threw?
If so I think a separate whenSettledExceptionally method might make more sense, but I don't feel super strongly about that.
| public <U> AsyncTask<U> attach(CompletableFuture<U> future) { | ||
| Objects.requireNonNull(future, "future"); |
There was a problem hiding this comment.
Can use @Nonnull annotation too to help IDEs
| * | ||
| * <p>This method does not close the scope. Scope lifetime remains caller-owned. | ||
| */ | ||
| <R> CompletableFuture<R> awaitAll(Supplier<R> resultSupplier); |
There was a problem hiding this comment.
Not sure I get what the point of this is. Seems like the no-param version and the transformer version are the ones that matter. This seems like super thin sugar of the no-param version?
| * Non-blocking wait that completes with each collected task's settled outcome in collection | ||
| * order. |
There was a problem hiding this comment.
| * Non-blocking wait that completes with each collected task's settled outcome in collection | |
| * order. | |
| * Non-blocking wait that completes with each collected task's settled outcome in completion | |
| * order. |
Is this true? Unclear to me what collection order means
| public T get() { | ||
| if (status == Status.SUCCESS) return value; | ||
| throw new IllegalStateException("not a success: " + status, cause); | ||
| } |
There was a problem hiding this comment.
Might make more sense to just return an Optional here?
|
|
||
| The equivalent code using `CompletableFuture` directly has to rebuild the same ownership and cancellation rules by hand: | ||
|
|
||
| ```java |
What was changed
Add a small, nonblocking structured concurrency-like wrapper for
CompletableFuture. Loosely inspired by Java 25StructuredTaskScopeand SwiftTaskGroup.Allows for us to write this:
See the README for more details.
Why?
I'm doing extstore work and fan-in / fan-out + proper cancellation + error handling boilerplate with raw
CompletableFutureis tedious and error prone. Scoped task creation gives us correctness guarantees and better ergonomics. I noticed other disparate abstractions solving similar problems and provided a general solution.Checklist
I wrote and generated a ton of tests for this and also ran some "stress" tests locally. Since this is just a wrapper on top of
CompletableFutureand we aren't creating/using our own executors or threading abstractions it's not as scary of a thing as it looks.