From 4f3edc376a2333ed9654f954f29cf4dd178e00d7 Mon Sep 17 00:00:00 2001 From: Alan Bateman Date: Wed, 6 May 2026 10:18:04 +0000 Subject: [PATCH] 8380109: Implement JEP 533: Structured Concurrency (Seventh Preview) Reviewed-by: vklang --- .../classes/java/util/concurrent/Joiners.java | 77 +- .../util/concurrent/StructuredTaskScope.java | 1430 ++++++++++------- .../concurrent/StructuredTaskScopeImpl.java | 37 +- .../jdk/internal/javac/PreviewFeature.java | 2 +- .../lang/ScopedValue/StressStackOverflow.java | 13 +- .../StressCancellation.java | 10 +- .../StructuredTaskScopeTest.java | 506 +++--- .../StructuredThreadDumpTest.java | 38 +- .../StructuredTaskScope/WithScopedValue.java | 39 +- .../auth/Subject/CallAsWithScopedValue.java | 7 +- 10 files changed, 1277 insertions(+), 882 deletions(-) diff --git a/src/java.base/share/classes/java/util/concurrent/Joiners.java b/src/java.base/share/classes/java/util/concurrent/Joiners.java index 5d2a0f23ad7..8cd5f24dd37 100644 --- a/src/java.base/share/classes/java/util/concurrent/Joiners.java +++ b/src/java.base/share/classes/java/util/concurrent/Joiners.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,6 +33,8 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.concurrent.StructuredTaskScope.Joiner; import java.util.concurrent.StructuredTaskScope.Subtask; +import java.util.concurrent.StructuredTaskScope.CancelledByTimeoutException; +import java.util.function.Function; import java.util.function.Predicate; import jdk.internal.invoke.MhUtil; @@ -66,15 +68,21 @@ class Joiners { * A joiner that returns a list of all results when all subtasks complete * successfully. Cancels the scope if any subtask fails. */ - static final class AllSuccessful implements Joiner> { + static final class AllSuccessful implements Joiner, R_X> { private static final VarHandle FIRST_EXCEPTION = MhUtil.findVarHandle(MethodHandles.lookup(), "firstException", Throwable.class); + private final Function esf; + // list of forked subtasks, created lazily, only accessed by owner thread private List> subtasks; private volatile Throwable firstException; + AllSuccessful(Function esf) { + this.esf = Objects.requireNonNull(esf); + } + @Override public boolean onFork(Subtask subtask) { ensureUnavailable(subtask); @@ -94,11 +102,11 @@ class Joiners { } @Override - public List result() throws Throwable { + public List result() throws R_X { Throwable ex = firstException; try { if (ex != null) { - throw ex; + throw esf.apply(ex); } return (subtasks != null) ? subtasks.stream().map(Subtask::get).toList() @@ -107,22 +115,37 @@ class Joiners { subtasks = null; // allow subtasks to be GC'ed } } + + @Override + public List timeout() throws R_X { + try { + throw esf.apply(new CancelledByTimeoutException()); + } finally { + subtasks = null; // allow subtasks to be GC'ed + } + } } /** * A joiner that returns the result of the first subtask to complete successfully. * Cancels the scope if any subtasks succeeds. */ - static final class AnySuccessful implements Joiner { + static final class AnySuccessful implements Joiner { private static final VarHandle SUBTASK = MhUtil.findVarHandle(MethodHandles.lookup(), "subtask", Subtask.class); + private final Function esf; + // UNAVAILABLE < FAILED < SUCCESS private static final Comparator SUBTASK_STATE_COMPARATOR = Comparator.comparingInt(AnySuccessful::stateToInt); private volatile Subtask subtask; + AnySuccessful(Function esf) { + this.esf = Objects.requireNonNull(esf); + } + /** * Maps a Subtask.State to an int that can be compared. */ @@ -148,28 +171,39 @@ class Joiners { } @Override - public T result() throws Throwable { + public T result() throws R_X { Subtask subtask = this.subtask; if (subtask == null) { - throw new NoSuchElementException("No subtasks completed"); + throw esf.apply(new NoSuchElementException("No subtasks completed")); } return switch (subtask.state()) { case SUCCESS -> subtask.get(); - case FAILED -> throw subtask.exception(); + case FAILED -> throw esf.apply(subtask.exception()); default -> throw new InternalError(); }; } + + @Override + public T timeout() throws R_X { + throw esf.apply(new CancelledByTimeoutException()); + } } /** * A joiner that that waits for all successful subtasks. Cancels the scope if any * subtask fails. */ - static final class AwaitSuccessful implements Joiner { + static final class AwaitSuccessful implements Joiner { private static final VarHandle FIRST_EXCEPTION = MhUtil.findVarHandle(MethodHandles.lookup(), "firstException", Throwable.class); + + private final Function esf; private volatile Throwable firstException; + AwaitSuccessful(Function esf) { + this.esf = Objects.requireNonNull(esf); + } + @Override public boolean onComplete(Subtask subtask) { Subtask.State state = ensureCompleted(subtask); @@ -179,26 +213,31 @@ class Joiners { } @Override - public Void result() throws Throwable { + public Void result() throws R_X { Throwable ex = firstException; if (ex != null) { - throw ex; + throw esf.apply(ex); } else { return null; } } + + @Override + public Void timeout() throws R_X { + throw esf.apply(new CancelledByTimeoutException()); + } } /** * A joiner that returns a list of all subtasks. */ - static final class AllSubtasks implements Joiner>> { - private final Predicate> isDone; + static final class AllSubtasks implements Joiner>, RuntimeException> { + private final Predicate> isDone; // list of forked subtasks, created lazily, only accessed by owner thread private List> subtasks; - AllSubtasks(Predicate> isDone) { + AllSubtasks(Predicate> isDone) { this.isDone = Objects.requireNonNull(isDone); } @@ -218,11 +257,6 @@ class Joiners { return isDone.test(subtask); } - @Override - public void onTimeout() { - // do nothing, this joiner does not throw TimeoutException - } - @Override public List> result() { if (subtasks != null) { @@ -233,5 +267,10 @@ class Joiners { return List.of(); } } + + @Override + public List> timeout() { + return result(); + } } } diff --git a/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java index 66ba2c29bb3..39ad6074b31 100644 --- a/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java +++ b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java @@ -26,6 +26,7 @@ package java.util.concurrent; import java.time.Duration; import java.util.List; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -36,166 +37,142 @@ import jdk.internal.javac.PreviewFeature; * where execution of a task (a unit of work) splits into several concurrent * subtasks, and where the subtasks must complete before the task continues. A {@code * StructuredTaskScope} can be used to ensure that the lifetime of a concurrent operation - * is confined by a syntax block, similar to that of a sequential operation in + * is confined by a syntactic block, similar to that of a sequential operation in * structured programming. * - *

{@code StructuredTaskScope} defines the static method {@link #open() open} to open - * a new {@code StructuredTaskScope} and the {@link #close() close} method to close it. - * The API is designed to be used with the {@code try}-with-resources statement where - * the {@code StructuredTaskScope} is opened as a resource and then closed automatically. - * The code inside the block uses the {@link #fork(Callable) fork} method to fork subtasks. - * After forking, it uses the {@link #join() join} method to wait for all subtasks to - * finish (or some other outcome) as a single operation. Forking a subtask starts a new - * {@link Thread} to run the subtask. The thread executing the task does not continue - * beyond the {@code close} method until all threads started to execute subtasks have finished. - * To ensure correct usage, the {@code fork}, {@code join} and {@code close} methods may - * only be invoked by the owner thread (the thread that opened the {@code - * StructuredTaskScope}), the {@code fork} method may not be called after {@code join}, - * the {@code join} method must be invoked to get the outcome after forking subtasks, and - * the {@code close} method throws an exception after closing if the owner did not invoke - * the {@code join} method after forking subtasks. + *

{@code StructuredTaskScope} defines the static {@link #open()} method to create + * and open a new {@code StructuredTaskScope}. It defines the {@link #close() close()} + * method to close it. The API is designed to be used with the {@code try}-with-resources + * statement where a {@code StructuredTaskScope} is opened as a resource and then closed + * automatically. The code inside the {@code try} block uses the {@link #fork(Callable)} + * method to fork subtasks. Each call to the {@code fork(Callable)} method starts + * a new {@link Thread} (typically a {@linkplain Thread##virtual-threads virtual thread}) + * to execute a subtask as a {@linkplain Callable value-returning method}. The subtask + * executes concurrently with the code inside the {@code try} block, and concurrently with + * other subtasks forked in the scope. After forking all subtasks, the code inside the + * block uses the {@link #join() join()} method to wait for all subtasks to finish (or + * some other outcome) as a single operation. The code after the {@code join()} method + * processes the outcome. Execution does not continue beyond the {@code try} block (or + * {@code close} method) until all threads started in the scope to execute subtasks have + * finished. * - *

As a first example, consider a task that splits into two subtasks to concurrently - * fetch resources from two URL locations "left" and "right". Both subtasks may complete - * successfully, one subtask may succeed and the other may fail, or both subtasks may - * fail. The task in this example is interested in the successful result from both - * subtasks. It waits in the {@link #join() join} method for both subtasks to complete - * successfully or for either subtask to fail. + *

To ensure correct usage, the {@link #fork(Callable)}, {@link #join()} and {@link + * #close()} methods may only be invoked by the owner thread (the thread that + * opened the {@code StructuredTaskScope}), the {@code fork(Callable)} method may not be + * called after {@code join()}, the {@code join()} method must be invoked to get the outcome + * after forking subtasks, and the {@code close()} method throws an exception after closing + * if the owner did not invoke the {@code join()} method after forking subtasks. + * + *

As a first example, consider a "main" task that splits into two subtasks to + * concurrently fetch values from two remote services. The main task aggregates the results + * of both subtasks. The example invokes {@link #fork(Callable)} to fork the two subtasks. + * Each call to {@code fork(Callable)} returns a {@link Subtask Subtask} as a handle to + * the forked subtask. Both subtasks may complete successfully, one subtask may succeed + * and the other may fail, or both subtasks may fail. + * + *

The main task in the example is interested in the successful result from both + * subtasks. It waits in the {@link #join()} method for both subtasks to complete + * successfully or for either subtask to fail. If both subtasks complete successfully then + * the {@code join()} method completes normally and the task uses the {@link Subtask#get() + * Subtask.get()} method to get the result of each subtask. If one of the subtasks fails + * then the other subtask is cancelled, and the {@code join()} method throws {@link + * ExecutionException} with the exception from the failed subtask as the {@linkplain + * Throwable#getCause() cause}. * {@snippet lang=java : - * // @link substring="open" target="#open()" : + * // @link substring="open()" target="#open()" : * try (var scope = StructuredTaskScope.open()) { * * // @link substring="fork" target="#fork(Callable)" : - * Subtask subtask1 = scope.fork(() -> query(left)); - * Subtask subtask2 = scope.fork(() -> query(right)); + * Subtask subtask1 = scope.fork(() -> fetchFromRemoteService1()); + * Subtask subtask2 = scope.fork(() -> fetchFromRemoteService2()); * - * // throws if either subtask fails - * scope.join(); // @link substring="join" target="#join()" + * // throws ExecutionException if either subtask fails + * scope.join(); // @link substring="join()" target="#join()" * * // both subtasks completed successfully - * // @link substring="get" target="Subtask#get()" : - * return new MyResult(subtask1.get(), subtask2.get()); + * // @link substring="get()" target="Subtask#get()" : + * var result = new MyResult(subtask1.get(), subtask2.get()); * * // @link substring="close" target="#close()" : * } // close * } * - *

If both subtasks complete successfully then the {@code join} method completes - * normally and the task uses the {@link Subtask#get() Subtask.get()} method to get - * the result of each subtask. If one of the subtasks fails then the other subtask is - * cancelled (this will {@linkplain Thread#interrupt() interrupt} the thread executing the - * other subtask) and the {@code join} method throws {@link FailedException} with the - * exception from the failed subtask as the {@linkplain Throwable#getCause() cause}. + *

The {@link #close() close()} method always waits for threads executing subtasks to + * finish, even if the {@code join()} method throws, so that execution cannot continue + * beyond the {@code close()} method until the interrupted threads finish. * *

To allow for cancellation, subtasks must be coded so that they finish as soon as * possible when interrupted. Subtasks that do not respond to interrupt, e.g. block on - * methods that are not interruptible, may delay the closing of a scope indefinitely. The - * {@link #close() close} method always waits for threads executing subtasks to finish, - * even if the scope is cancelled, so execution cannot continue beyond the {@code close} - * method until the interrupted threads finish. + * methods that are not interruptible, may delay the {@link #close() close()} method + * indefinitely. * *

In the example, the subtasks produce results of different types ({@code String} and * {@code Integer}). In other cases the subtasks may all produce results of the same type. - * If the example had used {@code StructuredTaskScope.open()} then it could - * only be used to fork subtasks that return a {@code String} result. + * If the example had used {@code StructuredTaskScope.open()} to open the scope + * then it could only be used to fork subtasks that return a {@code String} result. * *

Joiners

* - *

In the example above, the task fails if any subtask fails. If all subtasks - * succeed then the {@code join} method completes normally. Other policy and outcome is - * supported by creating a {@code StructuredTaskScope} with a {@link Joiner} that - * implements the desired policy. A {@code Joiner} handles subtask completion and produces - * the outcome for the {@link #join() join} method. In the example above, {@code join} - * returns {@code null}. Depending on the {@code Joiner}, {@code join} may return a - * result, a list of elements, or some other object. The {@code Joiner} interface defines - * factory methods to create {@code Joiner}s for some common cases. + *

The {@link #join()} method in the example above completes normally, and returns + * {@code null}, if all subtasks succeed. It throws {@code ExecutionException} if any subtask + * fails. Other policies and outcomes are possible by creating a {@code StructuredTaskScope} + * with a {@link Joiner Joiner} that implements the desired policy and outcome. A {@code + * Joiner} handles subtasks as they are forked and when they complete, and produces the + * outcome for the {@code join()} method. Instead of {@code null}, a {@code Joiner} may + * cause {@code join()} to return the result of a specific subtask, a collection of results, + * or an object constructed from the results of some or all subtasks. When the outcome + * is an exception, a {@code Joiner} may cause {@code join()} to throw an exception + * other than {@code ExecutionException}. * - *

A {@code Joiner} may cancel the scope (sometimes called - * "short-circuiting") when some condition is reached that does not require the result of - * subtasks that are still executing. Cancelling the scope prevents new threads from being - * started to execute further subtasks, {@linkplain Thread#interrupt() interrupts} the - * threads executing subtasks that have not completed, and causes the {@code join} method - * to wakeup with the outcome (result or exception). In the above example, the outcome is - * that {@code join} completes with a result of {@code null} when all subtasks succeed. - * The scope is cancelled if any of the subtasks fail and {@code join} throws {@code - * FailedException} with the exception from the failed subtask as the cause. Other {@code - * Joiner} implementations may cancel the scope for other reasons. + *

A {@code Joiner} may cancel the scope (sometimes + * called "short-circuiting") when some condition is reached, e.g. a subtask fails, that + * does not require the outcome of other subtasks that are still executing. Cancelling the + * scope prevents new threads from being started in the scope, cancels subtasks in the + * scope that have not completed execution, and causes the {@code join()} method to wake up + * with the outcome (result or exception). In the above example, the outcome is that {@code + * join()} completes normally when all subtasks succeed. The scope is cancelled if any + * subtask fails and {@code join()} throws {@code ExecutionException} with the exception + * from the failed subtask as the {@linkplain Throwable#getCause() cause}. Other {@code + * Joiner} implementations may cancel the scope for other reasons, and may cause {@code + * join()} to throw a different exception when the outcome is an exception. * - *

Now consider another example that splits into two subtasks. In this example, - * each subtask produces a {@code String} result and the task is only interested in - * the result from the first subtask to complete successfully. The example uses {@link - * Joiner#anySuccessfulOrThrow() Joiner.anySuccessfulOrThrow()} to create a {@code Joiner} - * that makes available the result of the first subtask to complete successfully. The type - * parameter in the example is "{@code String}" so that only subtasks that return a - * {@code String} can be forked. + *

The {@link Joiner Joiner} interface defines static factory methods to create a + * {@code Joiner} for a number of common cases. The interface can be implemented when a + * more advanced or custom policy is required. A {@code Joiner} that returns a + * non-{@code null} result may remove the need for bookkeeping and the need to keep + * a reference to {@code Subtask} objects returned by the {@link #fork(Callable)} method. + + *

Now consider another example where a main task splits into two subtasks. In this + * example, each subtask produces a {@code String} result and the main task is only + * interested in the result from the first subtask to complete successfully. The example + * uses {@link Joiner#anySuccessfulOrThrow() Joiner.anySuccessfulOrThrow()} to create a + * {@code Joiner} that produces the result of any subtask that completes successfully. * {@snippet lang=java : - * // @link substring="open" target="#open(Joiner)" : + * // @link substring="anySuccessfulOrThrow()" target="Joiner#anySuccessfulOrThrow()" : * try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow())) { * + * // @link substring="fork" target="#fork(Callable)" : * scope.fork(callable1); * scope.fork(callable2); * - * // throws if both subtasks fail - * String firstResult = scope.join(); + * // throws ExecutionException if both subtasks fail + * String firstResult = scope.join(); // @link substring="join" target="#join()" * - * } + * // @link substring="close" target="#close()" : + * } // close * } * *

In the example, the task forks the two subtasks, then waits in the {@code - * join} method for either subtask to complete successfully or for both subtasks to fail. + * join()} method for either subtask to complete successfully or for both subtasks to fail. * If one of the subtasks completes successfully then the {@code Joiner} causes the other * subtask to be cancelled (this will interrupt the thread executing the subtask), and - * the {@code join} method returns the result from the successful subtask. Cancelling the + * the {@code join()} method returns the result from the successful subtask. Cancelling the * other subtask avoids the task waiting for a result that it doesn't care about. If - * both subtasks fail then the {@code join} method throws {@code FailedException} with the - * exception from one of the subtasks as the {@linkplain Throwable#getCause() cause}. - * - *

Whether code uses the {@code Subtask} returned from {@code fork} will depend on - * the {@code Joiner} and usage. Some {@code Joiner} implementations are suited to subtasks - * that return results of the same type and where the {@code join} method returns a result - * for the task to use. Code that forks subtasks that return results of different - * types, and uses a {@code Joiner} such as {@link Joiner#awaitAllSuccessfulOrThrow() - * awaitAllSuccessfulOrThrow} that does not return a result, will use {@link Subtask#get() - * Subtask.get()} after joining. - * - *

Exception handling

- * - *

A {@code StructuredTaskScope} is opened with a {@link Joiner Joiner} that - * handles subtask completion and produces the outcome for the {@link #join() join} method. - * In some cases, the outcome will be a result, in other cases it will be an exception. - * If the outcome is an exception then the {@code join} method throws {@link - * FailedException} with the exception as the {@linkplain Throwable#getCause() - * cause}. For many {@code Joiner} implementations, the exception will be an exception - * thrown by a subtask that failed. In the case of {@link Joiner#allSuccessfulOrThrow() - * allSuccessfulOrThrow} and {@link Joiner#awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow} - * for example, the exception is from the first subtask to fail. - * - *

Many of the details for how exceptions are handled will depend on usage. In some - * cases it may be useful to add a {@code catch} block to the {@code try}-with-resources - * statement to catch {@code FailedException}. The exception handling may use {@code - * instanceof} with pattern matching to handle specific causes. - * {@snippet lang=java : - * try (var scope = StructuredTaskScope.open()) { - * - * .. - * - * } catch (StructuredTaskScope.FailedException e) { - * - * Throwable cause = e.getCause(); - * switch (cause) { - * case IOException ioe -> .. - * default -> .. - * } - * - * } - * } - * In other cases it may not be useful to catch {@code FailedException} but instead leave - * it to propagate to the configured {@linkplain Thread.UncaughtExceptionHandler uncaught - * exception handler} for logging purposes. - * - *

For cases where a specific exception triggers the use of a default result then it - * may be more appropriate to handle this in the subtask itself rather than the subtask - * failing and the scope owner handling the exception. + * both subtasks fail then the {@code join()} method throws {@link ExecutionException} with + * the exception from one of the subtasks as the {@linkplain Throwable#getCause() cause}. + * {@link Joiner#anySuccessfulOrThrow(Function) Joiner.anySuccessfulOrThrow(Function)} can + * be used with a function that produces an exception other than {@code ExecutionException} + * to throw when all subtasks fail. * *

Configuration

* @@ -209,37 +186,40 @@ import jdk.internal.javac.PreviewFeature; * configuration has a {@code ThreadFactory} that creates unnamed {@linkplain * Thread##virtual-threads virtual threads}, does not name the scope, and has no timeout. * - *

The 2-arg {@link #open(Joiner, UnaryOperator) open} method can be used to create a - * {@code StructuredTaskScope} that uses a different {@code ThreadFactory}, is named for - * monitoring and management purposes, or has a timeout that cancels the scope if the - * timeout expires before or while waiting for subtasks to complete. The {@code open} - * method is called with an {@linkplain UnaryOperator operator} that is applied to the - * default configuration and returns a {@link Configuration Configuration} for the - * {@code StructuredTaskScope} under construction. + *

The {@link #open(UnaryOperator)} and {@link #open(Joiner, UnaryOperator)} methods + * can be used to create a {@code StructuredTaskScope} that uses a different {@code + * ThreadFactory}, is named for monitoring and management purposes, or has a timeout that + * cancels the scope if the timeout expires before or while waiting for subtasks to + * complete. The {@code open} methods are called with an {@linkplain UnaryOperator operator} + * that is applied to the default configuration and returns a {@link Configuration + * Configuration} for the {@code StructuredTaskScope} under construction. * *

The following example opens a new {@code StructuredTaskScope} with a {@code - * ThreadFactory} that creates virtual threads {@linkplain Thread#setName(String) named} + * ThreadFactory} that creates virtual threads {@linkplain Thread#getName() named} * "duke-0", "duke-1" ... * {@snippet lang = java: * // @link substring="name" target="Thread.Builder#name(String, long)" : * ThreadFactory factory = Thread.ofVirtual().name("duke-", 0).factory(); * * // @link substring="withThreadFactory" target="Configuration#withThreadFactory(ThreadFactory)" : - * try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory(factory))) { + * try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { * - * scope.fork( .. ); // runs in a virtual thread with name "duke-0" - * scope.fork( .. ); // runs in a virtual thread with name "duke-1" + * var subtask1 = scope.fork( .. ); // runs in a virtual thread with name "duke-0" + * var subtask2 = scope.fork( .. ); // runs in a virtual thread with name "duke-1" * * scope.join(); * + * var result = new MyResult(subtask1.get(), subtask2.get()); + * * } *} * *

A second example sets a timeout, represented by a {@link Duration}. The timeout - * starts when the new scope is opened. If the timeout expires before the {@code join} - * method has completed then the scope is {@linkplain ##Cancellation cancelled} (this - * interrupts the threads executing the two subtasks), and the {@code join} method - * throws {@link TimeoutException TimeoutException}. + * starts when the new scope is opened. If the timeout expires before or while waiting in + * the {@link #join()} method then the scope is {@linkplain ##Cancellation cancelled} + * (this interrupts the threads executing the subtasks that have not completed), and the + * {@code join()} method throws {@link ExecutionException} with a {@link + * CancelledByTimeoutException CancelledByTimeoutException} as the cause. * {@snippet lang=java : * Duration timeout = Duration.ofSeconds(10); * @@ -248,14 +228,56 @@ import jdk.internal.javac.PreviewFeature; * // @link substring="withTimeout" target="Configuration#withTimeout(Duration)" : * cf -> cf.withTimeout(timeout))) { * - * scope.fork(callable1); + * scope.fork(callable1); // subtask takes a really long time * scope.fork(callable2); * + * // throws ExecutionException with CancelledByTimeoutException as cause * List results = scope.join(); * - * } + * } * } * + *

Exception handling

+ * + *

The outcome of the {@link #join()} method is a result or exception. When the outcome + * is an exception then its {@linkplain Throwable#getCause() cause} will typically be + * the exception from a failed subtask or {@link CancelledByTimeoutException + * CancelledByTimeoutException} if a timeout was configured. + * + *

In some cases it may be useful to add a {@code catch} block to the + * {@code try}-with-resources statement to handle the exception. The following example + * uses the {@link #open(UnaryOperator)} method to open a scope with a timeout configured. + * The {@code join()} method in this example throws {@link ExecutionException} if any + * subtask fails or the timeout expires. The exception cause is the exception from a failed + * subtask or {@code CancelledByTimeoutException}. The example uses the {@code switch} + * statement to select based on the cause. + * {@snippet lang=java : + * try (var scope = StructuredTaskScope.open(cf -> cf.withTimeout(timeout))) { + * + * .. + * + * } catch (ExecutionException e) { + * switch (e.getCause()) { + * case CancelledByTimeoutException -> + * case IOException ioe -> .. + * default -> .. + * } + * } + * } + * + *

In other cases it may not be useful to catch the exception but instead leave it to + * propagate to the configured {@linkplain Thread.UncaughtExceptionHandler uncaught + * exception handler} for logging purposes. + * + *

For cases where a specific exception triggers the use of a default result then it + * may be more appropriate to handle this in the subtask itself rather than the subtask + * failing and the scope owner handling the exception. + * + *

The {@link #join()} method throws {@link InterruptedException} when interrupted + * before or while waiting in the {@code join()} method. + * The {@link Thread##thread-interruption Thread Interruption} section of the {@code Thread} + * specification provides guidance on handling this exception. + * *

Inheritance of scoped value bindings

* * {@link ScopedValue} supports the execution of a method with a {@code ScopedValue} bound @@ -266,21 +288,21 @@ import jdk.internal.javac.PreviewFeature; *

When used in conjunction with a {@code StructuredTaskScope}, a {@code ScopedValue} * can also safely and efficiently share a value to methods executed by subtasks forked * in the scope. When a {@code ScopedValue} object is bound to a value in the thread - * executing the task then that binding is inherited by the threads created to - * execute the subtasks. The thread executing the task does not continue beyond the - * {@link #close() close} method until all threads executing the subtasks have finished. + * executing a "main" task then that binding is inherited by the threads created to + * execute subtasks. The thread executing the main task does not continue beyond the + * {@link #close() close()} method until all threads executing the subtasks have finished. * This ensures that the {@code ScopedValue} is not reverted to being {@linkplain * ScopedValue#isBound() unbound} (or its previous value) while subtasks are executing. * In addition to providing a safe and efficient means to inherit a value into subtasks, - * the inheritance allows sequential code using {@code ScopedValue} be refactored to use - * structured concurrency. + * the inheritance allows sequential code using {@code ScopedValue} to be refactored to + * use structured concurrency. * *

To ensure correctness, opening a new {@code StructuredTaskScope} captures the * current thread's scoped value bindings. These are the scoped values bindings that are * inherited by the threads created to execute subtasks in the scope. Forking a * subtask checks that the bindings in effect at the time that the subtask is forked * match the bindings when the {@code StructuredTaskScope} was created. This check ensures - * that a subtask does not inherit a binding that is reverted in the task before the + * that a subtask does not inherit a binding that is reverted in the main task before the * subtask has completed. * *

A {@code ScopedValue} that is shared across threads requires that the value be an @@ -289,7 +311,7 @@ import jdk.internal.javac.PreviewFeature; *

The following example demonstrates the inheritance of scoped value bindings. The * scoped value USERNAME is bound to the value "duke" for the bounded period of a lambda * expression by the thread executing it. The code in the block opens a {@code - * StructuredTaskScope} and forks two subtasks, it then waits in the {@code join} method + * StructuredTaskScope} and forks two subtasks, it then waits in the {@code join()} method * and aggregates the results from both subtasks. If code executed by the threads * running subtask1 and subtask2 uses {@link ScopedValue#get()}, to get the value of * USERNAME, then value "duke" will be returned. @@ -315,16 +337,16 @@ import jdk.internal.javac.PreviewFeature; *

A scoped value inherited into a subtask may be {@linkplain ScopedValue##rebind * rebound} to a new value in the subtask for the bounded execution of some method executed * in the subtask. When the method completes, the value of the {@code ScopedValue} reverts - * to its previous value, the value inherited from the thread executing the task. + * to its previous value, the value inherited from the thread executing the main task. * *

A subtask may execute code that itself opens a new {@code StructuredTaskScope}. - * A task executing in thread T1 opens a {@code StructuredTaskScope} and forks a + * A main task executing in thread T1 opens a {@code StructuredTaskScope} and forks a * subtask that runs in thread T2. The scoped value bindings captured when T1 opens the * scope are inherited into T2. The subtask (in thread T2) executes code that opens a - * new {@code StructuredTaskScope} and forks a subtask that runs in thread T3. The scoped - * value bindings captured when T2 opens the scope are inherited into T3. These - * include (or may be the same) as the bindings that were inherited from T1. In effect, - * scoped values are inherited into a tree of subtasks, not just one level of subtask. + * new {@code StructuredTaskScope} and forks a (sub-)subtask that runs in thread T3. The + * scoped value bindings captured when T2 opens the scope are inherited into T3. These + * include the bindings that were inherited from T1. In effect, scoped values are + * inherited into a tree of subtasks, not just one level of subtask. * *

Memory consistency effects

* @@ -335,29 +357,29 @@ import jdk.internal.javac.PreviewFeature; * subtask outcome with {@link Subtask#get() Subtask.get()} or {@link Subtask#exception() * Subtask.exception()}. If a subtask's outcome contributes to the result or exception * from {@link #join()}, then any actions taken by the thread executing that subtask - * happen-before the owner thread returns from {@code join} with a result or - * {@link FailedException FailedException}. + * happen-before the owner thread returns from {@code join()} with the outcome. * *

General exceptions

* *

Unless otherwise specified, passing a {@code null} argument to a method in this * class will cause a {@link NullPointerException} to be thrown. * - * @param the result type of subtasks executed in the scope - * @param the result type of the scope - * + * @param the result type of subtasks {@linkplain #fork(Callable) forked} in the scope + * @param the type of the result returned by the {@link #join() join()} method + * @param the type of the exception thrown by the {@link #join() join()} method * @jls 17.4.5 Happens-before Order * @since 21 */ @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY) -public sealed interface StructuredTaskScope +public sealed interface StructuredTaskScope extends AutoCloseable permits StructuredTaskScopeImpl { /** - * Represents a subtask forked with {@link #fork(Callable)} or {@link #fork(Runnable)}. + * Represents a subtask forked in a {@link StructuredTaskScope} with {@link + * #fork(Callable) fork(Callable)} or {@link #fork(Runnable) fork(Runnable)}. * - *

Code that forks subtasks can use the {@link #get() get()} method after {@linkplain + *

The scope owner can use the {@link #get() get()} method after {@linkplain * #join() joining} to obtain the result of a subtask that completed successfully. It * can use the {@link #exception()} method to obtain the exception thrown by a subtask * that failed. @@ -368,7 +390,7 @@ public sealed interface StructuredTaskScope @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY) sealed interface Subtask extends Supplier permits StructuredTaskScopeImpl.SubtaskImpl { /** - * Represents the state of a subtask. + * Represents the state of a {@link Subtask Subtask}. * @see Subtask#state() * @since 21 */ @@ -376,20 +398,24 @@ public sealed interface StructuredTaskScope enum State { /** * The subtask result or exception is not available. This state indicates that - * the subtask was forked but has not completed, it completed after the scope - * was cancelled, or it was forked after the scoped was cancelled (in which - * case a thread was not created to execute the subtask). + * the subtask was forked but has not completed, the subtask was forked after + * the scope was {@linkplain StructuredTaskScope##Cancellation cancelled}, or + * the subtask completed after the scope was cancelled. */ UNAVAILABLE, /** - * The subtask completed successfully. The {@link Subtask#get() Subtask.get()} - * method can be used to get the result. This is a terminal state. + * The subtask completed successfully. If the scope is {@linkplain + * StructuredTaskScope##Cancellation cancelled}, the subtask completed + * successfully before the scope was cancelled. The {@link Subtask#get() + * Subtask.get()} method can be used to get the result. This is a terminal + * state. */ SUCCESS, /** - * The subtask failed with an exception. The {@link Subtask#exception() - * Subtask.exception()} method can be used to get the exception. This is a - * terminal state. + * The subtask failed with an exception. If the scope is {@linkplain + * StructuredTaskScope##Cancellation cancelled}, the subtask failed before + * the scope was cancelled. The {@link Subtask#exception() Subtask.exception()} + * method can be used to get the exception. This is a terminal state. */ FAILED, } @@ -400,16 +426,18 @@ public sealed interface StructuredTaskScope State state(); /** - * Returns the result of this subtask if it completed successfully. If the subtask - * was forked with {@link #fork(Callable) fork(Callable)} then the result from the - * {@link Callable#call() call} method is returned. If the subtask was forked with - * {@link #fork(Runnable) fork(Runnable)} then {@code null} is returned. + * Returns the result of this subtask if it completed successfully. If the scope + * is {@linkplain StructuredTaskScope##Cancellation cancelled}, the subtask + * completed successfully before the scope was cancelled. If the subtask was + * forked with {@link #fork(Callable) fork(Callable)} then the result from the + * {@link Callable#call() call()} method is returned. If the subtask was forked + * with {@link #fork(Runnable) fork(Runnable)} then {@code null} is returned. * *

Code executing in the scope owner thread can use this method to get the * result of a successful subtask after it has {@linkplain #join() joined}. * *

Code executing in the {@code Joiner} {@link Joiner#onComplete(Subtask) - * onComplete} method should test that the {@linkplain #state() subtask state} is + * onComplete(Subtask)} method should test that the {@linkplain #state() state} is * {@link State#SUCCESS SUCCESS} before using this method to get the result. * *

This method may be invoked by any thread after the scope owner has joined. @@ -418,7 +446,7 @@ public sealed interface StructuredTaskScope * * @return the possibly-null result * @throws IllegalStateException if the subtask has not completed or did not - * complete successfully, or this method if invoked outside the context of the + * complete successfully, or this method is invoked outside the context of the * {@code onComplete(Subtask)} method before the owner thread has joined * @see State#SUCCESS */ @@ -426,16 +454,18 @@ public sealed interface StructuredTaskScope /** * {@return the exception or error thrown by this subtask if it failed} - * If the subtask was forked with {@link #fork(Callable) fork(Callable)} then the - * exception or error thrown by the {@link Callable#call() call} method is returned. - * If the subtask was forked with {@link #fork(Runnable) fork(Runnable)} then the - * exception or error thrown by the {@link Runnable#run() run} method is returned. + * If the scope is {@linkplain StructuredTaskScope##Cancellation cancelled}, the + * subtask failed before the scope was cancelled. If the subtask was forked with + * {@link #fork(Callable) fork(Callable)} then the exception or error thrown by + * the {@link Callable#call() call()} method is returned. If the subtask was + * forked with {@link #fork(Runnable) fork(Runnable)} then the exception or error + * thrown by the {@link Runnable#run() run()} method is returned. * *

Code executing in the scope owner thread can use this method to get the * exception thrown by a failed subtask after it has {@linkplain #join() joined}. * *

Code executing in a {@code Joiner} {@link Joiner#onComplete(Subtask) - * onComplete} method should test that the {@linkplain #state() subtask state} is + * onComplete(Subtask)} method should test that the {@linkplain #state() state} is * {@link State#FAILED FAILED} before using this method to get the exception. * *

This method may be invoked by any thread after the scope owner has joined. @@ -443,7 +473,7 @@ public sealed interface StructuredTaskScope * owner has joined is when called from the {@code onComplete(Subtask)} method. * * @throws IllegalStateException if the subtask has not completed or completed - * with a result, or this method if invoked outside the context of the {@code + * with a result, or this method is invoked outside the context of the {@code * onComplete(Subtask)} method before the owner thread has joined * @see State#FAILED */ @@ -451,83 +481,99 @@ public sealed interface StructuredTaskScope } /** - * An object used with a {@link StructuredTaskScope} to handle subtask completion and - * produce the result for the scope owner waiting in the {@link #join() join} method - * for subtasks to complete. + * An object used with a {@link StructuredTaskScope} to produce the outcome for the + * scope's {@link #join() join()} method. * - *

Joiner defines static methods to create {@code Joiner} objects for common cases: + *

A {@code StructuredTaskScope} is opened with a {@code Joiner} that handles + * {@linkplain Subtask subtasks} as they are {@linkplain #fork(Callable) forked} in + * the scope and again when they complete execution. The {@code Joiner} handles + * subtasks that complete successfully with a result of type {@code T}, or fail with + * any exception or error. The {@code Joiner} implements a policy that may + * {@linkplain StructuredTaskScope##Cancellation cancel} the scope when some condition + * is reached (for example, a subtask fails). When all subtasks complete execution, or + * the scope is cancelled, the {@code Joiner} produces the outcome for the {@link + * #join() join()} method. The outcome is a result of type {@code R} or an exception + * of type {@code R_X}. + * + *

{@code Joiner} defines static methods to create {@code Joiner} objects for + * common cases: *

    - *
  • {@link #allSuccessfulOrThrow() allSuccessfulOrThrow()} creates a {@code Joiner} - * that yields a list of all results for {@code join} to return when all subtasks - * complete successfully. It cancels the scope and causes {@code join} to throw if - * any subtask fails. - *
  • {@link #anySuccessfulOrThrow() anySuccessfulOrThrow()} creates a {@code Joiner} - * that yields the result of the first subtask to succeed for {@code join} to return. - * It causes {@code join} to throw if all subtasks fail. - *
  • {@link #awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow()} creates a - * {@code Joiner} that waits for all successful subtasks. It cancels the scope and - * causes {@code join} to throw if any subtask fails. - *
  • {@link #awaitAll() awaitAll()} creates a {@code Joiner} that waits for all - * subtasks to complete. It does not cancel the scope or cause {@code join} to throw. + *
  • {@link #allSuccessfulOrThrow()} and {@link #allSuccessfulOrThrow(Function)} + * create a {@code Joiner} that produces a list of all results for {@code join()} to + * return when all subtasks complete successfully. The {@code Joiner} cancels the + * scope and causes {@code join()} to throw {@link ExecutionException}, or an + * exception returned by an exception supplying function, if any subtask fails. + *
  • {@link #anySuccessfulOrThrow()} and {@link #anySuccessfulOrThrow(Function)} + * create a {@code Joiner} that produces the result of any successful subtask for + * {@code join()} to return. The {@code Joiner} causes {@code join()} to throw + * {@code ExecutionException}, or an exception returned by an exception supplying + * function, if all subtasks fail. + *
  • {@link #awaitAllSuccessfulOrThrow()} and {@link #awaitAllSuccessfulOrThrow(Function)} + * create a {@code Joiner} that waits for all subtasks to complete successfully. + * The {@code Joiner} does not produce a non-{@code null} result for {@code join()} + * to return. The {@code Joiner} cancels the scope and causes {@code join()} to throw + * {@code ExecutionException}, or an exception returned by an exception supplying + * function, if any subtask fails. *
* *

In addition to the methods to create {@code Joiner} objects for common cases, - * the {@link #allUntil(Predicate) allUntil(Predicate)} method is defined to create a - * {@code Joiner} that yields a list of all subtasks. It is created with a {@link - * Predicate Predicate} that determines if the scope should continue or be cancelled. - * This {@code Joiner} can be built upon to create custom policies that cancel the - * scope based on some condition. + * the {@link #allUntil(Predicate) allUntil(Predicate)} method can be used to create a + * {@code Joiner} that implements a cancellation policy. The {@code Joiner} + * is created with a {@linkplain Predicate predicate} that is evaluated on completed + * subtasks. The {@code Joiner} {@linkplain StructuredTaskScope##Cancellation cancels} + * the scope if the predicate evaluates to {@code true}. When using this {@code Joiner}, + * the outcome of the {@code join()} method is the list of all subtasks forked in the + * scope. * *

More advanced policies can be developed by implementing the {@code Joiner} * interface. The {@link #onFork(Subtask)} method is invoked when subtasks are forked. * The {@link #onComplete(Subtask)} method is invoked when subtasks complete with a - * result or exception. These methods return a {@code boolean} to indicate if the scope - * should be cancelled. These methods can be used to collect subtasks, results, or - * exceptions, and control when to cancel the scope. The {@link #result()} method - * must be implemented to produce the result (or exception) for the {@code join} - * method. - * - *

If a {@code StructuredTaskScope} is opened with a {@linkplain - * Configuration#withTimeout(Duration) timeout}, and the timeout expires before or - * while waiting in {@link StructuredTaskScope#join() join()}, then the scope is - * {@linkplain StructuredTaskScope##Cancellation cancelled}, and the {@code Joiners}'s - * {@link #onTimeout()} method is invoked to notify the {@code Joiner} and optionally - * throw {@link TimeoutException TimeoutException}. If the {@code onTimeout()} method - * does not throw then the {@code join()} method will invoke the {@link #result()} - * method to produce a result. This result may be based on the outcome of subtasks - * that completed before the timeout expired. + * result or exception. These methods return a {@code boolean} to indicate whether the + * scope should be cancelled. These methods can be used to collect subtasks, results, + * or exceptions, and control when to cancel the scope. The {@link #result()} method + * must be implemented to produce the outcome (result or exception) for the {@code + * join()} method. The {@link #timeout()} method must be implemented to produce the + * outcome for the {@linkplain Configuration#withTimeout(Duration) timeout} case. * *

Unless otherwise specified, passing a {@code null} argument to a method * in this class will cause a {@link NullPointerException} to be thrown. * * @implSpec Implementations of this interface must be thread-safe. The {@link - * #onComplete(Subtask)} method may be invoked concurrently, as multiple subtasks can - * complete at the same time. Additionally, the {@code onComplete} method may be - * called concurrently with the scope owner thread invoking the {@link #onFork(Subtask)} - * or {@link #onTimeout()} methods. + * #onComplete(Subtask)} method may be invoked concurrently as multiple subtasks can + * complete at the same time. Additionally, the {@code onComplete(Subtask)} method may + * be called concurrently with the scope owner thread invoking the {@link + * #onFork(Subtask)}, {@link #result()}, or {@link #timeout()} methods. * - * @apiNote It is very important that a new {@code Joiner} object is created for each - * {@code StructuredTaskScope}. {@code Joiner} objects should never be shared with - * different scopes or re-used after a scope is closed. + *

A {@code Joiner} should clearly document how it handles {@linkplain + * Configuration#withTimeout(Duration) timeouts}. In many cases, a timeout will cause + * the {@code join()} method to throw an exception with {@link + * CancelledByTimeoutException CancelledByTimeoutException} as the cause. Some + * {@code Joiner} implementation may be capable of returning a result for the + * timeout case. * *

Designing a {@code Joiner} should take into account the code at the use-site - * where the results from the {@link StructuredTaskScope#join() join} method are + * where the results from the {@link StructuredTaskScope#join() join()} method are * processed. It should be clear what the {@code Joiner} does vs. the application * code at the use-site. In general, the {@code Joiner} implementation is not the * place for "business logic". A {@code Joiner} should be designed to be as general * purpose as possible. * - * @param the result type of subtasks executed in the scope - * @param the result type of the scope + * @apiNote It is very important that a new {@code Joiner} object is created for each + * {@code StructuredTaskScope}. {@code Joiner} objects should never be shared with + * different scopes or re-used after a scope is closed. + * + * @param the result type of subtasks {@linkplain #fork(Callable) forked} in the scope + * @param the type of the result returned by the {@link #join() join()} method + * @param the type of the exception thrown by the {@link #join() join()} method * @since 25 * @see #open(Joiner) */ @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY) - interface Joiner { + interface Joiner { /** * Invoked by {@link #fork(Callable) fork(Callable)} and {@link #fork(Runnable) * fork(Runnable)} when forking a subtask. The method is invoked before a thread - * is created to run the subtask. + * is created to execute the subtask. * * @implSpec The default implementation throws {@code NullPointerException} if the * subtask is {@code null}. It throws {@code IllegalArgumentException} if the @@ -548,9 +594,10 @@ public sealed interface StructuredTaskScope } /** - * Invoked by the thread started to execute a subtask after the subtask completes - * successfully or fails with an exception. This method is not invoked if a - * subtask completes after the scope is cancelled. + * Invoked by the thread that executed a subtask after the subtask completes + * successfully or fails with an exception. This method is not invoked by subtasks + * that complete after the scope is {@linkplain StructuredTaskScope##Cancellation + * cancelled}. * * @implSpec The default implementation throws {@code NullPointerException} if the * subtask is {@code null}. It throws {@code IllegalArgumentException} if the @@ -571,227 +618,411 @@ public sealed interface StructuredTaskScope } /** - * Invoked by the {@link #join() join()} method if the scope was opened with a - * timeout, and the timeout expires before or while waiting in the {@code join} - * method. + * Invoked by the {@link #join() join()} method to produce the outcome (result or + * exception) after waiting for all subtasks to complete or the scope is {@linkplain + * StructuredTaskScope##Cancellation cancelled}. This method is not invoked if the + * scope was opened with a timeout and the timeout expires before or while waiting. * - * @implSpec The default implementation throws {@link TimeoutException TimeoutException}. + *

This method will be called at most once, by the {@code join()} method, to + * produce the outcome. The behavior of this method when invoked directly is undefined. * - * @apiNote This method is intended for {@code Joiner} implementations that do not - * throw {@link TimeoutException TimeoutException}, or require a notification when - * the timeout expires before or while waiting in {@code join}. - * - *

This method is invoked by the {@code join} method. It should not be - * invoked directly. - * - * @throws TimeoutException for {@code join} to throw - * @since 26 - */ - default void onTimeout() { - throw new TimeoutException(); - } - - /** - * Invoked by the {@link #join() join()} method to produce the result (or exception) - * after waiting for all subtasks to complete or the scope cancelled. The result - * from this method is returned by the {@code join} method. If this method throws, - * then {@code join} throws {@link FailedException} with the exception thrown by - * this method as the cause. - * - *

In normal usage, this method will be called at most once by the {@code join} - * method to produce the result (or exception). The behavior of this method when - * invoked directly is undefined. - * - * @apiNote This method is invoked by the {@code join} method. It should not be + * @apiNote This method is invoked by the {@code join()} method. It should not be * invoked directly. * * @return the result - * @throws Throwable the exception + * @throws R_X if the outcome is an exception */ - R result() throws Throwable; + R result() throws R_X; /** - * {@return a new Joiner object that yields a list of all results when all - * subtasks complete successfully} - * The {@code Joiner} {@linkplain StructuredTaskScope##Cancellation cancels} - * the scope and causes {@code join} to throw if any subtask fails. + * Invoked by the {@link #join() join()} method to produce the outcome (result or + * exception) when the scope was opened with a timeout and the timeout expires before + * or while waiting in the {@code join()} method. * - *

If all subtasks complete successfully then the joiner's {@link - * Joiner#result()} method returns a list of all results, in the order that the - * subtasks were forked, for the {@link StructuredTaskScope#join() join()} to return. - * If the scope was opened with a {@linkplain Configuration#withTimeout(Duration) - * timeout}, and the timeout expires before or while waiting for all subtasks to - * complete, then the {@code join} method throws {@code TimeoutException}. + *

If the outcome is an exception, this method throws the exception with a + * {@link CancelledByTimeoutException CancelledByTimeoutException} as the + * {@link Throwable#getCause() cause}. + * + *

This method will be called at most once, by the {@code join()} method, to + * produce the outcome. The behavior of this method when invoked directly is undefined. + * + * @apiNote This method is invoked by the {@code join()} method. It should not be + * invoked directly. + * + * @return the result + * @throws R_X with a cause of {@code CancelledByTimeoutException}, if the outcome + * is an exception + * @since 27 + */ + R timeout() throws R_X; + + /** + * {@return a new Joiner that produces a list of all results when all subtasks + * complete successfully} + * The {@code Joiner} {@linkplain StructuredTaskScope##Cancellation cancels} the + * scope and causes the {@link #join() join()} method to throw if any subtask fails. + * + *

The {@link #join() join()} method of a {@link StructuredTaskScope} opened + * with this Joiner returns the list of the results, in the order that the subtasks + * were {@linkplain #fork(Callable) forked}, when all subtasks complete successfully. + * An empty list is returned if no subtasks were forked. If any subtask fails then + * the {@code Joiner} causes the {@code join()} method to throw the exception + * returned by the given exception supplying function when {@linkplain + * Function#apply(Object) applied} to the exception from the first subtask to fail. + * The function should return an exception with the exception from the failed + * subtask (the function argument) as the {@linkplain Throwable#getCause() cause}. + * If the function returns {@code null} then it causes the {@code join()} method + * to throw {@code NullPointerException}. + * + *

Timeout Handling: The {@code Joiner} cannot produce a result when + * the scope is cancelled by a timeout. If the scope was opened with a {@linkplain + * Configuration#withTimeout(Duration) timeout}, and the timeout expires before or + * while waiting for all subtasks to complete successfully, then the {@code Joiner} + * causes the {@code join()} method to throw the exception returned by the + * exception supplying function when applied to a {@link CancelledByTimeoutException + * CancelledByTimeoutException}. * * @apiNote Joiners returned by this method are suited to cases where all subtasks - * return a result of the same type. Joiners returned by {@link - * #awaitAllSuccessfulOrThrow()} are suited to cases where the subtasks return - * results of different types. + * return a result of the same type. It removes the need for bookkeeping + * and the need to keep a reference to the {@link Subtask Subtask} objects returned + * by the {@link #fork(Callable) fork(Callable)} method. Joiners returned by {@link + * #awaitAllSuccessfulOrThrow(Function)} and {@link #awaitAllSuccessfulOrThrow()} + * are suited to cases where the subtasks return results of different types and + * where it is necessary to keep a reference to the {@code Subtask} objects. * + *

The following example is a method that opens a {@code StructuredTaskScope} + * with a Joiner created with {@code allSuccessfulOrThrow(Function)}. The method + * is invoked with a collection of {@linkplain Callable callables}. It {@linkplain + * #fork(Callable) forks} a subtask to execute each callable, waits in {@link + * #join() join()} for all subtasks to complete successfully, and then returns a + * list of the results. It throws the runtime exception {@link CompletionException} + * if any subtask fails, with the exception from the first subtask to fail as the + * cause. + * {@snippet lang=java : + * List invokeAll(Collection> tasks) throws InterruptedException { + * try (var scope = StructuredTaskScope.open( + * Joiner.allSuccessfulOrThrow(CompletionException::new))) { + * tasks.forEach(scope::fork); + * List results = scope.join(); + * return results; + * } + * } + * } + * + * @param esf the exception supplying function * @param the result type of subtasks + * @param the type of the exception thrown by the {@link #join() join()} method + * @since 27 + * @see #allSuccessfulOrThrow() */ - static Joiner> allSuccessfulOrThrow() { - return new Joiners.AllSuccessful<>(); + static Joiner, R_X> + allSuccessfulOrThrow(Function esf) { + return new Joiners.AllSuccessful<>(esf); } /** - * {@return a new Joiner object that yields the result of any subtask that - * completed successfully} - * The {@code Joiner} causes {@code join} to throw if all subtasks fail. + * {@return a new Joiner that produces a list of all results when all subtasks + * complete successfully} + * The {@code Joiner} {@linkplain StructuredTaskScope##Cancellation cancels} the + * scope and causes the {@link #join() join()} method to throw {@link + * ExecutionException} if any subtask fails. * - *

The joiner's {@link Joiner#result()} method returns the result of a subtask, - * that completed successfully, for the {@link StructuredTaskScope#join() join()} - * to return. If all subtasks fail then the {@code result} method throws the - * exception from one of the failed subtasks. The {@code result} method throws - * {@code NoSuchElementException} if no subtasks were forked. If the scope was - * opened with a {@linkplain Configuration#withTimeout(Duration) timeout}, and - * the timeout expires before or while waiting for any subtask to complete - * successfully, then the {@code join} method throws {@code TimeoutException}. + *

The {@link #join() join()} method of a {@link StructuredTaskScope} opened + * with this Joiner returns the list of the results, in the order that the subtasks + * were {@linkplain #fork(Callable) forked}, when all subtasks complete successfully. + * An empty list is returned if no subtasks were forked. If any subtask fails then + * the Joiner causes the {@code join()} method to throw {@code ExecutionException} + * with the exception from the first subtask to fail as the {@linkplain + * Throwable#getCause() cause}. + * + *

Timeout Handling: The {@code Joiner} cannot produce a result when + * the scope is cancelled by a timeout. If the scope was opened with a {@linkplain + * Configuration#withTimeout(Duration) timeout}, and the timeout expires before or + * while waiting for all subtasks to complete successfully, then the {@code Joiner} + * causes the {@code join()} method to throw {@code ExecutionException} with + * a {@link CancelledByTimeoutException CancelledByTimeoutException} as the cause. + * + * @implSpec A Joiner returned by this method is equivalent to invoking {@link + * #allSuccessfulOrThrow(Function) allSuccessfulOrThrow(ExecutionException::new)}. + * + * @param the result type of subtasks + */ + static Joiner, ExecutionException> allSuccessfulOrThrow() { + return allSuccessfulOrThrow(ExecutionException::new); + } + + /** + * {@return a new Joiner that produces the result of any successful subtask} + * The {@code Joiner} {@linkplain StructuredTaskScope##Cancellation cancels} the + * scope when a subtask completes successfully. It causes the {@link #join() join()} + * method to throw if all subtasks fail. + * + *

The {@link #join() join()} method of a {@link StructuredTaskScope} opened + * with this Joiner returns the result of a successful subtask. If a subtask + * completes successfully then the scope is cancelled and the {@code join()} + * method returns its result. If all subtasks fail then the Joiner causes the + * {@code join()} method to throw the exception returned by the given exception + * supplying function when {@linkplain Function#apply(Object) applied} to the + * exception from one of the failed subtasks. The function should return an + * exception with the exception from the failed subtask (the function argument) as + * the {@linkplain Throwable#getCause() cause}. If the function returns {@code null} + * then it causes the {@code join()} method to throw {@code NullPointerException}. + * If no subtasks were forked then the Joiner causes the {@code join()} method to + * throw the exception returned by the function when applied to an instance of + * {@link java.util.NoSuchElementException}. + * + *

Timeout Handling: The {@code Joiner} cannot produce a result when + * the scope is cancelled by a timeout. If the scope was opened with a {@linkplain + * Configuration#withTimeout(Duration) timeout}, and the timeout expires before or + * while waiting for a subtask to complete successfully, then the {@code Joiner} + * causes the {@code join()} method to throw the exception returned by the + * exception supplying function when applied to a {@link CancelledByTimeoutException + * CancelledByTimeoutException}. + * + * @apiNote The following example is a method that opens a {@code StructuredTaskScope} + * with a Joiner created with {@code anySuccessfulOrThrow(Function)}. The method + * is invoked with a collection of {@linkplain Callable callables}. It {@linkplain + * #fork(Callable) forks} a subtask to execute each callable, waits in {@link + * #join() join()} for any subtask to complete successfully, returning its result. + * It throws the runtime exception {@link CompletionException} if all subtasks fail, + * with the exception from a failed subtask as the cause. + * {@snippet lang=java : + * T invokeAny(Collection> tasks) throws InterruptedException { + * try (var scope = StructuredTaskScope.open( + * Joiner.anySuccessfulOrThrow(CompletionException::new))) { + * tasks.forEach(scope::fork); + * T result = scope.join(); + * return result; + * } + * } + * } + * + * @param esf the exception supplying function + * @param the result type of subtasks + * @param the type of the exception thrown by the {@link #join() join()} method + * @since 27 + * @see #anySuccessfulOrThrow() + */ + static Joiner anySuccessfulOrThrow(Function esf) { + return new Joiners.AnySuccessful<>(esf); + } + + /** + * {@return a new Joiner that produces the result of any successful subtask} + * The {@code Joiner} {@linkplain StructuredTaskScope##Cancellation cancels} the + * scope when a subtask completes successfully. It causes the {@link #join() join()} + * method to throw {@link ExecutionException} if all subtasks fail. + * + *

The {@link #join() join()} method of a {@link StructuredTaskScope} opened + * with this Joiner returns the result of a successful subtask. If a subtask + * completes successfully then the scope is cancelled and the {@code join()} method + * returns its result. If all subtasks fail then the Joiner causes the {@code + * join()} method to throw {@code ExecutionException} with the exception from one + * of the failed subtasks as the {@linkplain Throwable#getCause() cause}. If no + * subtasks were forked then the {@code Joiner} causes the {@code join()} method + * to throw {@code ExecutionException} with {@link java.util.NoSuchElementException} + * as the cause. + * + *

Timeout Handling: The {@code Joiner} cannot produce a result when + * the scope is cancelled by a timeout. If the scope was opened with a {@linkplain + * Configuration#withTimeout(Duration) timeout}, and the timeout expires before or + * while waiting for a subtask to complete successfully, then the {@code Joiner} + * causes the {@code join()} method to throw {@code ExecutionException} with a + * {@link CancelledByTimeoutException CancelledByTimeoutException} as the cause. + * + * @implSpec A Joiner returned by this method is equivalent to invoking {@link + * #anySuccessfulOrThrow(Function) anySuccessfulOrThrow(ExecutionException::new)}. * * @param the result type of subtasks * @since 26 */ - static Joiner anySuccessfulOrThrow() { - return new Joiners.AnySuccessful<>(); + static Joiner anySuccessfulOrThrow() { + return anySuccessfulOrThrow(ExecutionException::new); } /** - * {@return a new Joiner object that waits for subtasks to complete successfully} - * The {@code Joiner} {@linkplain StructuredTaskScope##Cancellation cancels} - * the scope and causes {@code join} to throw if any subtask fails. + * {@return a new Joiner that causes the {@link #join() join()} method to wait + * for all subtasks to complete successfully} + * The {@code Joiner} {@linkplain StructuredTaskScope##Cancellation cancels} the + * scope and causes the {@link #join() join()} method to throw if any subtask fails. * - *

The joiner's {@link Joiner#result() result} method returns {@code null} - * if all subtasks complete successfully, or throws the exception from the first - * subtask to fail. If the scope was opened with a {@linkplain + *

The {@link #join() join()} method of a {@link StructuredTaskScope} opened + * with this Joiner returns {@code null} when all subtasks complete successfully. + * If any subtask fails then the Joiner causes the {@code join()} method to throw + * the exception returned by the given exception supplying function when {@linkplain + * Function#apply(Object) applied} to the exception from the first subtask to fail. + * The function should return an exception with the exception from the failed + * subtask (the function argument) as the {@linkplain Throwable#getCause() cause}. + * If the function returns {@code null} then it causes the {@code join()} method + * to throw {@code NullPointerException}. + * + *

Timeout Handling: The {@code Joiner} cannot produce a result when + * the scope is cancelled by a timeout. If the scope was opened with a {@linkplain * Configuration#withTimeout(Duration) timeout}, and the timeout expires before or - * while waiting for all subtasks to complete, then the {@code join} method throws - * {@code TimeoutException}. + * while waiting for all subtasks to complete successfully, then the {@code Joiner} + * causes the {@code join()} method to throw the exception returned by the + * exception supplying function when applied to a {@link CancelledByTimeoutException + * CancelledByTimeoutException}. * * @apiNote Joiners returned by this method are suited to cases where subtasks - * return results of different types. Joiners returned by {@link #allSuccessfulOrThrow()} - * are suited to cases where the subtasks return a result of the same type. + * return results of different types and where it is necessary to keep a reference + * to each {@link Subtask Subtask}. Joiners returned by {@link + * #allSuccessfulOrThrow(Function)} and {@link #allSuccessfulOrThrow()} are suited + * to cases where the subtasks return a result of the same type. * - * @param the result type of subtasks - */ - static Joiner awaitAllSuccessfulOrThrow() { - return new Joiners.AwaitSuccessful<>(); - } - - /** - * {@return a new Joiner object that waits for all subtasks to complete} - * The {@code Joiner} does not cancel the scope if a subtask fails. - * - *

The joiner's {@link Joiner#result() result} method returns {@code null}. - * If the scope was opened with a {@linkplain Configuration#withTimeout(Duration) - * timeout}, and the timeout expires before or while waiting for all subtasks to - * complete, then the {@code join} method throws {@code TimeoutException}. - * - * @apiNote This Joiner is useful for cases where subtasks make use of - * side-effects rather than return results or fail with exceptions. - * The {@link #fork(Runnable) fork(Runnable)} method can be used to fork subtasks - * that do not return a result. - * - *

This Joiner can also be used for fan-in scenarios where subtasks - * are forked to handle incoming connections and the number of subtasks is unbounded. - * In this example, the thread executing the {@code acceptLoop} method will only - * stop when interrupted or the listener socket is closed asynchronously. + *

The following example opens a {@code StructuredTaskScope} with a Joiner + * created with {@code awaitAllSuccessfulOrThrow()}. It {@linkplain + * #fork(Callable) forks} two subtasks, then waits in {@link #join() join()} for + * both subtasks to complete successfully or either subtask to fail. If both + * subtasks complete successfully then it invokes {@link Subtask#get() Subtask.get()} + * on both subtasks to get their results. It throws the runtime exception {@link + * CompletionException} if any subtask fails, with the exception from a failed + * subtask as the cause. * {@snippet lang=java : - * void acceptLoop(ServerSocket listener) throws IOException, InterruptedException { - * try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { - * while (true) { - * Socket socket = listener.accept(); - * scope.fork(() -> handle(socket)); - * } - * } + * try (var scope = StructuredTaskScope.open( + * Joiner.awaitAllSuccessfulOrThrow(CompletionException::new))) { + * Subtask subtask1 = scope.fork(callable1); + * Subtask subtask2 = scope.fork(callable2); + * + * // throws CompletionException if either subtask fails + * scope.join(); + * + * // both subtasks completed successfully + * var result = new MyResult(subtask1.get(), subtask2.get()); * } * } - * + * @param esf the exception supplying function * @param the result type of subtasks + * @param the type of the exception thrown by the {@link #join() join()} method + * @since 27 */ - static Joiner awaitAll() { - // ensure that new Joiner object is returned - return new Joiner() { - @Override - public Void result() { - return null; - } - }; + static Joiner + awaitAllSuccessfulOrThrow(Function esf) { + return new Joiners.AwaitSuccessful<>(esf); } /** - * {@return a new Joiner object that yields a list of all subtasks when all - * subtasks complete or a predicate returns {@code true} to cancel the scope} + * {@return a new Joiner that causes the {@link #join() join()} method to wait + * for all subtasks to complete successfully} + * The {@code Joiner} {@linkplain StructuredTaskScope##Cancellation cancels} the + * scope and causes the {@link #join() join()} method to throw {@link + * ExecutionException} if any subtask fails. * - *

The joiner's {@link #onComplete(Subtask)} method invokes the predicate's - * {@link Predicate#test(Object) test} method with the subtask that completed - * successfully or failed with an exception. If the {@code test} method - * returns {@code true} then {@linkplain StructuredTaskScope##Cancellation - * the scope is cancelled}. The {@code test} method must be thread safe as it - * may be invoked concurrently from several threads. If the {@code test} method - * completes with an exception or error, then the thread that executed the subtask + *

The {@link #join() join()} method of a {@link StructuredTaskScope} opened + * with this Joiner returns {@code null} when all subtasks complete successfully. + * If any subtask fails then the Joiner causes the {@code join()} method to throw + * {@code ExecutionException} with the exception from the first subtask to fail + * as the {@linkplain Throwable#getCause() cause}. + * + *

Timeout Handling: The {@code Joiner} cannot produce a result when + * the scope is cancelled by a timeout. If the scope was opened with a {@linkplain + * Configuration#withTimeout(Duration) timeout}, and the timeout expires before or + * while waiting for all subtasks to complete successfully, then the {@code Joiner} + * causes the {@code join()} method to throw {@code ExecutionException} with a + * {@link CancelledByTimeoutException CancelledByTimeoutException} as the cause. + * + * @implSpec A Joiner returned by this method is equivalent to invoking {@link + * #awaitAllSuccessfulOrThrow(Function) awaitAllSuccessfulOrThrow(ExecutionException::new)}. + * + * @param the result type of subtasks + * @see #open() + * @see #open(UnaryOperator) + */ + static Joiner awaitAllSuccessfulOrThrow() { + return awaitAllSuccessfulOrThrow(ExecutionException::new); + } + + /** + * {@return a new Joiner that produces a list of all subtasks when all subtasks + * complete or {@linkplain Predicate#test(Object) evaluating} a predicate on a + * completed subtask causes the scope to be {@linkplain + * StructuredTaskScope##Cancellation cancelled}} + * The {@code Joiner} does not cause the {@link #join() join()} method to throw + * if subtasks fail or a configured {@linkplain Configuration#withTimeout(Duration) + * timeout} expires. This method can be used to create a {@code Joiner} that + * implements a cancellation policy. + * + *

The {@link #join() join()} method of a {@link StructuredTaskScope} opened + * with this Joiner returns a list of all subtasks, in the order that they were + * {@linkplain #fork(Callable) forked}, when all subtasks complete or the scope is + * cancelled. The returned list may contain subtasks that completed (in the {@link + * Subtask.State#SUCCESS SUCCESS} or {@link Subtask.State#FAILED FAILED} state), + * or subtasks in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state if + * they were forked or completed after the scope was cancelled. The scope is + * cancelled if the given predicate {@linkplain Predicate#test(Object) evaluates} + * to {@code true}, or a configured timeout expires before or while waiting in the + * {@code join()} method. + * + *

The given {@code Predicate}'s {@link Predicate#test(Object) test(Object)} + * method is invoked on a completed subtask by the thread that executed the subtask. + * The method is invoked after the subtask completes (successfully or with an + * exception) before the thread terminates. The scope is cancelled if the {@code + * test} method returns {@code true}. The {@code test} method must be thread safe. + * It may be invoked concurrently from several threads as multiple subtasks can + * complete at the same time. If the method throws an exception or error, the thread * invokes the {@linkplain Thread.UncaughtExceptionHandler uncaught exception handler} * with the exception or error before the thread terminates. * - *

The joiner's {@link #result()} method returns the list of all subtasks, - * in fork order. The list may contain subtasks that have completed - * (in {@link Subtask.State#SUCCESS SUCCESS} or {@link Subtask.State#FAILED FAILED} - * state) or subtasks in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state - * if the scope was cancelled before all subtasks were forked or completed. + *

Timeout Handling: If used with a scope that has a {@linkplain + * Configuration#withTimeout(Duration) timeout} set, and the timeout expires before + * all subtasks complete then the {@code join()} method returns the list of + * all subtasks. It does not throw an exception. Subtasks that did not complete + * before the timeout expires will be in the {@code UNAVAILABLE} state. * - *

The joiner's {@link #onTimeout()} method does nothing. If configured with - * a {@linkplain Configuration#withTimeout(Duration) timeout}, and the timeout - * expires before or while waiting in {@link StructuredTaskScope#join() join}, - * then the {@link #result()} method returns the list of all subtasks. - * Subtasks that did not complete before the timeout expired will be in the - * {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state. - * - *

The following example uses this method to create a {@code Joiner} that + * @apiNote The following example uses {@code allUntil} to create a Joiner that * {@linkplain StructuredTaskScope##Cancellation cancels} the scope when two or * more subtasks fail. * {@snippet lang=java : - * class CancelAfterTwoFailures implements Predicate> { + * class CancelAfterTwoFailures implements Predicate> { * private final AtomicInteger failedCount = new AtomicInteger(); * @Override - * public boolean test(Subtask subtask) { + * public boolean test(Subtask subtask) { * return subtask.state() == Subtask.State.FAILED * && failedCount.incrementAndGet() >= 2; * } * } * - * var joiner = Joiner.allUntil(new CancelAfterTwoFailures()); + * var joiner = Joiner.allUntil(new CancelAfterTwoFailures()); * } * - *

The following example uses {@code allUntil} to wait for all subtasks to - * complete without any cancellation. This is similar to {@link #awaitAll()} - * except that it yields a list of the completed subtasks. + *

The following example uses {@code allUntil} to create a Joiner that + * cancels the scope when any subtask completes successfully. The subtasks are + * grouped according to their {@linkplain Subtask.State state} to produce a map + * with up to three key-value mappings. The map key is the subtask state, the + * value is the list of subtasks in that state. * {@snippet lang=java : - * List> invokeAll(Collection> tasks) throws InterruptedException { - * try (var scope = StructuredTaskScope.open(Joiner.allUntil(_ -> false))) { + * Predicate> isSuccessful = s -> s.state() == Subtask.State.SUCCESS; + * + * try (var scope = StructuredTaskScope.open(Joiner.allUntil(isSuccessful))) { + * tasks.forEach(scope::fork); + * + * Map>> subtasksByState = scope.join() + * .stream() + * // @link substring="Collectors.groupingBy" target="java.util.stream.Collectors#groupingBy" : + * .collect(Collectors.groupingBy(Subtask::state)); + * } + * } + * + *

The following example is a method that uses {@code allUntil} to create a + * Joiner that does not cancel the scope. The method waits for all subtasks to + * complete or a timeout to expire. It returns a list of all subtasks, in the + * same order as the collection of callables, even if the timeout expires before + * or while waiting in {@code join()}. + * {@snippet lang=java : + * List> invokeAll(Collection> tasks, Duration timeout) throws InterruptedException { + * // @link substring="withTimeout" target="Configuration#withTimeout(Duration)" : + * try (var scope = StructuredTaskScope.open(Joiner.allUntil(_ -> false), cf -> cf.withTimeout(timeout))) { * tasks.forEach(scope::fork); * return scope.join(); * } * } * } * - *

The following example uses {@code allUntil} to get the results of all - * subtasks that complete successfully within a timeout period. - * {@snippet lang=java : - * List invokeAll(Collection> tasks, Duration timeout) throws InterruptedException { - * try (var scope = StructuredTaskScope.open(Joiner.allUntil(_ -> false), cf -> cf.withTimeout(timeout))) { - * tasks.forEach(scope::fork); - * return scope.join() - * .stream() - * .filter(s -> s.state() == Subtask.State.SUCCESS) - * .map(Subtask::get) - * .toList(); - * } - * } - * } - * * @param isDone the predicate to evaluate completed subtasks * @param the result type of subtasks */ - static Joiner>> allUntil(Predicate> isDone) { + static Joiner>, RuntimeException> + allUntil(Predicate> isDone) { return new Joiners.AllSubtasks<>(isDone); } } @@ -803,8 +1034,9 @@ public sealed interface StructuredTaskScope * ThreadFactory} to create threads, an optional name for the scope, and an optional * timeout. The name is intended for monitoring and management purposes. * - *

Creating a {@code StructuredTaskScope} with its 2-arg {@link #open(Joiner, UnaryOperator) - * open} method allows a different configuration to be used. The operator specified + *

Creating a {@code StructuredTaskScope} with the {@link #open(UnaryOperator) + * open(UnaryOperator)} or {@link #open(Joiner, UnaryOperator) open(Joiner, UnaryOperator)} + * method allows a different configuration to be used. The operator specified * to the {@code open} method is applied to the default configuration and returns the * configuration for the {@code StructuredTaskScope} under construction. The operator * can use the {@code with-} prefixed methods defined here to specify the components @@ -824,9 +1056,9 @@ public sealed interface StructuredTaskScope * @param threadFactory the thread factory * * @apiNote The thread factory will typically create {@linkplain Thread##virtual-threads - * virtual threads}, maybe with names for monitoring purposes, an {@linkplain - * Thread.UncaughtExceptionHandler uncaught exception handler}, or other properties - * configured. + * virtual threads}, maybe with {@linkplain Thread#getName() thread names} for + * monitoring purposes, an {@linkplain Thread.UncaughtExceptionHandler uncaught + * exception handler}, or other properties configured. * * @see #fork(Callable) */ @@ -850,182 +1082,253 @@ public sealed interface StructuredTaskScope * compute the timeout for this method. * * @see #join() - * @see Joiner#onTimeout() + * @see CancelledByTimeoutException */ Configuration withTimeout(Duration timeout); } /** - * Exception thrown by {@link #join()} when the outcome is an exception rather than a - * result. + * The exception {@linkplain Throwable#getCause() cause} when {@link #join() join()} + * throws because the scope was {@linkplain StructuredTaskScope##Cancellation cancelled} + * by a timeout. * - * @since 25 - */ - @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY) - final class FailedException extends RuntimeException { - @java.io.Serial - static final long serialVersionUID = -1533055100078459923L; - - /** - * Constructs a {@code FailedException} with the specified cause. - * - * @param cause the cause, can be {@code null} - */ - FailedException(Throwable cause) { - super(cause); - } - } - - /** - * Exception thrown by {@link #join()} if the scope was opened with a timeout, - * the timeout expired before or while waiting in {@code join}, and the {@link - * Joiner#onTimeout() Joiner.onTimeout} method throws this exception. - * - * @since 25 + * @since 27 * @see Configuration#withTimeout(Duration) - * @see Joiner#onTimeout() */ @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY) - final class TimeoutException extends RuntimeException { + final class CancelledByTimeoutException extends RuntimeException { @java.io.Serial static final long serialVersionUID = 705788143955048766L; /** - * Constructs a {@code TimeoutException} with no detail message. + * Constructs a {@code CancelledByTimeoutException} with no detail message. */ - TimeoutException() { } + public CancelledByTimeoutException() { } + } /** - * Opens a new {@code StructuredTaskScope} to use the given {@code Joiner} object and - * with configuration that is the result of applying the given operator to the - * {@linkplain ##DefaultConfiguration default configuration}. + * Opens a new {@code StructuredTaskScope} that uses the given {@code Joiner} object + * and the {@code Configuration} that is the result of applying the given operator to + * the {@linkplain ##DefaultConfiguration default configuration}. * - *

The {@code configOperator} is called with the default configuration and returns - * the configuration for the new scope. The operator may, for example, set the - * {@linkplain Configuration#withThreadFactory(ThreadFactory) ThreadFactory} or set a - * {@linkplain Configuration#withTimeout(Duration) timeout}. If the operator completes - * with an exception or error then it is propagated by this method. If the operator - * returns {@code null} then {@code NullPointerException} is thrown. + *

The {@code Joiner} specified to this method implements the desired policy and + * produces the outcome (result or exception) for the {@link #join()} method when all + * subtasks forked in the scope complete execution or the scope is {@linkplain + * ##Cancellation cancelled}. * - *

If a {@code ThreadFactory} is set then its {@link ThreadFactory#newThread(Runnable) - * newThread} method will be called to create threads when {@linkplain #fork(Callable) - * forking} subtasks in this scope. If a {@code ThreadFactory} is not set then - * forking subtasks will create an unnamed virtual thread for each subtask. - * - *

If a {@linkplain Configuration#withTimeout(Duration) timeout} is set then it - * starts when the scope is opened. If the timeout expires before or while waiting in - * {@link #join()} then the scope is {@linkplain ##Cancellation cancelled} - * and the {@code Joiner}'s {@link Joiner#onTimeout() onTimeout()} method is invoked - * to optionally throw {@link TimeoutException TimeoutException}. + *

This method invokes the operator's {@link UnaryOperator#apply(Object) apply} + * method with the default configuration to produce the configuration for the new + * scope: + *

    + *
  • If the {@code apply} method returns a {@code Configuration} with a {@link + * ThreadFactory}, set using {@link Configuration#withThreadFactory(ThreadFactory) + * withThreadFactory(ThreadFactory)}, its {@link ThreadFactory#newThread(Runnable) + * newThread(Runnable)} method will be invoked to create threads when {@linkplain + * #fork(Callable) forking} subtasks in the scope. If a {@code ThreadFactory} is not set + * then forking subtasks will create an unnamed virtual thread for each subtask.
  • + *
  • If the {@code apply} method returns a {@code Configuration} with a timeout, set + * using {@link Configuration#withTimeout(Duration) withTimeout(Duration)}, the timeout + * will start when the scope is opened. If the timeout expires before or while waiting in + * {@link #join()} then the scope will be {@linkplain ##Cancellation cancelled}. It is + * {@link Joiner Joiner} specific as to whether the {@code join()} method returns a + * result or throws an exception when a timeout occurs. If the outcome is an exception + * then it will be thrown with a {@link CancelledByTimeoutException + * CancelledByTimeoutException} as the {@linkplain Throwable#getCause() cause}.
  • + *
  • If the {@code apply} method returns a {@code Configuration} with a name, set + * using {@linkplain Configuration#withName(String) withName(String)}, the name will + * be used for monitoring and management purposes.
  • + *
  • If the {@code apply} method throws an exception or error then it is propagated + * by this method.
  • + *
  • If the {@code apply} method returns {@code null} then {@code NullPointerException} + * is thrown.
  • + *
* *

The new scope is owned by the current thread. Only code executing in this * thread can {@linkplain #fork(Callable) fork}, {@linkplain #join() join}, or * {@linkplain #close close} the scope. * *

Construction captures the current thread's {@linkplain ScopedValue scoped - * value} bindings for inheritance by threads started in the scope. + * value} bindings for inheritance by threads forked in the scope. * - * @param joiner the joiner + * @param joiner the Joiner * @param configOperator the operator to produce the configuration * @return a new scope - * @param the result type of subtasks executed in the scope - * @param the result type of the scope + * @param the result type of subtasks {@linkplain #fork(Callable) forked} in the scope + * @param the type of the result returned by the {@link #join() join()} method + * @param the type of the exception thrown by the {@link #join() join()} method * @since 26 */ - static StructuredTaskScope open(Joiner joiner, - UnaryOperator configOperator) { + static StructuredTaskScope + open(Joiner joiner, UnaryOperator configOperator) { return StructuredTaskScopeImpl.open(joiner, configOperator); } /** - * Opens a new {@code StructuredTaskScope}to use the given {@code Joiner} object. The - * scope is created with the {@linkplain ##DefaultConfiguration default configuration}. - * The default configuration has a {@code ThreadFactory} that creates unnamed - * {@linkplain Thread##irtual-threads virtual threads}, does not name the scope, and - * has no timeout. - * - * @implSpec - * This factory method is equivalent to invoking the 2-arg open method with the given - * joiner and the {@linkplain UnaryOperator#identity() identity operator}. - * - * @param joiner the joiner - * @return a new scope - * @param the result type of subtasks executed in the scope - * @param the result type of the scope - * @since 25 - */ - static StructuredTaskScope open(Joiner joiner) { - return open(joiner, UnaryOperator.identity()); - } - - /** - * Opens a new {@code StructuredTaskScope} that can be used to fork subtasks that return - * results of any type. The scope's {@link #join()} method waits for all subtasks to - * succeed or any subtask to fail. - * - *

The {@code join} method returns {@code null} if all subtasks complete successfully. - * It throws {@link FailedException} if any subtask fails, with the exception from - * the first subtask to fail as the cause. + * Opens a new {@code StructuredTaskScope} that uses the given {@code Joiner} object. + * The {@code Joiner} implements the desired policy and produces the outcome (result + * or exception) for the {@link #join()} method when all subtasks forked in the scope + * complete execution or the scope is {@linkplain ##Cancellation cancelled}. * *

The scope is created with the {@linkplain ##DefaultConfiguration default * configuration}. The default configuration has a {@code ThreadFactory} that creates * unnamed {@linkplain Thread##virtual-threads virtual threads}, does not name the * scope, and has no timeout. * - * @implSpec - * This factory method is equivalent to invoking the 2-arg open method with a joiner - * created with {@link Joiner#awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow()} + *

The new scope is owned by the current thread. Only code executing in this + * thread can {@linkplain #fork(Callable) fork}, {@linkplain #join() join}, or + * {@linkplain #close close} the scope. + * + *

Construction captures the current thread's {@linkplain ScopedValue scoped + * value} bindings for inheritance by threads forked in the scope. + * + * @implSpec This factory method is equivalent to invoking the + * {@linkplain #open(Joiner, UnaryOperator) 2-arg open} method with the given Joiner * and the {@linkplain UnaryOperator#identity() identity operator}. * + * @param joiner the Joiner + * @return a new scope + * @param the result type of subtasks {@linkplain #fork(Callable) forked} in the scope + * @param the type of the result returned by the {@link #join() join()} method + * @param the type of the exception thrown by the {@link #join() join()} method + * @since 25 + */ + static StructuredTaskScope + open(Joiner joiner) { + return open(joiner, UnaryOperator.identity()); + } + + /** + * Opens a new {@code StructuredTaskScope} that uses the {@code Configuration} that is + * the result of applying the given operator to the {@linkplain ##DefaultConfiguration + * default configuration}. This method invokes the operator's {@link + * UnaryOperator#apply(Object) apply} method to produce the configuration for the new + * scope, as specified by the {@linkplain #open(Joiner, UnaryOperator) 2-arg open} + * method. + * + *

The {@link #join()} method of the new scope waits for all subtasks to succeed + * or any subtask to fail. The {@code join() method} returns {@code null} if all + * subtasks complete successfully. It throws {@link ExecutionException} if any subtask + * fails, with the exception from the first subtask to fail as the {@linkplain + * Throwable#getCause() cause}. If a {@linkplain Configuration#withTimeout(Duration) + * timeout} is configured, and the timeout expires before or while waiting in the + * {@code join()} method, it throws {@code ExecutionException} with a {@link + * CancelledByTimeoutException CancelledByTimeoutException} as the cause. + * + *

The new scope is owned by the current thread. Only code executing in this + * thread can {@linkplain #fork(Callable) fork}, {@linkplain #join() join}, or + * {@linkplain #close close} the scope. + * + *

Construction captures the current thread's {@linkplain ScopedValue scoped + * value} bindings for inheritance by threads forked in the scope. + * + * @implSpec This factory method is equivalent to invoking the {@linkplain + * #open(Joiner, UnaryOperator) 2-arg open} method with a Joiner created with {@link + * Joiner#awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow()} and the given + * configuration operator. + * + * @param configOperator the operator to produce the configuration + * @return a new scope + * @param the result type of subtasks forked in the scope + * @since 27 + */ + static StructuredTaskScope + open(UnaryOperator configOperator) { + return open(Joiner.awaitAllSuccessfulOrThrow(), configOperator); + } + + /** + * Opens a new {@code StructuredTaskScope} where {@link #join()} waits for all subtasks + * to succeed or any subtask to fail. The {@code join()} method returns {@code null} + * if all subtasks complete successfully. It throws {@link ExecutionException} if any + * subtask fails, with the exception from the first subtask to fail as the {@linkplain + * Throwable#getCause() cause}. + * + *

The scope is created with the {@linkplain ##DefaultConfiguration default + * configuration}. The default configuration has a {@code ThreadFactory} that creates + * unnamed {@linkplain Thread##virtual-threads virtual threads}, does not name the + * scope, and has no timeout. + * + *

The new scope is owned by the current thread. Only code executing in this + * thread can {@linkplain #fork(Callable) fork}, {@linkplain #join() join}, or + * {@linkplain #close close} the scope. + * + *

Construction captures the current thread's {@linkplain ScopedValue scoped + * value} bindings for inheritance by threads forked in the scope. + * + * @implSpec This factory method is equivalent to invoking the + * {@linkplain #open(Joiner, UnaryOperator) 2-arg open} method with a Joiner created + * with {@link Joiner#awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow()} and + * the {@linkplain UnaryOperator#identity() identity operator}. + * * @param the result type of subtasks * @return a new scope * @since 25 */ - static StructuredTaskScope open() { + static StructuredTaskScope open() { return open(Joiner.awaitAllSuccessfulOrThrow(), UnaryOperator.identity()); } /** - * Fork a subtask by starting a new thread in this scope to execute a value-returning + * Forks a subtask by starting a new thread in this scope to execute a value-returning * method. The new thread executes the subtask concurrently with the current thread. * The parameter to this method is a {@link Callable}, the new thread executes its - * {@link Callable#call() call()} method. + * {@link Callable#call() call()} method. The thread inherits the current thread's + * {@linkplain ScopedValue scoped value bindings} that must match the bindings + * captured when the scope was opened. * - *

This method first creates a {@link Subtask Subtask} object to represent the - * forked subtask. It invokes the joiner's {@link Joiner#onFork(Subtask) onFork} - * method with the subtask in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state. - * If the {@code onFork} completes with an exception or error then it is propagated by - * the {@code fork} method without creating a thread. If the scope is already - * {@linkplain ##Cancellation cancelled}, or {@code onFork} returns {@code true} to - * cancel the scope, then this method returns the {@code Subtask}, in the - * {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state, without creating a thread to - * execute the subtask. + *

If the scope was opened with a function to produce the {@link Configuration + * Configuration} for the scope, and a {@link ThreadFactory} is {@linkplain + * Configuration#withThreadFactory(ThreadFactory) set}, then its {@link + * ThreadFactory#newThread(Runnable) newThread(Runnable)} method is invoked to create + * the thread that will execute the subtask. {@link RejectedExecutionException} is + * thrown if the {@code newThread(Runnable)} method returns {@code null}. + * If a {@code ThreadFactory} is not set, the {@code fork(Callable)} method creates an + * unnamed {@linkplain Thread##virtual-threads virtual thread} to execute the subtask. * - *

If the scope is not cancelled, and the {@code onFork} method returns {@code false}, - * then a thread is created with the {@link ThreadFactory} configured when the scope - * was opened, and the thread is started. Forking a subtask inherits the current thread's - * {@linkplain ScopedValue scoped value} bindings. The bindings must match the bindings - * captured when the scope was opened. If the subtask completes (successfully or with - * an exception) before the scope is cancelled, then the thread invokes the joiner's - * {@link Joiner#onComplete(Subtask) onComplete} method with the subtask in the - * {@link Subtask.State#SUCCESS SUCCESS} or {@link Subtask.State#FAILED FAILED} state. - * If the {@code onComplete} method completes with an exception or error, then the - * {@linkplain Thread.UncaughtExceptionHandler uncaught exception handler} is invoked - * with the exception or error before the thread terminates. + *

This method returns a {@link Subtask Subtask} object as a handle to the + * forked subtask. If the scope is {@linkplain ##Cancellation cancelled}, the + * {@code Subtask} is returned in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} + * state without creating a thread. In some usages, {@code Subtask} object will be used + * by the "main" task (the scope owner) to get the subtask's outcome (result or + * exception) after it has invoked {@link #join() join()} to wait for all subtasks to + * complete. In other usages, the scope is created with a {@link Joiner Joiner} that + * produces the outcome for the main task to process after joining. A {@code Joiner} + * that produces a result reduces the need for bookkeeping and the need for the + * main task to retain references to {@code Subtask} objects for correlation purposes. * - *

This method returns the {@link Subtask Subtask} object. In some usages, this - * object may be used to get its result. In other cases it may be used for correlation - * or be discarded. To ensure correct usage, the {@link Subtask#get() Subtask.get()} - * method may only be called by the scope owner to get the result after it has - * waited for subtasks to complete with the {@link #join() join} method and the subtask - * completed successfully. Similarly, the {@link Subtask#exception() Subtask.exception()} - * method may only be called by the scope owner after it has joined and the subtask - * failed. If the scope was cancelled before the subtask was forked, or before it - * completes, then neither method can be used to obtain the outcome. + *

To ensure correct usage, the {@link Subtask#get() Subtask.get()} method may + * only be called by the scope owner to get the result of a successful subtask after + * it has waited for subtasks to complete with the {@link #join() join()} method. + * Similarly, the {@link Subtask#exception() Subtask.exception()} method may only be + * called by the scope owner to get the exception (or error) of a failed subtask after + * it has joined. * *

This method may only be invoked by the scope owner. * + *

Joiner Implementers: + * + *

If the scope was opened with a {@link Joiner Joiner}, its {@link + * Joiner#onFork(Subtask) onFork(Subtask)} method is invoked with the newly created + * {@code Subtask} object before the thread is created. It is invoked with the subtask + * in the {@code UNAVAILABLE} state. If the method throws an exception or error then + * it is propagated by the {@code fork(Callable)} method without creating a thread to + * execute the subtask. If the scope is not already cancelled, and the {@code onFork} + * method return {@code false}, then a thread is created and {@linkplain Thread#start() + * scheduled} to start execution of the subtask. If the scope is cancelled, or the + * {@code onFork} method returns {@code true} to cancel the scope, {@code fork(Callable)} + * returns the subtask in the {@code UNAVAILABLE} state. + * + *

If the subtask executes and completes (successfully or with an exception) before + * the scope is cancelled, then the thread invokes the Joiner's {@link + * Joiner#onComplete(Subtask) onComplete(Subtask)} method with the subtask in the + * {@link Subtask.State#SUCCESS SUCCESS} or {@link Subtask.State#FAILED FAILED} state. + * If the {@code onComplete(Subtask}) method returns {@code true} then the scope is + * cancelled, if not already cancelled. If the {@code onComplete(Subtask)} method + * completes with an exception or error, then the thread executes the {@linkplain + * Thread.UncaughtExceptionHandler uncaught exception handler} before the thread + * terminates. + * * @param task the value-returning task for the thread to execute * @param the result type * @return the subtask @@ -1035,17 +1338,18 @@ public sealed interface StructuredTaskScope * @throws StructureViolationException if the current scoped value bindings are not * the same as when the scope was created * @throws RejectedExecutionException if the thread factory rejected creating a - * thread to run the subtask + * thread to execute the subtask + * @see Thread##inheritance Inheritance When Creating Threads */ Subtask fork(Callable task); /** - * Fork a subtask by starting a new thread in this scope to execute a method that + * Forks a subtask by starting a new thread in this scope to execute a method that * does not return a result. * *

This method works exactly the same as {@link #fork(Callable)} except that the * parameter to this method is a {@link Runnable}, the new thread executes its - * {@link Runnable#run() run} method, and {@link Subtask#get() Subtask.get()} returns + * {@link Runnable#run() run()} method, and {@link Subtask#get() Subtask.get()} returns * {@code null} if the subtask completes successfully. * * @param task the task for the thread to execute @@ -1057,48 +1361,59 @@ public sealed interface StructuredTaskScope * @throws StructureViolationException if the current scoped value bindings are not * the same as when the scope was created * @throws RejectedExecutionException if the thread factory rejected creating a - * thread to run the subtask + * thread to execute the subtask * @since 25 */ Subtask fork(Runnable task); /** - * Returns the result, or throws, after waiting for all subtasks to complete or - * the scope to be {@linkplain ##Cancellation cancelled}. + * Returns a result, or throws, after waiting for all subtasks to complete or the + * scope to be {@linkplain ##Cancellation cancelled}. * - *

This method waits for all subtasks started in this scope to complete or the - * scope to be cancelled. Once finished waiting, the {@code Joiner}'s {@link - * Joiner#result() result()} method is invoked to get the result or throw an exception. - * If the {@code result()} method throws then {@code join()} throws - * {@code FailedException} with the exception from the {@code Joiner} as the cause. + *

If the scope was opened with the {@link #open()} or {@link #open(UnaryOperator)} + * method, then {@code join()} waits for all subtasks to succeed or any subtask to fail. + * It returns {@code null} if all subtasks complete successfully. It throws {@link + * ExecutionException} if any subtask fails, with exception from the first subtask to + * fail as the {@linkplain Throwable#getCause() cause}. If a {@linkplain + * Configuration#withTimeout(Duration) timeout} is configured and the timeout expires + * before or while waiting, it throws {@code ExecutionException} with a {@link + * CancelledByTimeoutException CancelledByTimeoutException} as the cause. * - *

If a {@linkplain Configuration#withTimeout(Duration) timeout} is configured, - * and the timeout expires before or while waiting, then the scope is cancelled and - * the {@code Joiner}'s {@link Joiner#onTimeout() onTimeout()} method is invoked - * before calling the {@code Joiner}'s {@code result()} method. If the {@code onTimeout()} - * method throws {@link TimeoutException TimeoutException} (or throws any exception - * or error), then it is propagated by this method. If the {@code onTimeout()} method - * does not throw then the {@code Joiner}'s {@code result()} method is invoked to - * get the result or throw. + *

If the scope was opened with a {@link Joiner Joiner}, it is invoked after + * waiting or cancellation to produce the outcome (result or exception). This includes + * the timeout case where a timeout is configured and it expires before or + * while waiting in the {@code join()} method. * *

This method may only be invoked by the scope owner. It may only be invoked once * to get the result, exception or timeout outcome, unless the previous invocation * resulted in an {@code InterruptedException} being thrown. * + *

Joiner Implementers: + * + *

When all subtasks complete, or the scope cancelled, this method invokes the + * {@code Joiner}'s {@link Joiner#result() result()} or {@link Joiner#timeout() timeout()} + * method to produce the outcome (result or exception) for the {@code join()} method. + * The {@code result()} method is invoked for the "no timeout" case. The {@code timeout()} + * method is invoked for the timeout case. If the outcome for the timeout case is an + * exception then it is thrown with a {@code CancelledByTimeoutException} as the cause. + * + * @apiNote When the outcome is an exception, the {@linkplain Throwable#getStackTrace() + * stack trace} will be the stack trace of the call to the {@code join()} method. + * Its {@linkplain Throwable#getCause() cause} will typically be the exception + * thrown by a failed subtask with the stack trace of the failed subtask. + * * @return the result * @throws WrongThreadException if the current thread is not the scope owner * @throws IllegalStateException if already joined or this scope is closed - * @throws FailedException if the outcome is an exception, thrown with the - * exception from {@link Joiner#result() Joiner.result()} as the cause - * @throws TimeoutException if a timeout is set, the timeout expires before or while - * waiting, and {@link Joiner#onTimeout() Joiner.onTimeout()} throws this exception - * @throws InterruptedException if the current thread is interrupted before or - * while waiting. The current thread's interrupted status is cleared when this - * exception is thrown. + * @throws R_X when the outcome is an exception + * @throws InterruptedException if the current thread is {@linkplain Thread#interrupt() + * interrupted} while waiting or this method is invoked with the current thread's + * {@linkplain Thread#isInterrupted() interrupted status} set. The current thread's + * interrupted status is cleared when this exception is thrown. * @since 25 * @see Thread##thread-interruption Thread Interruption */ - R join() throws InterruptedException; + R join() throws R_X, InterruptedException; /** * {@return {@code true} if this scope is {@linkplain ##Cancellation cancelled} or in @@ -1110,9 +1425,10 @@ public sealed interface StructuredTaskScope * method may return {@code true} before all threads have been interrupted or before * all threads have finished. * - * @apiNote A task with a lengthy "forking phase" (the code that executes before - * it invokes {@link #join() join}) may use this method to avoid doing work in cases - * where scope is cancelled by the completion of a previously forked subtask or timeout. + * @apiNote A task with a lengthy "forking phase" (the code in the {@code try} block + * that forks subtasks before the {@link #join()} method is invoked) may use this + * method to avoid doing work in cases where the scope is cancelled by the completion + * of a previously forked subtask or a timeout. * * @since 25 */ @@ -1121,29 +1437,27 @@ public sealed interface StructuredTaskScope /** * Closes this scope. * - *

This method first {@linkplain ##Cancellation cancels} the scope, if not - * already cancelled. This interrupts the threads executing unfinished subtasks. This - * method then waits for all threads to finish. If interrupted while waiting then it - * will continue to wait until the threads finish, before completing with the - * {@linkplain Thread#isInterrupted() interrupted status} set. + *

This method first {@linkplain ##Cancellation cancels} the scope, if not already + * cancelled. This {@linkplain Thread#interrupt() interrupts} the threads executing + * unfinished subtasks. This method then waits for all threads to finish. If interrupted + * while waiting then it will continue to wait until the threads finish, before + * completing with the {@linkplain Thread#isInterrupted() interrupted status} set. * *

This method may only be invoked by the scope owner. If the scope * is already closed then the scope owner invoking this method has no effect. * *

A {@code StructuredTaskScope} is intended to be used in a structured - * manner. If this method is called to close a scope before nested task - * scopes are closed then it closes the underlying construct of each nested scope - * (in the reverse order that they were created in), closes this scope, and then - * throws {@link StructureViolationException}. - * Similarly, if this method is called to close a scope while executing with - * {@linkplain ScopedValue scoped value} bindings, and the scope was created - * before the scoped values were bound, then {@code StructureViolationException} is - * thrown after closing the scope. - * If a thread terminates without first closing scopes that it owns then - * termination will cause the underlying construct of each of its open tasks scopes to - * be closed. Closing is performed in the reverse order that the scopes were - * created in. Thread termination may therefore be delayed when the scope owner - * has to wait for threads forked in these scopes to finish. + * manner. If this method is called to close a scope before nested scopes are + * closed then it closes the underlying construct of each nested scope (in the reverse + * order that they were created in), closes this scope, and then throws {@link + * StructureViolationException}. Similarly, if this method is called to close a scope + * while executing with {@linkplain ScopedValue scoped value} bindings, and the scope + * was created before the scoped values were bound, then {@code StructureViolationException} + * is thrown after closing the scope. If a thread terminates without first closing + * scopes that it owns then termination will cause the underlying construct of each + * of its open scopes to be closed. Closing is performed in the reverse order that the + * scopes were created in. Thread termination may therefore be delayed when the scope + * owner has to wait for threads forked in these scopes to finish. * * @throws IllegalStateException thrown after closing the scope if the scope * owner did not attempt to join after forking diff --git a/src/java.base/share/classes/java/util/concurrent/StructuredTaskScopeImpl.java b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScopeImpl.java index 1051f9244f3..fbd58fc2a21 100644 --- a/src/java.base/share/classes/java/util/concurrent/StructuredTaskScopeImpl.java +++ b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScopeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,11 +36,12 @@ import jdk.internal.vm.annotation.Stable; /** * StructuredTaskScope implementation. */ -final class StructuredTaskScopeImpl implements StructuredTaskScope { +final class StructuredTaskScopeImpl + implements StructuredTaskScope { private static final VarHandle CANCELLED = MhUtil.findVarHandle(MethodHandles.lookup(), "cancelled", boolean.class); - private final Joiner joiner; + private final Joiner joiner; private final ThreadFactory threadFactory; private final ThreadFlock flock; @@ -61,7 +62,7 @@ final class StructuredTaskScopeImpl implements StructuredTaskScope { private volatile boolean timeoutExpired; @SuppressWarnings("this-escape") - private StructuredTaskScopeImpl(Joiner joiner, + private StructuredTaskScopeImpl(Joiner joiner, ThreadFactory threadFactory, String name) { this.joiner = joiner; @@ -74,12 +75,12 @@ final class StructuredTaskScopeImpl implements StructuredTaskScope { * and with configuration that is the result of applying the given function to the * default configuration. */ - static StructuredTaskScope open(Joiner joiner, - UnaryOperator configOperator) { + static StructuredTaskScope + open(Joiner joiner, UnaryOperator configOperator) { Objects.requireNonNull(joiner); var config = (ConfigImpl) configOperator.apply(ConfigImpl.defaultConfig()); - var scope = new StructuredTaskScopeImpl(joiner, config.threadFactory(), config.name()); + var scope = new StructuredTaskScopeImpl(joiner, config.threadFactory(), config.name()); // schedule timeout Duration timeout = config.timeout(); @@ -173,7 +174,7 @@ final class StructuredTaskScopeImpl implements StructuredTaskScope { private void onComplete(SubtaskImpl subtask) { assert subtask.state() != Subtask.State.UNAVAILABLE; @SuppressWarnings("unchecked") - var j = (Joiner) joiner; + var j = (Joiner) joiner; if (j.onComplete(subtask)) { cancel(); } @@ -192,7 +193,7 @@ final class StructuredTaskScopeImpl implements StructuredTaskScope { // notify joiner, even if cancelled @SuppressWarnings("unchecked") - var j = (Joiner) joiner; + var j = (Joiner) joiner; if (j.onFork(subtask)) { cancel(); } @@ -228,7 +229,7 @@ final class StructuredTaskScopeImpl implements StructuredTaskScope { } @Override - public R join() throws InterruptedException { + public R join() throws R_X, InterruptedException { ensureOwner(); if (state >= ST_JOIN_COMPLETED) { throw new IllegalStateException("Already joined or scope is closed"); @@ -245,19 +246,13 @@ final class StructuredTaskScopeImpl implements StructuredTaskScope { // all subtasks completed or scope cancelled state = ST_JOIN_COMPLETED; - // invoke joiner onTimeout if timeout expired + // invoke joiner result() or timeout() method if (timeoutExpired) { - cancel(); // ensure cancelled before calling onTimeout - joiner.onTimeout(); + cancel(); // ensure cancelled before calling joiner + return joiner.timeout(); } else { cancelTimeout(); - } - - // invoke joiner to get result - try { return joiner.result(); - } catch (Throwable e) { - throw new FailedException(e); } } @@ -310,12 +305,12 @@ final class StructuredTaskScopeImpl implements StructuredTaskScope { } } - private final StructuredTaskScopeImpl scope; + private final StructuredTaskScopeImpl scope; private final Callable task; private volatile Object result; @Stable private Thread thread; - SubtaskImpl(StructuredTaskScopeImpl scope, Callable task) { + SubtaskImpl(StructuredTaskScopeImpl scope, Callable task) { this.scope = scope; this.task = task; } diff --git a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java index a698440c15d..2c8ab1f86c0 100644 --- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java +++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java @@ -64,7 +64,7 @@ public @interface PreviewFeature { * Values should be annotated with the feature's {@code JEP}. */ public enum Feature { - @JEP(number=525, title="Structured Concurrency", status="Sixth Preview") + @JEP(number=533, title="Structured Concurrency", status="Seventh Preview") STRUCTURED_CONCURRENCY, @JEP(number = 526, title = "Lazy Constants", status = "Second Preview") LAZY_CONSTANTS, diff --git a/test/jdk/java/lang/ScopedValue/StressStackOverflow.java b/test/jdk/java/lang/ScopedValue/StressStackOverflow.java index 088bf62ee72..5014a8eeae9 100644 --- a/test/jdk/java/lang/ScopedValue/StressStackOverflow.java +++ b/test/jdk/java/lang/ScopedValue/StressStackOverflow.java @@ -25,26 +25,26 @@ * @test id=default * @summary Stress ScopedValue stack overflow recovery path * @enablePreview - * @run main/othervm/timeout=300 StressStackOverflow + * @run main/othervm/timeout=300 ${test.main.class} */ /* * @test id=no-TieredCompilation * @enablePreview - * @run main/othervm/timeout=300 -XX:-TieredCompilation StressStackOverflow + * @run main/othervm/timeout=300 -XX:-TieredCompilation ${test.main.class} */ /* * @test id=TieredStopAtLevel1 * @enablePreview - * @run main/othervm/timeout=300 -XX:TieredStopAtLevel=1 StressStackOverflow + * @run main/othervm/timeout=300 -XX:TieredStopAtLevel=1 ${test.main.class} */ /* * @test id=no-vmcontinuations * @requires vm.continuations * @enablePreview - * @run main/othervm/timeout=300 -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations StressStackOverflow + * @run main/othervm/timeout=300 -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations ${test.main.class} */ import java.lang.ScopedValue.CallableOp; @@ -52,7 +52,6 @@ import java.time.Duration; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.StructureViolationException; import java.util.concurrent.StructuredTaskScope; -import java.util.concurrent.StructuredTaskScope.Joiner; import java.util.function.Supplier; public class StressStackOverflow { @@ -170,7 +169,7 @@ public class StressStackOverflow { void runInNewThread(Runnable op) { var threadFactory = (ThreadLocalRandom.current().nextBoolean() ? Thread.ofPlatform() : Thread.ofVirtual()).factory(); - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withThreadFactory(threadFactory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(threadFactory))) { var handle = scope.fork(() -> { op.run(); return null; @@ -187,7 +186,7 @@ public class StressStackOverflow { public void run() { try { ScopedValue.where(inheritedValue, 42).where(el, 0).run(() -> { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { try { if (ThreadLocalRandom.current().nextBoolean()) { // Repeatedly test Scoped Values set by ScopedValue::call(), get(), and run() diff --git a/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java index 83adc930bb3..3fdc582a29c 100644 --- a/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java +++ b/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,7 @@ * @test * @summary Stress test of StructuredTaskScope cancellation with running and starting threads * @enablePreview - * @run junit StressCancellation + * @run junit ${test.main.class} */ import java.time.Duration; @@ -65,7 +65,7 @@ class StressCancellation { @ParameterizedTest @MethodSource("testCases") void test(ThreadFactory factory, int beforeCancel, int afterCancel) throws Exception { - var joiner = new Joiner() { + var joiner = new Joiner() { @Override public boolean onComplete(Subtask subtask) { boolean cancel = subtask.get(); @@ -75,6 +75,10 @@ class StressCancellation { public Void result() { return null; } + @Override + public Void timeout() { + return null; + } }; try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory(factory))) { diff --git a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java index b0d3ae0cf6b..fa08834a559 100644 --- a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java +++ b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,16 +23,16 @@ /* * @test id=platform - * @bug 8284199 8296779 8306647 + * @bug 8284199 8296779 8306647 8380109 * @summary Basic tests for StructuredTaskScope * @enablePreview - * @run junit/othervm -DthreadFactory=platform StructuredTaskScopeTest + * @run junit/othervm -DthreadFactory=platform ${test.main.class} */ /* * @test id=virtual * @enablePreview - * @run junit/othervm -DthreadFactory=virtual StructuredTaskScopeTest + * @run junit/othervm -DthreadFactory=virtual ${test.main.class} */ import java.time.Duration; @@ -42,9 +42,11 @@ import java.util.List; import java.util.Set; import java.util.NoSuchElementException; import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.ThreadFactory; @@ -52,15 +54,15 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.StructuredTaskScope; -import java.util.concurrent.StructuredTaskScope.TimeoutException; import java.util.concurrent.StructuredTaskScope.Configuration; -import java.util.concurrent.StructuredTaskScope.FailedException; import java.util.concurrent.StructuredTaskScope.Joiner; import java.util.concurrent.StructuredTaskScope.Subtask; +import java.util.concurrent.StructuredTaskScope.CancelledByTimeoutException; import java.util.concurrent.StructureViolationException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.UnaryOperator; import java.util.stream.Stream; @@ -102,12 +104,12 @@ class StructuredTaskScopeTest { } /** - * Test that fork creates virtual threads when no ThreadFactory is configured. + * Test that fork creates unnamed virtual threads when no ThreadFactory is configured. */ @Test void testForkCreatesVirtualThread() throws Exception { Set threads = ConcurrentHashMap.newKeySet(); - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { for (int i = 0; i < 50; i++) { // runnable scope.fork(() -> { @@ -123,11 +125,14 @@ class StructuredTaskScopeTest { scope.join(); } assertEquals(100, threads.size()); - threads.forEach(t -> assertTrue(t.isVirtual())); + threads.forEach(t -> { + assertTrue(t.isVirtual()); + assertEquals("", t.getName()); + }); } /** - * Test that fork create threads with the configured ThreadFactory. + * Test that fork creates threads with the configured ThreadFactory. */ @ParameterizedTest @MethodSource("factories") @@ -151,8 +156,7 @@ class StructuredTaskScopeTest { } var recordingThreadFactory = new RecordingThreadFactory(factory); Set threads = ConcurrentHashMap.newKeySet(); - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(recordingThreadFactory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(recordingThreadFactory))) { for (int i = 0; i < 50; i++) { // runnable @@ -178,8 +182,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testForkConfined(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { // random thread cannot fork try (var pool = Executors.newSingleThreadExecutor()) { @@ -210,8 +213,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testForkAfterJoinCompleted1(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { scope.join(); assertThrows(IllegalStateException.class, () -> scope.fork(() -> "bar")); } @@ -223,8 +225,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testForkAfterJoinCompleted2(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { scope.fork(() -> "foo"); scope.join(); assertThrows(IllegalStateException.class, () -> scope.fork(() -> "bar")); @@ -237,8 +238,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testForkAfterJoinInterrupted(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { var subtask1 = scope.fork(() -> { Thread.sleep(Duration.ofDays(1)); return "foo"; @@ -259,13 +259,14 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testForkAfterJoinTimeout(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), + try (var scope = StructuredTaskScope.open( cf -> cf.withThreadFactory(factory) .withTimeout(Duration.ofMillis(100)))) { awaitCancelled(scope); // join throws - assertThrows(TimeoutException.class, scope::join); + var e = assertThrows(ExecutionException.class, scope::join); + assertInstanceOf(CancelledByTimeoutException.class, e.getCause()); // fork should throw assertThrows(IllegalStateException.class, () -> scope.fork(() -> "bar")); @@ -317,8 +318,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testForkAfterClose(ThreadFactory factory) { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { scope.close(); assertThrows(IllegalStateException.class, () -> scope.fork(() -> null)); } @@ -330,8 +330,7 @@ class StructuredTaskScopeTest { @Test void testForkRejectedExecutionException() { ThreadFactory factory = task -> null; - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { assertThrows(RejectedExecutionException.class, () -> scope.fork(() -> null)); } } @@ -341,7 +340,7 @@ class StructuredTaskScopeTest { */ @Test void testJoinWithNoSubtasks() throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { scope.join(); } } @@ -352,8 +351,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testJoinWithRemainingSubtasks(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask = scope.fork(() -> { Thread.sleep(Duration.ofMillis(100)); return "foo"; @@ -368,9 +366,7 @@ class StructuredTaskScopeTest { */ @Test void testJoinAfterJoin1() throws Exception { - var results = new LinkedTransferQueue<>(List.of("foo", "bar", "baz")); - Joiner joiner = results::take; - try (var scope = StructuredTaskScope.open(joiner)) { + try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow())) { scope.fork(() -> "foo"); assertEquals("foo", scope.join()); @@ -386,9 +382,9 @@ class StructuredTaskScopeTest { */ @Test void testJoinAfterJoin2() throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow())) { + try (var scope = StructuredTaskScope.open()) { scope.fork(() -> { throw new FooException(); }); - Throwable ex = assertThrows(FailedException.class, scope::join); + Throwable ex = assertThrows(ExecutionException.class, scope::join); assertTrue(ex.getCause() instanceof FooException); // join already called @@ -430,11 +426,11 @@ class StructuredTaskScopeTest { */ @Test void testJoinAfterJoinTimeout() throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow(), - cf -> cf.withTimeout(Duration.ofMillis(100)))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withTimeout(Duration.ofMillis(100)))) { // wait for scope to be cancelled by timeout awaitCancelled(scope); - assertThrows(TimeoutException.class, scope::join); + var e = assertThrows(ExecutionException.class, scope::join); + assertInstanceOf(CancelledByTimeoutException.class, e.getCause()); // join already called for (int i = 0 ; i < 3; i++) { @@ -444,31 +440,31 @@ class StructuredTaskScopeTest { } /** - * Test join invoked from Joiner.onTimeout. + * Test join invoked from Joiner.timeout() */ @Test - void testJoinInOnTimeout() throws Exception { + void testJoinInTimeout() throws Exception { Thread owner = Thread.currentThread(); - var scopeRef = new AtomicReference>(); + var scopeRef = new AtomicReference>(); - var joiner = new Joiner() { - @Override - public void onTimeout() { - assertTrue(Thread.currentThread() == owner); - var scope = scopeRef.get(); - assertThrows(IllegalStateException.class, scope::join); - } + var joiner = new Joiner() { @Override public Void result() { return null; } + @Override + public Void timeout() { + assertSame(owner, Thread.currentThread()); + var scope = scopeRef.get(); + assertThrows(IllegalStateException.class, scope::join); + return null; + } }; - try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withTimeout(Duration.ofMillis(100)))) { awaitCancelled(scope); scopeRef.set(scope); - scope.join(); // invokes onTimeout + scope.join(); // invokes timeout() } } @@ -478,8 +474,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testJoinConfined(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { // random thread cannot join try (var pool = Executors.newSingleThreadExecutor()) { @@ -506,8 +501,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testInterruptJoin1(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask = scope.fork(() -> { Thread.sleep(Duration.ofDays(1)); @@ -531,8 +525,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testInterruptJoin2(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask = scope.fork(() -> { Thread.sleep(Duration.ofDays(1)); return "foo"; @@ -583,7 +576,7 @@ class StructuredTaskScopeTest { */ @Test void testJoinAfterClose() throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { scope.close(); assertThrows(IllegalStateException.class, () -> scope.join()); } @@ -595,7 +588,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testJoinWithTimeout1(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), + try (var scope = StructuredTaskScope.open( cf -> cf.withThreadFactory(factory) .withTimeout(Duration.ofDays(1)))) { @@ -618,7 +611,7 @@ class StructuredTaskScopeTest { @MethodSource("factories") void testJoinWithTimeout2(ThreadFactory factory) throws Exception { long startMillis = millisTime(); - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), + try (var scope = StructuredTaskScope.open( cf -> cf.withThreadFactory(factory) .withTimeout(Duration.ofSeconds(2)))) { @@ -627,7 +620,8 @@ class StructuredTaskScopeTest { return null; }); - assertThrows(TimeoutException.class, scope::join); + var e = assertThrows(ExecutionException.class, scope::join); + assertInstanceOf(CancelledByTimeoutException.class, e.getCause()); expectDuration(startMillis, /*min*/1900, /*max*/20_000); assertTrue(scope.isCancelled()); @@ -641,7 +635,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testJoinWithTimeout3(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), + try (var scope = StructuredTaskScope.open( cf -> cf.withThreadFactory(factory) .withTimeout(Duration.ofSeconds(-1)))) { @@ -650,7 +644,8 @@ class StructuredTaskScopeTest { return null; }); - assertThrows(TimeoutException.class, scope::join); + var e = assertThrows(ExecutionException.class, scope::join); + assertInstanceOf(CancelledByTimeoutException.class, e.getCause()); assertTrue(scope.isCancelled()); assertEquals(Subtask.State.UNAVAILABLE, subtask.state()); @@ -701,7 +696,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testTimeoutInterruptsThreads(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), + try (var scope = StructuredTaskScope.open( cf -> cf.withThreadFactory(factory) .withTimeout(Duration.ofSeconds(2)))) { @@ -725,7 +720,8 @@ class StructuredTaskScopeTest { interrupted.await(); } - assertThrows(TimeoutException.class, scope::join); + var e = assertThrows(ExecutionException.class, scope::join); + assertInstanceOf(CancelledByTimeoutException.class, e.getCause()); assertEquals(Subtask.State.UNAVAILABLE, subtask.state()); } @@ -736,7 +732,7 @@ class StructuredTaskScopeTest { */ @Test void testCloseWithoutJoin1() { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { // do nothing } } @@ -747,8 +743,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testCloseWithoutJoin2(ThreadFactory factory) { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask = scope.fork(() -> { Thread.sleep(Duration.ofDays(1)); return null; @@ -775,8 +770,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testCloseAfterJoinThrows(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { var subtask = scope.fork(() -> { Thread.sleep(Duration.ofDays(1)); return null; @@ -796,8 +790,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testCloseConfined(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { // random thread cannot close scope try (var pool = Executors.newCachedThreadPool(factory)) { @@ -914,8 +907,8 @@ class StructuredTaskScopeTest { */ @Test void testCloseThrowsStructureViolation() throws Exception { - try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll())) { - try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope1 = StructuredTaskScope.open()) { + try (var scope2 = StructuredTaskScope.open()) { // close enclosing scope try { @@ -939,7 +932,7 @@ class StructuredTaskScopeTest { */ @Test void testIsCancelledAfterClose() throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { assertFalse(scope.isCancelled()); scope.close(); assertTrue(scope.isCancelled()); @@ -951,7 +944,7 @@ class StructuredTaskScopeTest { */ @Test void testOnForkThrows() throws Exception { - var joiner = new Joiner() { + var joiner = new Joiner() { @Override public boolean onFork(Subtask subtask) { throw new FooException(); @@ -960,6 +953,10 @@ class StructuredTaskScopeTest { public Void result() { return null; } + @Override + public Void timeout() { + return null; + } }; try (var scope = StructuredTaskScope.open(joiner)) { assertThrows(FooException.class, () -> scope.fork(() -> "foo")); @@ -971,7 +968,7 @@ class StructuredTaskScopeTest { */ @Test void testOnForkCancelsExecution() throws Exception { - var joiner = new Joiner() { + var joiner = new Joiner() { @Override public boolean onFork(Subtask subtask) { return true; @@ -980,6 +977,10 @@ class StructuredTaskScopeTest { public Void result() { return null; } + @Override + public Void timeout() { + return null; + } }; try (var scope = StructuredTaskScope.open(joiner)) { assertFalse(scope.isCancelled()); @@ -994,7 +995,7 @@ class StructuredTaskScopeTest { */ @Test void testOnCompleteThrows() throws Exception { - var joiner = new Joiner() { + var joiner = new Joiner() { @Override public boolean onComplete(Subtask subtask) { throw new FooException(); @@ -1003,6 +1004,10 @@ class StructuredTaskScopeTest { public Void result() { return null; } + @Override + public Void timeout() { + return null; + } }; var excRef = new AtomicReference(); Thread.UncaughtExceptionHandler uhe = (t, e) -> excRef.set(e); @@ -1021,7 +1026,7 @@ class StructuredTaskScopeTest { */ @Test void testOnCompleteCancelsExecution() throws Exception { - var joiner = new Joiner() { + var joiner = new Joiner() { @Override public boolean onComplete(Subtask subtask) { return true; @@ -1030,6 +1035,10 @@ class StructuredTaskScopeTest { public Void result() { return null; } + @Override + public Void timeout() { + return null; + } }; try (var scope = StructuredTaskScope.open(joiner)) { assertFalse(scope.isCancelled()); @@ -1040,22 +1049,24 @@ class StructuredTaskScopeTest { } /** - * Test Joiner.onTimeout invoked by owner thread when timeout expires. + * Test Joiner.timeout() invoked by owner thread when timeout expires. */ @Test - void testOnTimeoutInvoked() throws Exception { - var scopeRef = new AtomicReference>(); + void testTimeoutInvoked() throws Exception { + var scopeRef = new AtomicReference>(); Thread owner = Thread.currentThread(); var invokeCount = new AtomicInteger(); - var joiner = new Joiner() { - @Override - public void onTimeout() { - assertTrue(Thread.currentThread() == owner); - assertTrue(scopeRef.get().isCancelled()); - invokeCount.incrementAndGet(); - } + var joiner = new Joiner() { @Override public Void result() { + fail("Should not be called"); + return null; + } + @Override + public Void timeout() { + assertSame(owner, Thread.currentThread()); + assertTrue(scopeRef.get().isCancelled()); + invokeCount.incrementAndGet(); return null; } }; @@ -1072,29 +1083,28 @@ class StructuredTaskScopeTest { } /** - * Test Joiner.onTimeout throwing an excepiton. + * Test Joiner.timeout() throwing an excepiton. */ @Test - void testOnTimeoutThrows() throws Exception { - var joiner = new Joiner() { - @Override - public void onTimeout() { - throw new FooException(); - } + void testTimeoutThrows() throws Exception { + var joiner = new Joiner() { @Override public Void result() { return null; } + @Override + public Void timeout() { + throw new FooException(); + } }; - try (var scope = StructuredTaskScope.open(joiner, - cf -> cf.withTimeout(Duration.ofMillis(100)))) { + try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withTimeout(Duration.ofMillis(100)))) { // wait for scope to be cancelled by timeout awaitCancelled(scope); // join should throw FooException on first usage assertThrows(FooException.class, scope::join); - // retry after onTimeout fails + // retry after join throws assertThrows(IllegalStateException.class, scope::join); } } @@ -1104,8 +1114,7 @@ class StructuredTaskScopeTest { */ @Test void testToString() throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withName("duke"))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withName("duke"))) { // open assertTrue(scope.toString().contains("duke")); @@ -1122,8 +1131,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testSubtaskWhenSuccess(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask = scope.fork(() -> "foo"); // before join, owner thread @@ -1154,8 +1162,8 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testSubtaskWhenFailed(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(Joiner.allUntil(s -> false), + cf -> cf.withThreadFactory(factory))) { Subtask subtask = scope.fork(() -> { throw new FooException(); }); @@ -1187,8 +1195,7 @@ class StructuredTaskScopeTest { @ParameterizedTest @MethodSource("factories") void testSubtaskWhenNotCompleted(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask = scope.fork(() -> { Thread.sleep(Duration.ofDays(1)); return null; @@ -1258,7 +1265,7 @@ class StructuredTaskScopeTest { */ @Test void testSubtaskToString() throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open(Joiner.allUntil(s -> false))) { var latch = new CountDownLatch(1); var subtask1 = scope.fork(() -> { latch.await(); @@ -1316,7 +1323,7 @@ class StructuredTaskScopeTest { scope.fork(() -> { throw new FooException(); }); try { scope.join(); - } catch (FailedException e) { + } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FooException); } } @@ -1334,9 +1341,10 @@ class StructuredTaskScopeTest { Thread.sleep(Duration.ofDays(1)); return "bar"; }); - assertThrows(TimeoutException.class, scope::join); + var e = assertThrows(ExecutionException.class, scope::join); + assertInstanceOf(CancelledByTimeoutException.class, e.getCause()); - // retry after join throws TimeoutException + // retry after join throws CancelledByTimeoutException assertThrows(IllegalStateException.class, scope::join); } } @@ -1363,6 +1371,34 @@ class StructuredTaskScopeTest { } } + /** + * Test Joiner.allSuccessfulOrThrow() with an exception supplying function. + */ + @Test + void testAllSuccessfulOrThrow6() throws Exception { + // return exception with subtask exception as cause + try (var scope = StructuredTaskScope.open( + Joiner.allSuccessfulOrThrow(CompletionException::new))) { + scope.fork(() -> { throw new FooException(); }); + var e = assertThrows(CompletionException.class, scope::join); + assertInstanceOf(FooException.class, e.getCause()); + } + + // return the subtask exception + try (var scope = StructuredTaskScope.open( + Joiner.allSuccessfulOrThrow(Function.identity()))) { + scope.fork(() -> { throw new FooException(); }); + assertThrows(FooException.class, scope::join); + } + + // return null + try (var scope = StructuredTaskScope.open( + Joiner.allSuccessfulOrThrow(e -> null))) { + scope.fork(() -> { throw new FooException(); }); + assertThrows(NullPointerException.class, scope::join); + } + } + /** * Test Joiner.anySuccessfulOrThrow() with no subtasks. */ @@ -1371,7 +1407,7 @@ class StructuredTaskScopeTest { try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow())) { try { scope.join(); - } catch (FailedException e) { + } catch (ExecutionException e) { assertTrue(e.getCause() instanceof NoSuchElementException); } } @@ -1406,41 +1442,44 @@ class StructuredTaskScopeTest { } } + /** + * Test Joiner.anySuccessfulOrThrow() where all subtasks fail. + */ + @ParameterizedTest + @MethodSource("factories") + void testAnySuccessfulOrThrow4(ThreadFactory factory) throws Exception { + try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow(), + cf -> cf.withThreadFactory(factory))) { + scope.fork(() -> { throw new FooException(); }); + Throwable ex = assertThrows(ExecutionException.class, scope::join); + assertTrue(ex.getCause() instanceof FooException); + } + } + /** * Test Joiner.anySuccessfulOrThrow() with a subtask that complete succcessfully * and a subtask that fails. */ @ParameterizedTest @MethodSource("factories") - void testAnySuccessfulOrThrow4(ThreadFactory factory) throws Exception { + void testAnySuccessfulOrThrow5(ThreadFactory factory) throws Exception { try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow(), cf -> cf.withThreadFactory(factory))) { - scope.fork(() -> "foo"); + scope.fork(() -> { + Thread.sleep(100); + return "foo"; + }); scope.fork(() -> { throw new FooException(); }); String first = scope.join(); assertEquals("foo", first); } } - /** - * Test Joiner.anySuccessfulOrThrow() with a subtask that fails. - */ - @ParameterizedTest - @MethodSource("factories") - void testAnySuccessfulOrThrow5(ThreadFactory factory) throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow(), - cf -> cf.withThreadFactory(factory))) { - scope.fork(() -> { throw new FooException(); }); - Throwable ex = assertThrows(FailedException.class, scope::join); - assertTrue(ex.getCause() instanceof FooException); - } - } - /** * Test Joiner.anySuccessfulOrThrow() with a timeout. */ @Test - void anySuccessfulOrThrow6() throws Exception { + void testAnySuccessfulOrThrow6() throws Exception { try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow(), cf -> cf.withTimeout(Duration.ofMillis(100)))) { scope.fork(() -> { throw new FooException(); }); @@ -1448,13 +1487,42 @@ class StructuredTaskScopeTest { Thread.sleep(Duration.ofDays(1)); return "bar"; }); - assertThrows(TimeoutException.class, scope::join); + var e = assertThrows(ExecutionException.class, scope::join); + assertInstanceOf(CancelledByTimeoutException.class, e.getCause()); - // retry after join throws TimeoutException + // retry after join throws CancelledByTimeoutException assertThrows(IllegalStateException.class, scope::join); } } + /** + * Test Joiner.anySuccessfulOrThrow() with an exception supplying function. + */ + @Test + void testAnySuccessfulOrThrow7() throws Exception { + // return exception with subtask exception as cause + try (var scope = StructuredTaskScope.open( + Joiner.anySuccessfulOrThrow(CompletionException::new))) { + scope.fork(() -> { throw new FooException(); }); + var e = assertThrows(CompletionException.class, scope::join); + assertInstanceOf(FooException.class, e.getCause()); + } + + // return the subtask exception + try (var scope = StructuredTaskScope.open( + Joiner.anySuccessfulOrThrow(Function.identity()))) { + scope.fork(() -> { throw new FooException(); }); + assertThrows(FooException.class, scope::join); + } + + // return null + try (var scope = StructuredTaskScope.open( + Joiner.anySuccessfulOrThrow(e -> null))) { + scope.fork(() -> { throw new FooException(); }); + assertThrows(NullPointerException.class, scope::join); + } + } + /** * Test Joiner.awaitAllSuccessfulOrThrow() with no subtasks. */ @@ -1496,7 +1564,7 @@ class StructuredTaskScopeTest { scope.fork(() -> { throw new FooException(); }); try { scope.join(); - } catch (FailedException e) { + } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FooException); } } @@ -1514,75 +1582,39 @@ class StructuredTaskScopeTest { Thread.sleep(Duration.ofDays(1)); return "bar"; }); - assertThrows(TimeoutException.class, scope::join); + var e = assertThrows(ExecutionException.class, scope::join); + assertInstanceOf(CancelledByTimeoutException.class, e.getCause()); - // retry after join throws TimeoutException + // retry after join throws CancelledByTimeoutException assertThrows(IllegalStateException.class, scope::join); } } /** - * Test Joiner.awaitAll() with no subtasks. + * Test Joiner.awaitAllSuccessfulOrThrow() with an exception supplying function. */ @Test - void testAwaitAll1() throws Throwable { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { - var result = scope.join(); - assertNull(result); + void awaitAllSuccessfulOrThrow5() throws Exception { + // return exception with subtask exception as cause + try (var scope = StructuredTaskScope.open( + Joiner.awaitAllSuccessfulOrThrow(CompletionException::new))) { + scope.fork(() -> { throw new FooException(); }); + var e = assertThrows(CompletionException.class, scope::join); + assertInstanceOf(FooException.class, e.getCause()); } - } - /** - * Test Joiner.awaitAll() with subtasks that complete successfully. - */ - @ParameterizedTest - @MethodSource("factories") - void testAwaitAll2(ThreadFactory factory) throws Throwable { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { - var subtask1 = scope.fork(() -> "foo"); - var subtask2 = scope.fork(() -> "bar"); - var result = scope.join(); - assertNull(result); - assertEquals("foo", subtask1.get()); - assertEquals("bar", subtask2.get()); + // return the subtask exception + try (var scope = StructuredTaskScope.open( + Joiner.awaitAllSuccessfulOrThrow(Function.identity()))) { + scope.fork(() -> { throw new FooException(); }); + assertThrows(FooException.class, scope::join); } - } - /** - * Test Joiner.awaitAll() with a subtask that complete successfully and a subtask - * that fails. - */ - @ParameterizedTest - @MethodSource("factories") - void testAwaitAll3(ThreadFactory factory) throws Throwable { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { - var subtask1 = scope.fork(() -> "foo"); - var subtask2 = scope.fork(() -> { throw new FooException(); }); - var result = scope.join(); - assertNull(result); - assertEquals("foo", subtask1.get()); - assertTrue(subtask2.exception() instanceof FooException); - } - } - - /** - * Test Joiner.awaitAll() with a timeout. - */ - @Test - void testAwaitAll4() throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withTimeout(Duration.ofMillis(100)))) { - scope.fork(() -> "foo"); - scope.fork(() -> { - Thread.sleep(Duration.ofDays(1)); - return "bar"; - }); - assertThrows(TimeoutException.class, scope::join); - - // retry after join throws TimeoutException - assertThrows(IllegalStateException.class, scope::join); + // return null + try (var scope = StructuredTaskScope.open( + Joiner.awaitAllSuccessfulOrThrow(e -> null))) { + scope.fork(() -> { throw new FooException(); }); + assertThrows(NullPointerException.class, scope::join); } } @@ -1648,16 +1680,15 @@ class StructuredTaskScopeTest { void testAllUntil4(ThreadFactory factory) throws Exception { // cancel execution after two or more failures - class CancelAfterTwoFailures implements Predicate> { + class CancelAfterTwoFailures implements Predicate> { final AtomicInteger failedCount = new AtomicInteger(); @Override - public boolean test(Subtask subtask) { + public boolean test(Subtask subtask) { return subtask.state() == Subtask.State.FAILED && failedCount.incrementAndGet() >= 2; } } - var joiner = Joiner.allUntil(new CancelAfterTwoFailures()); - + var joiner = Joiner.allUntil(new CancelAfterTwoFailures()); try (var scope = StructuredTaskScope.open(joiner)) { int forkCount = 0; @@ -1680,7 +1711,7 @@ class StructuredTaskScopeTest { } /** - * Test Test Joiner.allUntil(Predicate) where the Predicate's test method throws. + * Test Joiner.allUntil(Predicate) where the Predicate's test method throws. */ @Test void testAllUntil5() throws Exception { @@ -1710,14 +1741,14 @@ class StructuredTaskScopeTest { return "bar"; }); - // TimeoutException should not be thrown + // join should not throw var subtasks = scope.join(); // stream should have two elements, subtask1 may or may not have completed assertEquals(List.of(subtask1, subtask2), subtasks); assertEquals(Subtask.State.UNAVAILABLE, subtask2.state()); - // retry after join throws TimeoutException + // retry after join completes assertThrows(IllegalStateException.class, scope::join); } } @@ -1767,14 +1798,16 @@ class StructuredTaskScopeTest { assertEquals(Subtask.State.UNAVAILABLE, subtask2.state()); // Joiner that does not override default methods - Joiner joiner = () -> null; + var joiner = new Joiner() { + @Override public Void result() { return null; } + @Override public Void timeout() { return null; } + }; assertThrows(NullPointerException.class, () -> joiner.onFork(null)); assertThrows(NullPointerException.class, () -> joiner.onComplete(null)); assertThrows(IllegalArgumentException.class, () -> joiner.onFork(subtask1)); assertFalse(joiner.onFork(subtask2)); assertFalse(joiner.onComplete(subtask1)); assertThrows(IllegalArgumentException.class, () -> joiner.onComplete(subtask2)); - assertThrows(TimeoutException.class, joiner::onTimeout); } } @@ -1798,8 +1831,6 @@ class StructuredTaskScopeTest { () -> Joiner.anySuccessfulOrThrow().onComplete(subtask)); assertThrows(IllegalArgumentException.class, () -> Joiner.awaitAllSuccessfulOrThrow().onComplete(subtask)); - assertThrows(IllegalArgumentException.class, - () -> Joiner.awaitAll().onComplete(subtask)); assertThrows(IllegalArgumentException.class, () -> Joiner.allUntil(_ -> false).onComplete(subtask)); @@ -1814,8 +1845,6 @@ class StructuredTaskScopeTest { () -> Joiner.anySuccessfulOrThrow().onFork(subtask)); assertThrows(IllegalArgumentException.class, () -> Joiner.awaitAllSuccessfulOrThrow().onFork(subtask)); - assertThrows(IllegalArgumentException.class, - () -> Joiner.awaitAll().onFork(subtask)); assertThrows(IllegalArgumentException.class, () -> Joiner.allUntil(_ -> false).onFork(subtask)); } @@ -1828,8 +1857,7 @@ class StructuredTaskScopeTest { @Test void testConfigFunctionThrows() throws Exception { assertThrows(FooException.class, - () -> StructuredTaskScope.open(Joiner.awaitAll(), - cf -> { throw new FooException(); })); + () -> StructuredTaskScope.open(cf -> { throw new FooException(); })); } /** @@ -1862,7 +1890,7 @@ class StructuredTaskScopeTest { return cf; }; - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), configOperator)) { + try (var scope = StructuredTaskScope.open(configOperator)) { // do nothing } } @@ -1873,47 +1901,63 @@ class StructuredTaskScopeTest { @Test void testNulls() throws Exception { assertThrows(NullPointerException.class, - () -> StructuredTaskScope.open(null)); + () -> StructuredTaskScope.open((Joiner) null)); + assertThrows(NullPointerException.class, + () -> StructuredTaskScope.open((UnaryOperator) null)); assertThrows(NullPointerException.class, () -> StructuredTaskScope.open(null, cf -> cf)); assertThrows(NullPointerException.class, - () -> StructuredTaskScope.open(Joiner.awaitAll(), null)); - - assertThrows(NullPointerException.class, () -> Joiner.allUntil(null)); + () -> StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(), null)); // fork - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { assertThrows(NullPointerException.class, () -> scope.fork((Callable) null)); assertThrows(NullPointerException.class, () -> scope.fork((Runnable) null)); } - // Configuration and withXXX methods - assertThrows(NullPointerException.class, - () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> null)); - assertThrows(NullPointerException.class, - () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName(null))); - assertThrows(NullPointerException.class, - () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withThreadFactory(null))); - assertThrows(NullPointerException.class, - () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withTimeout(null))); - // Joiner.onFork/onComplete + Function esf = CompletionException::new; assertThrows(NullPointerException.class, - () -> Joiner.awaitAllSuccessfulOrThrow().onFork(null)); + () -> Joiner.allSuccessfulOrThrow(esf).onFork(null)); assertThrows(NullPointerException.class, - () -> Joiner.awaitAllSuccessfulOrThrow().onComplete(null)); - assertThrows(NullPointerException.class, - () -> Joiner.awaitAll().onFork(null)); - assertThrows(NullPointerException.class, - () -> Joiner.awaitAll().onComplete(null)); + () -> Joiner.allSuccessfulOrThrow(esf).onComplete(null)); assertThrows(NullPointerException.class, () -> Joiner.allSuccessfulOrThrow().onFork(null)); assertThrows(NullPointerException.class, () -> Joiner.allSuccessfulOrThrow().onComplete(null)); + assertThrows(NullPointerException.class, + () -> Joiner.anySuccessfulOrThrow(esf).onFork(null)); + assertThrows(NullPointerException.class, + () -> Joiner.anySuccessfulOrThrow(esf).onComplete(null)); assertThrows(NullPointerException.class, () -> Joiner.anySuccessfulOrThrow().onFork(null)); assertThrows(NullPointerException.class, () -> Joiner.anySuccessfulOrThrow().onComplete(null)); + assertThrows(NullPointerException.class, + () -> Joiner.awaitAllSuccessfulOrThrow(esf).onFork(null)); + assertThrows(NullPointerException.class, + () -> Joiner.awaitAllSuccessfulOrThrow(esf).onComplete(null)); + assertThrows(NullPointerException.class, + () -> Joiner.awaitAllSuccessfulOrThrow().onFork(null)); + assertThrows(NullPointerException.class, + () -> Joiner.awaitAllSuccessfulOrThrow().onComplete(null)); + assertThrows(NullPointerException.class, + () -> Joiner.allUntil(_ -> false).onFork(null)); + assertThrows(NullPointerException.class, + () -> Joiner.allUntil(_ -> false).onComplete(null)); + + // other Joiner methods + assertThrows(NullPointerException.class, () -> Joiner.allUntil(null)); + + // Configuration and withXXX methods + assertThrows(NullPointerException.class, + () -> StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(), cf -> null)); + assertThrows(NullPointerException.class, + () -> StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(), cf -> cf.withName(null))); + assertThrows(NullPointerException.class, + () -> StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(), cf -> cf.withThreadFactory(null))); + assertThrows(NullPointerException.class, + () -> StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(), cf -> cf.withTimeout(null))); } /** @@ -1939,7 +1983,7 @@ class StructuredTaskScopeTest { * A joiner that counts that counts the number of subtasks that are forked and the * number of subtasks that complete. */ - private static class CountingJoiner implements Joiner { + private static class CountingJoiner implements Joiner { final AtomicInteger onForkCount = new AtomicInteger(); final AtomicInteger onCompleteCount = new AtomicInteger(); @Override @@ -1956,6 +2000,10 @@ class StructuredTaskScopeTest { public Void result() { return null; } + @Override + public Void timeout() { + return null; + } int onForkCount() { return onForkCount.get(); } @@ -1968,7 +2016,7 @@ class StructuredTaskScopeTest { * A joiner that cancels execution when a subtask completes. It also keeps a count * of the number of subtasks that are forked and the number of subtasks that complete. */ - private static class CancelAfterOneJoiner implements Joiner { + private static class CancelAfterOneJoiner implements Joiner { final AtomicInteger onForkCount = new AtomicInteger(); final AtomicInteger onCompleteCount = new AtomicInteger(); @Override @@ -1985,6 +2033,10 @@ class StructuredTaskScopeTest { public Void result() { return null; } + @Override + public Void timeout() { + return null; + } int onForkCount() { return onForkCount.get(); } @@ -2028,7 +2080,7 @@ class StructuredTaskScopeTest { /** * Wait for the given scope to be cancelled. */ - private static void awaitCancelled(StructuredTaskScope scope) throws InterruptedException { + private static void awaitCancelled(StructuredTaskScope scope) throws InterruptedException { while (!scope.isCancelled()) { Thread.sleep(Duration.ofMillis(20)); } diff --git a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java index a2de88f329b..372d3e9a543 100644 --- a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java +++ b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,11 +27,10 @@ * @summary Test thread dumps with StructuredTaskScope * @enablePreview * @library /test/lib - * @run junit/othervm StructuredThreadDumpTest + * @run junit/othervm ${test.main.class} */ import java.util.concurrent.StructuredTaskScope; -import java.util.concurrent.StructuredTaskScope.Joiner; import java.io.IOException; import java.lang.management.ManagementFactory; import java.nio.file.Files; @@ -54,7 +53,7 @@ class StructuredThreadDumpTest { */ @Test void testTree() throws Exception { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName("scope"))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withName("scope"))) { Thread thread1 = fork(scope, "child-scope-A"); Thread thread2 = fork(scope, "child-scope-B"); try { @@ -68,15 +67,15 @@ class StructuredThreadDumpTest { // check parents assertFalse(rootContainer.parent().isPresent()); - assertTrue(container1.parent().get() == rootContainer); - assertTrue(container2.parent().get() == container1); - assertTrue(container3.parent().get() == container1); + assertSame(rootContainer, container1.parent().get()); + assertSame(container1, container2.parent().get()); + assertSame(container1, container3.parent().get()); // check owners assertFalse(rootContainer.owner().isPresent()); - assertTrue(container1.owner().getAsLong() == Thread.currentThread().threadId()); - assertTrue(container2.owner().getAsLong() == thread1.threadId()); - assertTrue(container3.owner().getAsLong() == thread2.threadId()); + assertEquals(Thread.currentThread().threadId(), container1.owner().getAsLong()); + assertEquals(thread1.threadId(), container2.owner().getAsLong()); + assertEquals(thread2.threadId(), container3.owner().getAsLong()); // thread1 and threads2 should be in threads array of "scope" container1.findThread(thread1.threadId()).orElseThrow(); @@ -96,10 +95,10 @@ class StructuredThreadDumpTest { */ @Test void testNested() throws Exception { - try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName("scope-A"))) { + try (var scope1 = StructuredTaskScope.open(cf -> cf.withName("scope-A"))) { Thread thread1 = fork(scope1); - try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName("scope-B"))) { + try (var scope2 = StructuredTaskScope.open(cf -> cf.withName("scope-B"))) { Thread thread2 = fork(scope2); try { ThreadDump threadDump = threadDump(); @@ -111,14 +110,14 @@ class StructuredThreadDumpTest { // check parents assertFalse(rootContainer.parent().isPresent()); - assertTrue(container1.parent().get() == rootContainer); - assertTrue(container2.parent().get() == container1); + assertSame(rootContainer, container1.parent().get()); + assertSame(container1, container2.parent().get()); // check owners long tid = Thread.currentThread().threadId(); assertFalse(rootContainer.owner().isPresent()); - assertTrue(container1.owner().getAsLong() == tid); - assertTrue(container2.owner().getAsLong() == tid); + assertEquals(tid, container1.owner().getAsLong()); + assertEquals(tid, container2.owner().getAsLong()); // thread1 should be in threads array of "scope-A" container1.findThread(thread1.threadId()).orElseThrow(); @@ -160,7 +159,7 @@ class StructuredThreadDumpTest { * Forks a subtask in the given scope that parks, returning the Thread that executes * the subtask. */ - private static Thread fork(StructuredTaskScope scope) throws Exception { + private static Thread fork(StructuredTaskScope scope) throws Exception { var ref = new AtomicReference(); scope.fork(() -> { ref.set(Thread.currentThread()); @@ -178,12 +177,11 @@ class StructuredThreadDumpTest { * Forks a subtask in the given scope. The subtask creates a new child scope with * the given name, then parks. This method returns Thread that executes the subtask. */ - private static Thread fork(StructuredTaskScope scope, + private static Thread fork(StructuredTaskScope scope, String childScopeName) throws Exception { var ref = new AtomicReference(); scope.fork(() -> { - try (var childScope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withName(childScopeName))) { + try (var childScope = StructuredTaskScope.open(cf -> cf.withName(childScopeName))) { ref.set(Thread.currentThread()); LockSupport.park(); } diff --git a/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java b/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java index 61783d3e34b..3c5a928cba2 100644 --- a/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java +++ b/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,12 +26,12 @@ * @bug 8284199 8296779 8306647 * @summary Basic tests for StructuredTaskScope with scoped values * @enablePreview - * @run junit WithScopedValue + * @run junit ${test.main.class} */ +import java.util.concurrent.ExecutionException; import java.util.concurrent.StructuredTaskScope; import java.util.concurrent.StructuredTaskScope.Subtask; -import java.util.concurrent.StructuredTaskScope.Joiner; import java.util.concurrent.StructureViolationException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicBoolean; @@ -56,8 +56,7 @@ class WithScopedValue { void testForkInheritsScopedValue1(ThreadFactory factory) throws Exception { ScopedValue name = ScopedValue.newInstance(); String value = ScopedValue.where(name, "x").call(() -> { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask = scope.fork(() -> { return name.get(); // child should read "x" }); @@ -76,11 +75,9 @@ class WithScopedValue { void testForkInheritsScopedValue2(ThreadFactory factory) throws Exception { ScopedValue name = ScopedValue.newInstance(); String value = ScopedValue.where(name, "x").call(() -> { - try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope1 = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask1 = scope1.fork(() -> { - try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope2 = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask2 = scope2.fork(() -> { return name.get(); // grandchild should read "x" }); @@ -103,15 +100,13 @@ class WithScopedValue { void testForkInheritsScopedValue3(ThreadFactory factory) throws Exception { ScopedValue name = ScopedValue.newInstance(); String value = ScopedValue.where(name, "x").call(() -> { - try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope1 = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask1 = scope1.fork(() -> { assertEquals(name.get(), "x"); // child should read "x" // rebind name to "y" String grandchildValue = ScopedValue.where(name, "y").call(() -> { - try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll(), - cf -> cf.withThreadFactory(factory))) { + try (var scope2 = StructuredTaskScope.open(cf -> cf.withThreadFactory(factory))) { Subtask subtask2 = scope2.fork(() -> { return name.get(); // grandchild should read "y" }); @@ -137,21 +132,21 @@ class WithScopedValue { void testStructureViolation1() throws Exception { ScopedValue name = ScopedValue.newInstance(); class Box { - StructuredTaskScope scope; + StructuredTaskScope scope; } var box = new Box(); try { try { ScopedValue.where(name, "x").run(() -> { - box.scope = StructuredTaskScope.open(Joiner.awaitAll()); + box.scope = StructuredTaskScope.open(); }); fail(); } catch (StructureViolationException expected) { } // underlying flock should be closed and fork should fail to start a thread - StructuredTaskScope scope = box.scope; + StructuredTaskScope scope = box.scope; AtomicBoolean ran = new AtomicBoolean(); - Subtask subtask = scope.fork(() -> { + Subtask subtask = scope.fork(() -> { ran.set(true); return null; }); @@ -159,7 +154,7 @@ class WithScopedValue { assertEquals(Subtask.State.UNAVAILABLE, subtask.state()); assertFalse(ran.get()); } finally { - StructuredTaskScope scope = box.scope; + StructuredTaskScope scope = box.scope; if (scope != null) { scope.close(); } @@ -172,7 +167,7 @@ class WithScopedValue { @Test void testStructureViolation2() throws Exception { ScopedValue name = ScopedValue.newInstance(); - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { ScopedValue.where(name, "x").run(() -> { assertThrows(StructureViolationException.class, scope::close); }); @@ -185,7 +180,7 @@ class WithScopedValue { @Test void testStructureViolation3() throws Exception { ScopedValue name = ScopedValue.newInstance(); - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { ScopedValue.where(name, "x").run(() -> { assertThrows(StructureViolationException.class, () -> scope.fork(() -> "foo")); @@ -203,7 +198,7 @@ class WithScopedValue { // rebind ScopedValue.where(name1, "x").run(() -> { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { ScopedValue.where(name1, "y").run(() -> { assertThrows(StructureViolationException.class, () -> scope.fork(() -> "foo")); @@ -213,7 +208,7 @@ class WithScopedValue { // new binding ScopedValue.where(name1, "x").run(() -> { - try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + try (var scope = StructuredTaskScope.open()) { ScopedValue.where(name2, "y").run(() -> { assertThrows(StructureViolationException.class, () -> scope.fork(() -> "foo")); diff --git a/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java b/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java index 5f6533777cf..bfd7ca71577 100644 --- a/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java +++ b/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,7 @@ * @enablePreview * @summary Implement Subject.current and Subject.callAs using scoped values. * Need enablePreview to use StructuredTaskScope. - * @run main/othervm CallAsWithScopedValue true + * @run main/othervm ${test.main.class} true */ import com.sun.security.auth.UserPrincipal; @@ -65,8 +65,7 @@ public class CallAsWithScopedValue { // Observable in structured concurrency in SV mode, but not in ACC mode Subject.callAs(subject, () -> { - var joiner = StructuredTaskScope.Joiner.awaitAll(); - try (var scope = StructuredTaskScope.open(joiner)) { + try (var scope = StructuredTaskScope.open()) { scope.fork(() -> check(3, Subject.current(), usv ? "Duke" : null)); scope.join(); }