diff --git a/README.md b/README.md index 5d28a25..d9acec9 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ These modules keep only sources, a `run.sh` and their transcripts (`output/`). T | [`http3/`](http3) | HTTP/3 in the Java HTTP Client (JEP 517): Migrating from HttpClient 11 | Java 11 code unchanged on 25 and 26, the `HTTP_3` opt-in on client vs request, the three `H3_DISCOVERY` modes, fallback timings (including the 60 s `HTTP_3_URI_ONLY` failure), proxy downgrade, HTTP/1.1 vs 2 vs 3 under iptables packet loss, with a local Hypercorn + aioquic server | | [`compact-strings/`](compact-strings) | Compact Strings in Java: Measured with JOL on JDK 25 (plus Compact Object Headers) | the `String` object and its `byte[]` field by field, Latin-1 vs UTF-16 footprint by length, one million strings with and without `-XX:-CompactStrings`, what flips a string to UTF-16 (and what flips it back), compact object headers on JDK 25 (opt-in) vs 27 (default), a JMH comparison of six operations | | [`pem-api/`](pem-api) | PEM Encodings in Java (JEP 538/542): Read and Write Keys and Certificates Without BouncyCastle | `PEMDecoder`/`PEMEncoder` round trips, JWT signing (RS256/ES256/EdDSA) and a TLS server built from PEM files, encrypted PKCS#8 keys, the failure catalogue (wrong target class, SEC1 `EC PRIVATE KEY`, CRLF output), BouncyCastle and Spring Security `RsaKeyConverters` side by side, preview flag on JDK 27 vs the JDK 28 EA build | +| [`structured-concurrency-final/`](structured-concurrency-final) | Structured Concurrency Is Final in JDK 28 (JEP 543): From Preview to Production | the thread leak of `ExecutorService` and its fix, every built-in `Joiner`, structure violations, timeouts, a custom joiner, JSON thread dumps, scoped values, `javap` diffs of the API on JDK 25, 26, 27 and 28-ea, the compiler output for six pieces of 25/26-era code, and the preview class-file trap between 27 and 28 | ## Captured output (`docs/output/`) diff --git a/structured-concurrency-final/README.md b/structured-concurrency-final/README.md new file mode 100644 index 0000000..bb079bc --- /dev/null +++ b/structured-concurrency-final/README.md @@ -0,0 +1,47 @@ +# structured-concurrency-final — Structured Concurrency, final in JDK 28 (JEP 543) + +Companion code for the ankurm.com article **Structured Concurrency Is Final in JDK 28 (JEP 543): From Preview to Production** +(). The explanation lives in the article, in collapsible sections; this folder holds the runnable +sources and the transcripts they produced. + +```bash +JDK25=/path/to/jdk-25 JDK26=/path/to/jdk-26 JDK27=/path/to/jdk-27 JDK28=/path/to/jdk-28 ./run.sh # regenerates output/*.txt +``` + +## What was tested against + +| Thing | Build | Note | +|---|---|---| +| JDK 28 | Temurin `28-beta+21-ea` (28-ea+21, build of 2026-10-22) | **early access**; JEP 543 status "Integrated", GA proposed 2027-03-09. No flags needed | +| JDK 27 | Temurin 27.0.1.0.2+1 | GA 2026-09-15. Seventh preview (JEP 533): needs `--enable-preview` | +| JDK 26 | Temurin 26.0.2.1+1 | sixth preview (JEP 525), for the old-API compile results | +| JDK 25 | Temurin 25.0.5+8 | fifth preview (JEP 505), for the API diffs | + +`javap` prints identical `StructuredTaskScope` signatures on 27 and on 28-ea+21 (`output/09-api-diff.txt`). Nothing here was run on a GA JDK 28, because none exists yet. + +## Layout + +- `src/` — runnable demos, no flags on JDK 28: `Server` (the canonical program), `Unstructured` and `FailFast` (the thread leak and its fix), + `Joiners` (the built-in joiners), `Failures` (structure violations and failure handling), `Timeouts`, `CollectingJoiner` (a custom joiner), + `RandomTasks` (seeded), `StuckClose` (a subtask that ignores interrupts), `ThreadDump` (JSON thread dump with the scope hierarchy), + `ScopedValues`, `Snippets` and `QuickReference` (the short fragments quoted in the article). +- `broken/` — six sources that compile on JDK 26 and fail on 28: `MissingCatch`, `OldFailedException`, `OldTimeoutException`, `AwaitAll`, `OldJoiner`, `ShutdownOnFailure`. +- `output/` — one transcript per script step; `run.sh` regenerates all of them. + +| Transcript | What it shows | +|---|---| +| `01-server.txt` | the canonical program on 28 (no flags), on 27 (`--enable-preview`), and the 27 compile errors without the flag | +| `02-unstructured-vs-structured.txt` | `ExecutorService` leaks the sibling; `StructuredTaskScope` interrupts it | +| `03-joiners.txt` | `allSuccessfulOrThrow`, `anySuccessfulOrThrow`, `awaitAllSuccessfulOrThrow`, `allUntil`, the `Function` overloads | +| `04-failures.txt` | `get()` before `join()`, no `join()`, non-owner `fork`, out-of-order close, join twice, cause pattern matching | +| `05-timeouts-and-custom-joiner.txt` | scope timeouts, a partial-result `timeout()`, thread factory, a custom joiner | +| `06-thread-dump.txt`, `15-jcmd-thread-dump.txt` | the scope as a container in a JSON thread dump (in-process and via `jcmd`) | +| `07-scoped-values.txt` | subtasks inherit a `ScopedValue` binding, a plain thread does not | +| `08-api-28.txt`, `09-api-diff.txt` | `javap` of the API on 28, and the diffs 25 to 26 to 27 to 28 | +| `10-broken-old-api.txt` | the compiler's messages for 25/26-era code on 28 | +| `11-preview-traps.txt` | a class compiled with preview on 27 does not load on 28; `--release 27` on a 28 compiler | +| `12-snippets.txt` | the short fragments in the article compile and run | +| `13-random-tasks.txt`, `14-stuck-close.txt` | seeded fan-out with one slow task; a subtask that ignores interrupts | + +Everything is byte-stable between runs (timings are reduced to booleans). The JEP statements (what changed in each preview) come from the JEP texts on openjdk.org; +only the 25 to 28 API differences were checked against binaries. diff --git a/structured-concurrency-final/broken/AwaitAll.java b/structured-concurrency-final/broken/AwaitAll.java new file mode 100644 index 0000000..102ee1e --- /dev/null +++ b/structured-concurrency-final/broken/AwaitAll.java @@ -0,0 +1,12 @@ +import java.util.concurrent.StructuredTaskScope; +import java.util.concurrent.StructuredTaskScope.Joiner; + +/** Joiner.awaitAll() existed in 25 and 26 and was removed in 27. */ +public class AwaitAll { + public static void main(String[] args) throws Exception { + try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + scope.fork(() -> "x"); + scope.join(); + } + } +} diff --git a/structured-concurrency-final/broken/MissingCatch.java b/structured-concurrency-final/broken/MissingCatch.java new file mode 100644 index 0000000..1787fc5 --- /dev/null +++ b/structured-concurrency-final/broken/MissingCatch.java @@ -0,0 +1,13 @@ +import java.util.concurrent.StructuredTaskScope; + +/** Valid on JDK 25 and 26 (join() threw only InterruptedException). On 27 and 28 join() can throw ExecutionException. */ +public class MissingCatch { + static String handle() throws InterruptedException { + try (var scope = StructuredTaskScope.open()) { + var user = scope.fork(() -> "Alice"); + scope.join(); + return user.get(); + } + } + public static void main(String[] args) throws Exception { System.out.println(handle()); } +} diff --git a/structured-concurrency-final/broken/OldFailedException.java b/structured-concurrency-final/broken/OldFailedException.java new file mode 100644 index 0000000..dc981b1 --- /dev/null +++ b/structured-concurrency-final/broken/OldFailedException.java @@ -0,0 +1,13 @@ +import java.util.concurrent.StructuredTaskScope; + +/** The JDK 25/26 way to catch a failed scope. FailedException is gone in 27. */ +public class OldFailedException { + public static void main(String[] args) throws Exception { + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> { throw new IllegalStateException("boom"); }); + scope.join(); + } catch (StructuredTaskScope.FailedException e) { + System.out.println("cause: " + e.getCause()); + } + } +} diff --git a/structured-concurrency-final/broken/OldJoiner.java b/structured-concurrency-final/broken/OldJoiner.java new file mode 100644 index 0000000..97fb5b7 --- /dev/null +++ b/structured-concurrency-final/broken/OldJoiner.java @@ -0,0 +1,12 @@ +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Joiner; +import java.util.concurrent.StructuredTaskScope.Subtask; + +/** A custom Joiner as written for JDK 26: two type parameters and onTimeout(). */ +public class OldJoiner implements Joiner> { + private final java.util.Queue results = new ConcurrentLinkedQueue<>(); + public boolean onComplete(Subtask s) { if (s.state() == Subtask.State.SUCCESS) results.add(s.get()); return false; } + public void onTimeout() { } + public List result() { return List.copyOf(results); } +} diff --git a/structured-concurrency-final/broken/OldTimeoutException.java b/structured-concurrency-final/broken/OldTimeoutException.java new file mode 100644 index 0000000..1a57835 --- /dev/null +++ b/structured-concurrency-final/broken/OldTimeoutException.java @@ -0,0 +1,15 @@ +import java.time.Duration; +import java.util.concurrent.StructuredTaskScope; + +/** The JDK 25/26 timeout exception. In 27 the scope throws ExecutionException with a CancelledByTimeoutException cause. */ +public class OldTimeoutException { + public static void main(String[] args) throws Exception { + try (var scope = StructuredTaskScope.open(StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow(), + cf -> cf.withTimeout(Duration.ofMillis(50)))) { + scope.fork(() -> { Thread.sleep(1000); return "late"; }); + scope.join(); + } catch (StructuredTaskScope.TimeoutException e) { + System.out.println("timed out"); + } + } +} diff --git a/structured-concurrency-final/broken/ShutdownOnFailure.java b/structured-concurrency-final/broken/ShutdownOnFailure.java new file mode 100644 index 0000000..a5a666e --- /dev/null +++ b/structured-concurrency-final/broken/ShutdownOnFailure.java @@ -0,0 +1,12 @@ +import java.util.concurrent.StructuredTaskScope; + +/** The JDK 21-24 API that older tutorials still show. It is not in 25 or later. */ +public class ShutdownOnFailure { + public static void main(String[] args) throws Exception { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + var user = scope.fork(() -> "Alice"); + scope.join().throwIfFailed(); + System.out.println(user.get()); + } + } +} diff --git a/structured-concurrency-final/output/01-server.txt b/structured-concurrency-final/output/01-server.txt new file mode 100644 index 0000000..91f7739 --- /dev/null +++ b/structured-concurrency-final/output/01-server.txt @@ -0,0 +1,35 @@ +$ javac src/Server.java && java Server (28-beta+21-ea) +Response[user=Alice, order=42] +Runtime: 28-beta+21-ea + +$ javac --enable-preview --release 27 src/Server.java && java --enable-preview Server (27.0.1.0.2+1) +Response[user=Alice, order=42] +Runtime: 27.0.1.0.2+1 + +$ javac src/Server.java (27.0.1.0.2+1) +src/Server.java:2: error: StructuredTaskScope is a preview API and is disabled by default. +import java.util.concurrent.StructuredTaskScope; + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:3: error: StructuredTaskScope is a preview API and is disabled by default. +import java.util.concurrent.StructuredTaskScope.Subtask; + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:3: error: Subtask is a preview API and is disabled by default. +import java.util.concurrent.StructuredTaskScope.Subtask; + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:22: error: StructuredTaskScope is a preview API and is disabled by default. + try (var scope = StructuredTaskScope.open()) { // 1. open + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:23: error: Subtask is a preview API and is disabled by default. + Subtask user = scope.fork(this::findUser); // 2. fork + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:24: error: Subtask is a preview API and is disabled by default. + Subtask order = scope.fork(this::fetchOrder); // fork + ^ + (use --enable-preview to enable preview APIs) +6 errors +exit=1 diff --git a/structured-concurrency-final/output/02-unstructured-vs-structured.txt b/structured-concurrency-final/output/02-unstructured-vs-structured.txt new file mode 100644 index 0000000..ed169e6 --- /dev/null +++ b/structured-concurrency-final/output/02-unstructured-vs-structured.txt @@ -0,0 +1,10 @@ +$ javac src/Unstructured.java && java Unstructured (28-beta+21-ea) +handle() failed: java.lang.IllegalStateException: user service down +handle() has returned. Waiting 400 ms to see what the other thread does: + fetchOrder() finished, although nobody wants the answer any more + +$ javac src/FailFast.java && java FailFast (28-beta+21-ea) + fetchOrder() was interrupted +handle() failed: java.lang.IllegalStateException: user service down +handle() has returned. Waiting 400 ms to see what the other thread does: +(nothing more happened) diff --git a/structured-concurrency-final/output/03-joiners.txt b/structured-concurrency-final/output/03-joiners.txt new file mode 100644 index 0000000..7d2a190 --- /dev/null +++ b/structured-concurrency-final/output/03-joiners.txt @@ -0,0 +1,21 @@ +$ javac src/Joiners.java && java Joiners (28-beta+21-ea) +-- allSuccessfulOrThrow(): List in FORK order, not completion order + [slow, fast, medium] +-- allSuccessfulOrThrow(): one failure cancels the others, join() throws ExecutionException + ExecutionException, cause = java.lang.IllegalStateException: broken failed + interrupted: [slow] +-- anySuccessfulOrThrow(): the race; losers are interrupted + winner: B-50ms + interrupted: [A-300ms, C-300ms] +-- anySuccessfulOrThrow(): when every subtask fails + ExecutionException, cause = java.lang.IllegalStateException: one failed +-- awaitAllSuccessfulOrThrow(): returns null (Void), like the zero-argument open() + join() = null, a=a, b=b +-- allUntil(predicate): stop when two subtasks have completed + SUCCESS t1 + SUCCESS t2 + UNAVAILABLE + UNAVAILABLE + interrupted: [t3, t4] +-- the Function overload: join() throws OUR exception type + caught ServiceException: service call failed: orders failed diff --git a/structured-concurrency-final/output/04-failures.txt b/structured-concurrency-final/output/04-failures.txt new file mode 100644 index 0000000..1bd0a42 --- /dev/null +++ b/structured-concurrency-final/output/04-failures.txt @@ -0,0 +1,15 @@ +$ javac src/Failures.java && java Failures (28-beta+21-ea) +-- Subtask.get() before join() + java.lang.IllegalStateException: null +-- leaving the try block without join() + java.lang.IllegalStateException: Owner did not join after forking +-- fork() from a thread that does not own the scope + returned java.lang.WrongThreadException: Current thread not owner +-- closing scopes out of order (not try-with-resources) + java.util.concurrent.StructureViolationException: null +-- join() twice + java.lang.IllegalStateException: Already joined or scope is closed +-- the failure surfaces as ExecutionException; pattern-match the cause + I/O failed: disk gone +-- two subtasks fail: which cause do you get? + cause = first (the first to fail wins) diff --git a/structured-concurrency-final/output/05-timeouts-and-custom-joiner.txt b/structured-concurrency-final/output/05-timeouts-and-custom-joiner.txt new file mode 100644 index 0000000..a4755f1 --- /dev/null +++ b/structured-concurrency-final/output/05-timeouts-and-custom-joiner.txt @@ -0,0 +1,15 @@ +$ javac src/Timeouts.java && java Timeouts (28-beta+21-ea) +-- inside the deadline + [fast, fast] +-- deadline 200 ms, one subtask needs 5 s + ExecutionException, cause = java.util.concurrent.StructuredTaskScope$CancelledByTimeoutException + returned within well under a second (not 5 s): the slow subtask was interrupted +-- the same deadline with a joiner whose timeout() returns a partial result + join() = [fast] +-- a thread factory decides the subtask thread's name + [order-0, order-1] + +$ javac src/CollectingJoiner.java && java CollectingJoiner (28-beta+21-ea) +collected (sorted): [1, 3] +-- with a 100 ms timeout and a 5 s subtask + CompletionException, cause = java.util.concurrent.StructuredTaskScope$CancelledByTimeoutException diff --git a/structured-concurrency-final/output/06-thread-dump.txt b/structured-concurrency-final/output/06-thread-dump.txt new file mode 100644 index 0000000..0539208 --- /dev/null +++ b/structured-concurrency-final/output/06-thread-dump.txt @@ -0,0 +1,12 @@ +$ javac src/ThreadDump.java && java ThreadDump (28-beta+21-ea) +"container": "orders\/jdk.internal.misc.ThreadFlock$ThreadContainerImpl@", +"parent": "", +"owner": , +"tid": , +"virtual": true, +"state": "TIMED_WAITING", +"ThreadDump.lambda$main$6(ThreadDump.java:28)", +"tid": , +"virtual": true, +"state": "TIMED_WAITING", +"ThreadDump.lambda$main$6(ThreadDump.java:28)", diff --git a/structured-concurrency-final/output/07-scoped-values.txt b/structured-concurrency-final/output/07-scoped-values.txt new file mode 100644 index 0000000..71c210a --- /dev/null +++ b/structured-concurrency-final/output/07-scoped-values.txt @@ -0,0 +1,4 @@ +$ javac src/ScopedValues.java && java ScopedValues (28-beta+21-ea) +forked subtask sees req-7731 +plain thread: bound? false +outside the binding: bound? false diff --git a/structured-concurrency-final/output/08-api-28.txt b/structured-concurrency-final/output/08-api-28.txt new file mode 100644 index 0000000..32d7db5 --- /dev/null +++ b/structured-concurrency-final/output/08-api-28.txt @@ -0,0 +1,48 @@ +# javap java.util.concurrent.StructuredTaskScope and its nested types (28-beta+21-ea) +Compiled from "StructuredTaskScope.java" +public interface StructuredTaskScope extends AutoCloseable { + public static StructuredTaskScope open(StructuredTaskScope$Joiner, UnaryOperator); + public static StructuredTaskScope open(StructuredTaskScope$Joiner); + public static StructuredTaskScope open(UnaryOperator); + public static StructuredTaskScope open(); + public abstract StructuredTaskScope$Subtask fork(Callable); + public abstract StructuredTaskScope$Subtask fork(Runnable); + public abstract R join() throws R_X, InterruptedException; + public abstract boolean isCancelled(); + public abstract void close(); +} +Compiled from "StructuredTaskScope.java" +public interface StructuredTaskScope$Joiner { + public default boolean onFork(StructuredTaskScope$Subtask); + public default boolean onComplete(StructuredTaskScope$Subtask); + public abstract R result() throws R_X; + public abstract R timeout() throws R_X; + public static StructuredTaskScope$Joiner, R_X> allSuccessfulOrThrow(Function); + public static StructuredTaskScope$Joiner, ExecutionException> allSuccessfulOrThrow(); + public static StructuredTaskScope$Joiner anySuccessfulOrThrow(Function); + public static StructuredTaskScope$Joiner anySuccessfulOrThrow(); + public static StructuredTaskScope$Joiner awaitAllSuccessfulOrThrow(Function); + public static StructuredTaskScope$Joiner awaitAllSuccessfulOrThrow(); + public static StructuredTaskScope$Joiner>, RuntimeException> allUntil(Predicate>); +} +Compiled from "StructuredTaskScope.java" +public interface StructuredTaskScope$Configuration { + public abstract StructuredTaskScope$Configuration withThreadFactory(ThreadFactory); + public abstract StructuredTaskScope$Configuration withName(String); + public abstract StructuredTaskScope$Configuration withTimeout(java.time.Duration); +} +Compiled from "StructuredTaskScope.java" +public interface StructuredTaskScope$Subtask extends Supplier { + public abstract StructuredTaskScope$Subtask$State state(); + public abstract T get(); + public abstract Throwable exception(); +} +Compiled from "StructuredTaskScope.java" +public final class StructuredTaskScope$Subtask$State extends Enum { + public static final StructuredTaskScope$Subtask$State UNAVAILABLE; + public static final StructuredTaskScope$Subtask$State SUCCESS; + public static final StructuredTaskScope$Subtask$State FAILED; + public static StructuredTaskScope$Subtask$State[] values(); + public static StructuredTaskScope$Subtask$State valueOf(String); + static {}; +} diff --git a/structured-concurrency-final/output/09-api-diff.txt b/structured-concurrency-final/output/09-api-diff.txt new file mode 100644 index 0000000..8737363 --- /dev/null +++ b/structured-concurrency-final/output/09-api-diff.txt @@ -0,0 +1,74 @@ +$ diff api-25 api-26 (signature lines only; the first line of each file names the build) +< public static StructuredTaskScope open(StructuredTaskScope$Joiner, Function); +> public static StructuredTaskScope open(StructuredTaskScope$Joiner, UnaryOperator); +< public default boolean onFork(StructuredTaskScope$Subtask); +< public default boolean onComplete(StructuredTaskScope$Subtask); +> public default boolean onFork(StructuredTaskScope$Subtask); +> public default boolean onComplete(StructuredTaskScope$Subtask); +> public default void onTimeout(); +< public static StructuredTaskScope$Joiner>> allSuccessfulOrThrow(); +< public static StructuredTaskScope$Joiner anySuccessfulResultOrThrow(); +> public static StructuredTaskScope$Joiner> allSuccessfulOrThrow(); +> public static StructuredTaskScope$Joiner anySuccessfulOrThrow(); +< public static StructuredTaskScope$Joiner>> allUntil(Predicate>); +> public static StructuredTaskScope$Joiner>> allUntil(Predicate>); + +$ diff api-26 api-27 (signature lines only; the first line of each file names the build) +< public interface StructuredTaskScope extends AutoCloseable { +< public static StructuredTaskScope open(StructuredTaskScope$Joiner, UnaryOperator); +< public static StructuredTaskScope open(StructuredTaskScope$Joiner); +< public static StructuredTaskScope open(); +> public interface StructuredTaskScope extends AutoCloseable { +> public static StructuredTaskScope open(StructuredTaskScope$Joiner, UnaryOperator); +> public static StructuredTaskScope open(StructuredTaskScope$Joiner); +> public static StructuredTaskScope open(UnaryOperator); +> public static StructuredTaskScope open(); +< public abstract R join() throws InterruptedException; +> public abstract R join() throws R_X, InterruptedException; +< public interface StructuredTaskScope$Joiner { +> public interface StructuredTaskScope$Joiner { +< public default void onTimeout(); +< public abstract R result() throws Throwable; +< public static StructuredTaskScope$Joiner> allSuccessfulOrThrow(); +< public static StructuredTaskScope$Joiner anySuccessfulOrThrow(); +< public static StructuredTaskScope$Joiner awaitAllSuccessfulOrThrow(); +< public static StructuredTaskScope$Joiner awaitAll(); +< public static StructuredTaskScope$Joiner>> allUntil(Predicate>); +> public abstract R result() throws R_X; +> public abstract R timeout() throws R_X; +> public static StructuredTaskScope$Joiner, R_X> allSuccessfulOrThrow(Function); +> public static StructuredTaskScope$Joiner, ExecutionException> allSuccessfulOrThrow(); +> public static StructuredTaskScope$Joiner anySuccessfulOrThrow(Function); +> public static StructuredTaskScope$Joiner anySuccessfulOrThrow(); +> public static StructuredTaskScope$Joiner awaitAllSuccessfulOrThrow(Function); +> public static StructuredTaskScope$Joiner awaitAllSuccessfulOrThrow(); +> public static StructuredTaskScope$Joiner>, RuntimeException> allUntil(Predicate>); + +$ diff api-27 api-28 (signature lines only; the first line of each file names the build) +(no difference) + +$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' (25.0.5+8-LTS) +java/util/concurrent/StructuredTaskScope$FailedException.class +java/util/concurrent/StructuredTaskScope$TimeoutException.class + +$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' (26.0.2.1+1) +java/util/concurrent/StructuredTaskScope$FailedException.class +java/util/concurrent/StructuredTaskScope$TimeoutException.class + +$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' (27.0.1.0.2+1) +java/util/concurrent/StructuredTaskScope$CancelledByTimeoutException.class + +$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' (28-beta+21-ea) +java/util/concurrent/StructuredTaskScope$CancelledByTimeoutException.class + +$ javap -v StructuredTaskScope | grep 'PreviewFeature(' -A1 (26.0.2.1+1) + jdk.internal.javac.PreviewFeature( + feature=Ljdk/internal/javac/PreviewFeature$Feature;.STRUCTURED_CONCURRENCY + +$ javap -v StructuredTaskScope | grep 'PreviewFeature(' -A1 (27.0.1.0.2+1) + jdk.internal.javac.PreviewFeature( + feature=Ljdk/internal/javac/PreviewFeature$Feature;.STRUCTURED_CONCURRENCY + +$ javap -v StructuredTaskScope | grep 'PreviewFeature(' -A1 (28-beta+21-ea) +(no @PreviewFeature annotation: the API is final) + diff --git a/structured-concurrency-final/output/10-broken-old-api.txt b/structured-concurrency-final/output/10-broken-old-api.txt new file mode 100644 index 0000000..38bd678 --- /dev/null +++ b/structured-concurrency-final/output/10-broken-old-api.txt @@ -0,0 +1,83 @@ +$ javac --enable-preview --release 26 broken/MissingCatch.java (26.0.2.1+1) +exit=0 + +$ javac broken/MissingCatch.java (28-beta+21-ea) +broken/MissingCatch.java:8: error: unreported exception ExecutionException; must be caught or declared to be thrown + scope.join(); + ^ +1 error +exit=1 + +$ javac --enable-preview --release 26 broken/OldFailedException.java (26.0.2.1+1) +exit=0 + +$ javac broken/OldFailedException.java (28-beta+21-ea) +broken/OldFailedException.java:9: error: cannot find symbol + } catch (StructuredTaskScope.FailedException e) { + ^ + symbol: class FailedException + location: interface StructuredTaskScope +1 error +exit=1 + +$ javac --enable-preview --release 26 broken/OldTimeoutException.java (26.0.2.1+1) +exit=0 + +$ javac broken/OldTimeoutException.java (28-beta+21-ea) +broken/OldTimeoutException.java:11: error: cannot find symbol + } catch (StructuredTaskScope.TimeoutException e) { + ^ + symbol: class TimeoutException + location: interface StructuredTaskScope +1 error +exit=1 + +$ javac --enable-preview --release 26 broken/AwaitAll.java (26.0.2.1+1) +exit=0 + +$ javac broken/AwaitAll.java (28-beta+21-ea) +broken/AwaitAll.java:7: error: cannot find symbol + try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { + ^ + symbol: method awaitAll() + location: interface Joiner +1 error +exit=1 + +$ javac --enable-preview --release 26 broken/OldJoiner.java (26.0.2.1+1) +exit=0 + +$ javac broken/OldJoiner.java (28-beta+21-ea) +broken/OldJoiner.java:7: error: wrong number of type arguments; required 3 +public class OldJoiner implements Joiner> { + ^ +1 error +exit=1 + +$ javac --enable-preview --release 26 broken/ShutdownOnFailure.java (26.0.2.1+1) +broken/ShutdownOnFailure.java:6: error: cannot find symbol + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + ^ + symbol: class ShutdownOnFailure + location: interface StructuredTaskScope +1 error +exit=1 + +$ javac broken/ShutdownOnFailure.java (28-beta+21-ea) +broken/ShutdownOnFailure.java:6: error: cannot find symbol + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + ^ + symbol: class ShutdownOnFailure + location: interface StructuredTaskScope +1 error +exit=1 + +# the same sources on JDK 25 for ShutdownOnFailure +$ javac --enable-preview --release 25 broken/ShutdownOnFailure.java (25.0.5+8-LTS) +broken/ShutdownOnFailure.java:6: error: cannot find symbol + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + ^ + symbol: class ShutdownOnFailure + location: interface StructuredTaskScope +1 error +exit=1 diff --git a/structured-concurrency-final/output/11-preview-traps.txt b/structured-concurrency-final/output/11-preview-traps.txt new file mode 100644 index 0000000..f7257e2 --- /dev/null +++ b/structured-concurrency-final/output/11-preview-traps.txt @@ -0,0 +1,59 @@ +# compile on 27 with preview, then run the class file on 27 and on 28 +$ java --enable-preview Server (27.0.1.0.2+1) +Response[user=Alice, order=42] +Runtime: 27.0.1.0.2+1 +$ java --enable-preview Server (28-beta+21-ea) +Error: LinkageError occurred while loading main class Server + java.lang.UnsupportedClassVersionError: Server (class file version 71.65535) was compiled with preview features that are unsupported. This version of the Java Runtime only recognizes preview features for class file version 72.65535 +$ java Server (27.0.1.0.2+1) +Error: LinkageError occurred while loading main class Server + java.lang.UnsupportedClassVersionError: Preview features are not enabled for Server (class file version 71.65535). Try running with '--enable-preview' + +# the class file's minor version + minor version: 65535 + major version: 71 + +# javac 28 asked for --release 27 +$ javac --release 27 src/Server.java (28-beta+21-ea) +src/Server.java:2: error: StructuredTaskScope is a preview API and is disabled by default. +import java.util.concurrent.StructuredTaskScope; + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:3: error: StructuredTaskScope is a preview API and is disabled by default. +import java.util.concurrent.StructuredTaskScope.Subtask; + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:3: error: Subtask is a preview API and is disabled by default. +import java.util.concurrent.StructuredTaskScope.Subtask; + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:22: error: StructuredTaskScope is a preview API and is disabled by default. + try (var scope = StructuredTaskScope.open()) { // 1. open + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:23: error: Subtask is a preview API and is disabled by default. + Subtask user = scope.fork(this::findUser); // 2. fork + ^ + (use --enable-preview to enable preview APIs) +src/Server.java:24: error: Subtask is a preview API and is disabled by default. + Subtask order = scope.fork(this::fetchOrder); // fork + ^ + (use --enable-preview to enable preview APIs) +6 errors + +# javac 28 with --enable-preview --release 28 on the same file (nothing in it needs preview) +$ javac --enable-preview --release 28 src/Server.java (28-beta+21-ea) +Note: src/Server.java uses preview features of Java SE 28. +Note: Recompile with -Xlint:preview for details. +exit=0 + +# where that Note comes from: the first lines of -Xlint:preview +$ javac --enable-preview --release 28 -Xlint:preview src/Server.java 2>&1 | head -3 (28-beta+21-ea) +warning: [preview] class file for /@/java.base/java/lang/Record.sig uses preview features of Java SE 28. +warning: [preview] class file for /@/java.base/java/time/Duration.sig uses preview features of Java SE 28. +warning: [preview] class file for /@/java.base/java/lang/Integer.sig uses preview features of Java SE 28. + +# recompile on 28 without any flag and run on 28 +$ javac src/Server.java && java Server (28-beta+21-ea) +Response[user=Alice, order=42] +Runtime: 28-beta+21-ea diff --git a/structured-concurrency-final/output/12-snippets.txt b/structured-concurrency-final/output/12-snippets.txt new file mode 100644 index 0000000..2c4c134 --- /dev/null +++ b/structured-concurrency-final/output/12-snippets.txt @@ -0,0 +1,9 @@ +$ javac src/Snippets.java && java Snippets (28-beta+21-ea) +Response[user=Alice, order=42] +Response[user=Alice, order=42] +[1, 2] +a +Response[user=Alice, order=42] + +$ javac src/QuickReference.java && java QuickReference (28-beta+21-ea) +quick reference compiled and ran diff --git a/structured-concurrency-final/output/13-random-tasks.txt b/structured-concurrency-final/output/13-random-tasks.txt new file mode 100644 index 0000000..9e02ad8 --- /dev/null +++ b/structured-concurrency-final/output/13-random-tasks.txt @@ -0,0 +1,20 @@ +$ java RandomTasks 5 (28-beta+21-ea) +Duration: 487 +Duration: 92 +Duration: 474 +Duration: 424 +Duration: 506 +Result: 487 +Result: 92 +Result: 474 +Result: 424 +Result: 506 + +$ java RandomTasks 3 (28-beta+21-ea) +Duration: 734 +Duration: 660 +Duration: 210 +Duration: 581 +Duration: 128 +ExecutionException: TooSlowException: Duration 734 greater than threshold 700 + diff --git a/structured-concurrency-final/output/14-stuck-close.txt b/structured-concurrency-final/output/14-stuck-close.txt new file mode 100644 index 0000000..6c2a1e1 --- /dev/null +++ b/structured-concurrency-final/output/14-stuck-close.txt @@ -0,0 +1,3 @@ +$ javac src/StuckClose.java && java StuckClose (28-beta+21-ea) +subtask that responds to interrupts: caught fails at 50 ms; the try block took >= 1400 ms: false +subtask that ignores interrupts : caught fails at 50 ms; the try block took >= 1400 ms: true diff --git a/structured-concurrency-final/output/15-jcmd-thread-dump.txt b/structured-concurrency-final/output/15-jcmd-thread-dump.txt new file mode 100644 index 0000000..f17a73a --- /dev/null +++ b/structured-concurrency-final/output/15-jcmd-thread-dump.txt @@ -0,0 +1,23 @@ +$ jcmd Thread.dump_to_file -format=json threads.json (28-beta+21-ea) +: +Created /threads.json + +# the scope "orders" and its child scope in threads.json (stack frames, timestamps and ids trimmed or masked) +"container": "orders\/jdk.internal.misc.ThreadFlock$ThreadContainerImpl@", +"parent": "", +"owner": , +"tid": , +"virtual": true, +"state": "TIMED_WAITING", +"ThreadDump.lambda$main$1(ThreadDump.java:12)", +"tid": , +"virtual": true, +"state": "WAITING", +"ThreadDump.lambda$main$2(ThreadDump.java:16)", +"container": "inventory\/jdk.internal.misc.ThreadFlock$ThreadContainerImpl@", +"parent": "orders\/jdk.internal.misc.ThreadFlock$ThreadContainerImpl@", +"owner": , +"tid": , +"virtual": true, +"state": "TIMED_WAITING", +"ThreadDump.lambda$main$4(ThreadDump.java:15)", diff --git a/structured-concurrency-final/run.sh b/structured-concurrency-final/run.sh new file mode 100755 index 0000000..5cf768c --- /dev/null +++ b/structured-concurrency-final/run.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Structured concurrency, final in JDK 28 (JEP 543). Regenerates every file in output/. +# JDK25=/path JDK26=/path JDK27=/path JDK28=/path ./run.sh +# JDK 28 is an early-access build at the time of writing (GA proposed 2027-03-09); JDK 27 still treats the API as a preview. +set -uo pipefail +unset JAVA_TOOL_OPTIONS +HERE="$(cd "$(dirname "$0")" && pwd)" +JDK25="${JDK25:-/opt/jdks/jdk25}"; JDK26="${JDK26:-/opt/jdks/jdk26}"; JDK27="${JDK27:-/opt/jdks/jdk27}"; JDK28="${JDK28:-/opt/jdks/jdk28}" +OUT="$HERE/output"; mkdir -p "$OUT"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT +cd "$HERE" +rel() { case "$1" in "$JDK25") echo 25;; "$JDK26") echo 26;; "$JDK27") echo 27;; *) echo 28;; esac; } +t() { "$1/bin/java" -version 2>&1 | sed -n 2p | sed 's/.*(build \(.*\))/\1/'; } +mk() { mktemp -d -p "$B"; } +# plain compile + run (no flags) +cr() { local jdk="$1" src="$2" cls="$3" o; o="$(mk)" + echo "\$ javac $src && java $cls ($(t "$jdk"))" + "$jdk/bin/javac" --add-modules jdk.management -d "$o" "$src" 2>&1 && "$jdk/bin/java" --add-modules jdk.management -cp "$o" "$cls" 2>&1; } +# preview compile + run +crp() { local jdk="$1" src="$2" cls="$3" o r; o="$(mk)"; r="$(rel "$jdk")" + echo "\$ javac --enable-preview --release $r $src && java --enable-preview $cls ($(t "$jdk"))" + "$jdk/bin/javac" --enable-preview --release "$r" --add-modules jdk.management -d "$o" "$src" 2>&1 | grep -v '^Note:' + "$jdk/bin/java" --enable-preview --add-modules jdk.management -cp "$o" "$cls" 2>&1; } +# compile only, no flags (prints the compiler's own message and the exit status) +co() { local jdk="$1" src="$2" o; o="$(mk)" + echo "\$ javac $src ($(t "$jdk"))" + "$jdk/bin/javac" -d "$o" "$src" 2>&1 | grep -v '^Note:'; echo "exit=${PIPESTATUS[0]}"; } +# compile only, with --enable-preview for the JDK's own release +cop() { local jdk="$1" src="$2" o r; o="$(mk)"; r="$(rel "$jdk")" + echo "\$ javac --enable-preview --release $r $src ($(t "$jdk"))" + "$jdk/bin/javac" --enable-preview --release "$r" -d "$o" "$src" 2>&1 | grep -v '^Note:'; echo "exit=${PIPESTATUS[0]}"; } + +# 01 - the canonical program: final on 28 (no flags), preview on 27 (flags), and without the flag on 27 +{ cr "$JDK28" src/Server.java Server; echo; crp "$JDK27" src/Server.java Server; echo; co "$JDK27" src/Server.java; } > "$OUT/01-server.txt" + +# 02 - the failure that motivates the feature, then the same subtasks in a scope +{ cr "$JDK28" src/Unstructured.java Unstructured; echo; cr "$JDK28" src/FailFast.java FailFast; } > "$OUT/02-unstructured-vs-structured.txt" + +# 03 - the built-in joiners +{ cr "$JDK28" src/Joiners.java Joiners; } > "$OUT/03-joiners.txt" + +# 04 - structure violations and failure handling +{ cr "$JDK28" src/Failures.java Failures; } > "$OUT/04-failures.txt" + +# 05 - timeouts and a custom joiner +{ cr "$JDK28" src/Timeouts.java Timeouts; echo; cr "$JDK28" src/CollectingJoiner.java CollectingJoiner; } > "$OUT/05-timeouts-and-custom-joiner.txt" + +# 06 - the thread dump +{ cr "$JDK28" src/ThreadDump.java ThreadDump; } > "$OUT/06-thread-dump.txt" + +# 07 - scoped values +{ cr "$JDK28" src/ScopedValues.java ScopedValues; } > "$OUT/07-scoped-values.txt" + +# 08 - the API surface, release by release (javap of java.base) +api() { local jdk="$1"; echo "# javap java.util.concurrent.StructuredTaskScope and its nested types ($(t "$jdk"))" + for c in 'StructuredTaskScope' 'StructuredTaskScope$Joiner' 'StructuredTaskScope$Configuration' 'StructuredTaskScope$Subtask' 'StructuredTaskScope$Subtask$State'; do + "$jdk/bin/javap" --module java.base "java.util.concurrent.$c" 2>&1; done | + sed 's/java\.util\.concurrent\.//g;s/java\.lang\.//g;s/java\.util\.function\.//g;s/java\.util\.//g'; } +{ api "$JDK28"; } > "$OUT/08-api-28.txt" +for v in 25 26 27 28; do eval "j=\$JDK$v"; api "$j" > "$B/api$v.txt"; done +{ for p in "25 26" "26 27" "27 28"; do set -- $p + echo "\$ diff api-$1 api-$2 (signature lines only; the first line of each file names the build)" + if diff <(tail -n +2 "$B/api$1.txt") <(tail -n +2 "$B/api$2.txt") > "$B/d.txt"; then echo "(no difference)" + else grep -v '^[0-9,]*[acd][0-9,]*$' "$B/d.txt" | grep -v '^---$'; fi; echo; done + for v in 25 26 27 28; do eval "j=\$JDK$v"; echo "\$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' ($(t "$j"))" + "$j/bin/jimage" list "$j/lib/modules" 2>/dev/null | grep -E 'StructuredTaskScope\$(FailedException|TimeoutException|CancelledByTimeoutException)' | sed 's/^ *//'; echo; done + for v in 26 27 28; do eval "j=\$JDK$v"; echo "\$ javap -v StructuredTaskScope | grep 'PreviewFeature(' -A1 ($(t "$j"))" + "$j/bin/javap" -v --module java.base java.util.concurrent.StructuredTaskScope 2>&1 | grep -A1 'jdk.internal.javac.PreviewFeature(' | head -2 || true + "$j/bin/javap" -v --module java.base java.util.concurrent.StructuredTaskScope 2>&1 | grep -q 'PreviewFeature(' || echo "(no @PreviewFeature annotation: the API is final)"; echo; done +} > "$OUT/09-api-diff.txt" + +# 10 - code written for 25 or 26 against 27 and 28 (the compiler's own words) +{ for f in MissingCatch OldFailedException OldTimeoutException AwaitAll OldJoiner ShutdownOnFailure; do + for j in "$JDK26" "$JDK28"; do + if [ "$j" = "$JDK26" ]; then cop "$j" broken/$f.java; else co "$j" broken/$f.java; fi; echo; done; done + echo "# the same sources on JDK 25 for ShutdownOnFailure"; cop "$JDK25" broken/ShutdownOnFailure.java; } > "$OUT/10-broken-old-api.txt" + +# 11 - the preview-flag traps +{ O="$(mk)" + echo "# compile on 27 with preview, then run the class file on 27 and on 28" + "$JDK27/bin/javac" --enable-preview --release 27 -d "$O" src/Server.java 2>&1 | grep -v '^Note:' + echo "\$ java --enable-preview Server ($(t "$JDK27"))"; "$JDK27/bin/java" --enable-preview -cp "$O" Server 2>&1 + echo "\$ java --enable-preview Server ($(t "$JDK28"))"; "$JDK28/bin/java" --enable-preview -cp "$O" Server 2>&1 + echo "\$ java Server ($(t "$JDK27"))"; "$JDK27/bin/java" -cp "$O" Server 2>&1 + echo; echo "# the class file's minor version" + "$JDK28/bin/javap" -v -cp "$O" Server 2>&1 | grep -E 'minor version|major version' + echo; echo "# javac 28 asked for --release 27" + echo "\$ javac --release 27 src/Server.java ($(t "$JDK28"))"; "$JDK28/bin/javac" --release 27 -d "$(mk)" src/Server.java 2>&1 | grep -v '^Note:' + echo; echo "# javac 28 with --enable-preview --release 28 on the same file (nothing in it needs preview)" + echo "\$ javac --enable-preview --release 28 src/Server.java ($(t "$JDK28"))"; "$JDK28/bin/javac" --enable-preview --release 28 -d "$(mk)" src/Server.java 2>&1; echo "exit=$?" + echo; echo "# where that Note comes from: the first lines of -Xlint:preview" + echo "\$ javac --enable-preview --release 28 -Xlint:preview src/Server.java 2>&1 | head -3 ($(t "$JDK28"))" + "$JDK28/bin/javac" --enable-preview --release 28 -Xlint:preview -d "$(mk)" src/Server.java 2>&1 | head -3 + echo; echo "# recompile on 28 without any flag and run on 28" + cr "$JDK28" src/Server.java Server +} > "$OUT/11-preview-traps.txt" +# 12 - the short fragments quoted in the article compile and run on 28 +{ cr "$JDK28" src/Snippets.java Snippets; echo; cr "$JDK28" src/QuickReference.java QuickReference; } > "$OUT/12-snippets.txt" + +# 13 - five seeded random tasks: one run where all succeed, one where a task is too slow +{ o="$(mk)"; "$JDK28/bin/javac" -d "$o" src/RandomTasks.java 2>&1 + for seed in 5 3; do echo "\$ java RandomTasks $seed ($(t "$JDK28"))"; "$JDK28/bin/java" -cp "$o" RandomTasks $seed 2>&1; echo; done; } > "$OUT/13-random-tasks.txt" + +# 14 - a subtask that ignores interrupts keeps close() waiting +{ cr "$JDK28" src/StuckClose.java StuckClose; } > "$OUT/14-stuck-close.txt" + +# 15 - the thread dump taken from outside with jcmd, as the JEP describes it +{ o="$(mk)"; "$JDK28/bin/javac" --add-modules jdk.management -d "$o" src/ThreadDump.java 2>&1 + "$JDK28/bin/java" -cp "$o" ThreadDump hold > /dev/null 2>&1 & + pid=$!; sleep 2 + echo "\$ jcmd Thread.dump_to_file -format=json threads.json ($(t "$JDK28"))" + "$JDK28/bin/jcmd" "$pid" Thread.dump_to_file -format=json "$B/threads.json" 2>&1 | sed -E 's#'"$B"'##; s/^[0-9]+:$/:/' + kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null + echo; echo "# the scope \"orders\" and its child scope in threads.json (stack frames, timestamps and ids trimmed or masked)" + awk '/"container": "orders/{f=1} f' "$B/threads.json" | + sed -E 's/@[0-9a-f]+/@/; s/"(tid|owner)": [0-9]+/"\1": /' | + grep -E '"(container|parent|owner|tid|virtual|state)"|ThreadDump.lambda' | sed -E 's/^ +//' +} > "$OUT/15-jcmd-thread-dump.txt" +echo done diff --git a/structured-concurrency-final/src/CollectingJoiner.java b/structured-concurrency-final/src/CollectingJoiner.java new file mode 100644 index 0000000..e2071c6 --- /dev/null +++ b/structured-concurrency-final/src/CollectingJoiner.java @@ -0,0 +1,54 @@ +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Joiner; +import java.util.concurrent.StructuredTaskScope.Subtask; + +/** A custom completion policy: keep what succeeds, ignore what fails. The final Joiner has THREE type parameters. */ +public class CollectingJoiner implements Joiner, CompletionException> { + private final Queue results = new ConcurrentLinkedQueue<>(); + + @Override + public boolean onComplete(Subtask subtask) { + if (subtask.state() == Subtask.State.SUCCESS) { + results.add(subtask.get()); + } + return false; // never cancel early: let everyone finish + } + + @Override + public List result() { + return List.copyOf(results); + } + + @Override + public List timeout() { + // the scope was opened with a timeout and it expired + throw new CompletionException(new StructuredTaskScope.CancelledByTimeoutException()); + } + + static List allSuccessful(List> tasks) throws InterruptedException { + try (var scope = StructuredTaskScope.open(new CollectingJoiner())) { + tasks.forEach(scope::fork); + return scope.join(); + } + } + + public static void main(String[] args) throws Exception { + List> tasks = List.of( + () -> 1, + () -> { throw new IllegalStateException("ignored"); }, + () -> 3); + List r = new ArrayList<>(allSuccessful(tasks)); + Collections.sort(r); + System.out.println("collected (sorted): " + r); + + System.out.println("-- with a 100 ms timeout and a 5 s subtask"); + try (var scope = StructuredTaskScope.open(new CollectingJoiner(), + cf -> cf.withTimeout(java.time.Duration.ofMillis(100)))) { + scope.fork(() -> { Thread.sleep(5000); return 1; }); + scope.join(); + } catch (CompletionException e) { + System.out.println(" " + e.getClass().getSimpleName() + ", cause = " + e.getCause().getClass().getName()); + } + } +} diff --git a/structured-concurrency-final/src/FailFast.java b/structured-concurrency-final/src/FailFast.java new file mode 100644 index 0000000..3a668b9 --- /dev/null +++ b/structured-concurrency-final/src/FailFast.java @@ -0,0 +1,41 @@ +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Subtask; + +/** The same two subtasks in a StructuredTaskScope: the failure of one cancels the other, before handle() returns. */ +public class FailFast { + + static String findUser() throws InterruptedException { + Thread.sleep(50); + throw new IllegalStateException("user service down"); + } + + static int fetchOrder() throws InterruptedException { + try { + Thread.sleep(300); + System.out.println(" fetchOrder() finished"); + return 42; + } catch (InterruptedException e) { + System.out.println(" fetchOrder() was interrupted"); + throw e; + } + } + + static String handle() throws InterruptedException { + try (var scope = StructuredTaskScope.open()) { + Subtask user = scope.fork(FailFast::findUser); + Subtask order = scope.fork(FailFast::fetchOrder); + scope.join(); + return user.get() + " " + order.get(); + } catch (ExecutionException e) { + System.out.println("handle() failed: " + e.getCause()); + return null; + } + } + + public static void main(String[] args) throws Exception { + handle(); + System.out.println("handle() has returned. Waiting 400 ms to see what the other thread does:"); + Thread.sleep(400); + System.out.println("(nothing more happened)"); + } +} diff --git a/structured-concurrency-final/src/Failures.java b/structured-concurrency-final/src/Failures.java new file mode 100644 index 0000000..143870b --- /dev/null +++ b/structured-concurrency-final/src/Failures.java @@ -0,0 +1,87 @@ +import java.io.IOException; +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Subtask; + +/** What goes wrong when the structure is violated, and what the exception looks like. */ +public class Failures { + + static void attempt(String title, Callable body) { + System.out.println("-- " + title); + try { + Object r = body.call(); + System.out.println(" returned " + r); + } catch (Throwable t) { + System.out.println(" " + t.getClass().getName() + ": " + t.getMessage()); + } + } + + public static void main(String[] args) throws Exception { + attempt("Subtask.get() before join()", () -> { + try (var scope = StructuredTaskScope.open()) { + Subtask s = scope.fork(() -> "x"); + return s.get(); + } + }); + + attempt("leaving the try block without join()", () -> { + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> "x"); + return "left the block"; + } + }); + + attempt("fork() from a thread that does not own the scope", () -> { + try (var scope = StructuredTaskScope.open()) { + var other = new CompletableFuture(); + Thread.ofVirtual().start(() -> { + try { scope.fork(() -> "x"); other.complete("forked"); } + catch (Throwable t) { other.complete(t.getClass().getName() + ": " + t.getMessage()); } + }); + Object r = other.get(); + scope.join(); + return r; + } + }); + + attempt("closing scopes out of order (not try-with-resources)", () -> { + var outer = StructuredTaskScope.open(); + var inner = StructuredTaskScope.open(); + try { + outer.close(); // the inner scope is still open + return "closed outer first"; + } finally { + try { inner.close(); } catch (Throwable ignore) { } + try { outer.close(); } catch (Throwable ignore) { } + } + }); + + attempt("join() twice", () -> { + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> "x"); + scope.join(); + return scope.join(); + } + }); + + System.out.println("-- the failure surfaces as ExecutionException; pattern-match the cause"); + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> { throw new IOException("disk gone"); }); + scope.join(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + switch (cause) { + case IOException ioe -> System.out.println(" I/O failed: " + ioe.getMessage()); + default -> System.out.println(" unexpected: " + cause); + } + } + + System.out.println("-- two subtasks fail: which cause do you get?"); + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> { Thread.sleep(20); throw new IllegalStateException("second"); }); + scope.fork(() -> { throw new IllegalStateException("first"); }); + scope.join(); + } catch (ExecutionException e) { + System.out.println(" cause = " + e.getCause().getMessage() + " (the first to fail wins)"); + } + } +} diff --git a/structured-concurrency-final/src/Joiners.java b/structured-concurrency-final/src/Joiners.java new file mode 100644 index 0000000..ecf134f --- /dev/null +++ b/structured-concurrency-final/src/Joiners.java @@ -0,0 +1,89 @@ +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Joiner; +import java.util.concurrent.StructuredTaskScope.Subtask; +import java.util.concurrent.atomic.AtomicInteger; + +/** The built-in Joiner factories, each with a result and a failure. */ +public class Joiners { + + /** Names of the subtasks that were interrupted; printed after the scope closes so the order is stable. */ + static final Set interrupted = new ConcurrentSkipListSet<>(); + + static void reportInterrupted() { + System.out.println(" interrupted: " + interrupted); + interrupted.clear(); + } + + static Callable task(String name, long ms) { + return () -> { + try { Thread.sleep(ms); return name; } + catch (InterruptedException e) { interrupted.add(name); throw e; } + }; + } + static Callable failing(String name, long ms) { + return () -> { Thread.sleep(ms); throw new IllegalStateException(name + " failed"); }; + } + + /** A checked exception of our own, to show the Function overloads. */ + static class ServiceException extends Exception { + ServiceException(Throwable cause) { super("service call failed: " + cause.getMessage(), cause); } + } + + public static void main(String[] args) throws Exception { + System.out.println("-- allSuccessfulOrThrow(): List in FORK order, not completion order"); + try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) { + scope.fork(task("slow", 150)); scope.fork(task("fast", 10)); scope.fork(task("medium", 80)); + System.out.println(" " + scope.join()); + } + + System.out.println("-- allSuccessfulOrThrow(): one failure cancels the others, join() throws ExecutionException"); + try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) { + scope.fork(task("slow", 500)); scope.fork(failing("broken", 30)); + scope.join(); + } catch (ExecutionException e) { + System.out.println(" " + e.getClass().getSimpleName() + ", cause = " + e.getCause()); + } + reportInterrupted(); + + System.out.println("-- anySuccessfulOrThrow(): the race; losers are interrupted"); + try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow())) { + scope.fork(task("A-300ms", 300)); scope.fork(task("B-50ms", 50)); scope.fork(task("C-300ms", 300)); + System.out.println(" winner: " + scope.join()); + } + reportInterrupted(); + + System.out.println("-- anySuccessfulOrThrow(): when every subtask fails"); + try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow())) { + scope.fork(failing("one", 10)); scope.fork(failing("two", 20)); + scope.join(); + } catch (ExecutionException e) { + System.out.println(" " + e.getClass().getSimpleName() + ", cause = " + e.getCause()); + } + + System.out.println("-- awaitAllSuccessfulOrThrow(): returns null (Void), like the zero-argument open()"); + try (var scope = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow())) { + Subtask a = scope.fork(task("a", 10)); + Subtask b = scope.fork(task("b", 20)); + Void v = scope.join(); + System.out.println(" join() = " + v + ", a=" + a.get() + ", b=" + b.get()); + } + + System.out.println("-- allUntil(predicate): stop when two subtasks have completed"); + var done = new AtomicInteger(); + try (var scope = StructuredTaskScope.open(Joiner.allUntil(s -> done.incrementAndGet() == 2))) { + scope.fork(task("t1", 20)); scope.fork(task("t2", 60)); scope.fork(task("t3", 400)); scope.fork(task("t4", 400)); + List> subtasks = scope.join(); + subtasks.forEach(s -> System.out.println(" " + s.state() + (s.state() == Subtask.State.SUCCESS ? " " + s.get() : ""))); + } + reportInterrupted(); + + System.out.println("-- the Function overload: join() throws OUR exception type"); + try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(ServiceException::new))) { + scope.fork(failing("orders", 10)); + scope.join(); + } catch (ServiceException e) { + System.out.println(" caught " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } +} diff --git a/structured-concurrency-final/src/QuickReference.java b/structured-concurrency-final/src/QuickReference.java new file mode 100644 index 0000000..ba073a3 --- /dev/null +++ b/structured-concurrency-final/src/QuickReference.java @@ -0,0 +1,40 @@ +import java.time.Duration; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Joiner; +import java.util.concurrent.StructuredTaskScope.Subtask; + +/** One line per API in the article's quick reference. Compiles on JDK 28 with no flags. */ +public class QuickReference { + + @SuppressWarnings("unused") + static void shapes(ThreadFactory factory) throws Exception { + // region quickref + // Open a scope: the policy is the Joiner, the type parameters are + var plain = StructuredTaskScope.open(); // join() -> Void, throws ExecutionException + var all = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow()); // join() -> List, in fork order + var any = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow()); // join() -> String, first success wins + var await = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow()); // join() -> Void + var until = StructuredTaskScope.open(Joiner.allUntil(s -> false)); // join() -> List>, no checked exception + var mine = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(IllegalStateException::new)); // your exception type + + // Configure: name for thread dumps, a deadline for the whole group, a thread factory for the subtasks + var cfg = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(), + cf -> cf.withName("orders").withTimeout(Duration.ofMillis(500)).withThreadFactory(factory)); + + // Fork, then read each Subtask only after join() + Subtask subtask = all.fork(() -> "value"); + all.join(); + String value = subtask.get(); // like Future.resultNow() + Subtask.State state = subtask.state(); // SUCCESS | FAILED | UNAVAILABLE + // Subtask also has exception(); see output/08-api-28.txt for every signature + // endregion quickref + // scopes opened by one thread must be closed in the reverse order (try-with-resources does this for you) + cfg.close(); mine.close(); until.close(); await.close(); any.close(); all.close(); plain.close(); + } + + public static void main(String[] args) throws Exception { + shapes(Thread.ofVirtual().factory()); + System.out.println("quick reference compiled and ran"); + } +} diff --git a/structured-concurrency-final/src/RandomTasks.java b/structured-concurrency-final/src/RandomTasks.java new file mode 100644 index 0000000..b0d7ae5 --- /dev/null +++ b/structured-concurrency-final/src/RandomTasks.java @@ -0,0 +1,33 @@ +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Joiner; + +/** Five tasks that sleep a random time and fail if they were "too slow". Seeded, so the transcript is repeatable. */ +public class RandomTasks { + + static class TooSlowException extends Exception { + TooSlowException(String m) { super(m); } + } + + public static void main(String[] args) throws Exception { + long seed = Long.parseLong(args[0]); + int threshold = 700; + Random random = new Random(seed); + List> tasks = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + int duration = random.nextInt(1000); + System.out.println("Duration: " + duration); + tasks.add(() -> { + Thread.sleep(duration); + if (duration > threshold) throw new TooSlowException("Duration " + duration + " greater than threshold " + threshold); + return duration; + }); + } + try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) { + tasks.forEach(scope::fork); + for (int r : scope.join()) System.out.println("Result: " + r); + } catch (ExecutionException e) { + System.out.println("ExecutionException: " + e.getCause().getClass().getSimpleName() + ": " + e.getCause().getMessage()); + } + } +} diff --git a/structured-concurrency-final/src/ScopedValues.java b/structured-concurrency-final/src/ScopedValues.java new file mode 100644 index 0000000..d22fe80 --- /dev/null +++ b/structured-concurrency-final/src/ScopedValues.java @@ -0,0 +1,24 @@ +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Subtask; + +/** Subtasks inherit ScopedValue bindings (JEP 506, final in JDK 25). A plain thread does not. */ +public class ScopedValues { + static final ScopedValue REQUEST_ID = ScopedValue.newInstance(); + + public static void main(String[] args) throws Exception { + ScopedValue.where(REQUEST_ID, "req-7731").run(() -> { + try (var scope = StructuredTaskScope.open()) { + Subtask a = scope.fork(() -> "forked subtask sees " + REQUEST_ID.get()); + scope.join(); + System.out.println(a.get()); + } catch (Exception e) { + throw new RuntimeException(e); + } + Thread t = new Thread(() -> + System.out.println("plain thread: bound? " + REQUEST_ID.isBound())); + t.start(); + try { t.join(); } catch (InterruptedException e) { } + }); + System.out.println("outside the binding: bound? " + REQUEST_ID.isBound()); + } +} diff --git a/structured-concurrency-final/src/Server.java b/structured-concurrency-final/src/Server.java new file mode 100644 index 0000000..d3baf8b --- /dev/null +++ b/structured-concurrency-final/src/Server.java @@ -0,0 +1,36 @@ +import java.util.concurrent.ExecutionException; +import java.util.concurrent.StructuredTaskScope; +import java.util.concurrent.StructuredTaskScope.Subtask; + +record Response(String user, int order) {} + +/** The canonical program: open, fork, join, process, close. */ +public class Server { + + String findUser() throws InterruptedException { + Thread.sleep(120); // pretend: a network call + return "Alice"; + } + + int fetchOrder() throws InterruptedException { + Thread.sleep(80); // pretend: another network call + return 42; + } + + // join() now declares ExecutionException (the scope's third type parameter, R_X) as well as InterruptedException + Response handle() throws ExecutionException, InterruptedException { + try (var scope = StructuredTaskScope.open()) { // 1. open + Subtask user = scope.fork(this::findUser); // 2. fork + Subtask order = scope.fork(this::fetchOrder); // fork + + scope.join(); // 3. wait for BOTH, propagate any failure + + return new Response(user.get(), order.get()); // 4. process results + } // 5. close: cancel + wait (automatic) + } + + public static void main(String[] args) throws Exception { + System.out.println(new Server().handle()); + System.out.println("Runtime: " + Runtime.version()); + } +} diff --git a/structured-concurrency-final/src/Snippets.java b/structured-concurrency-final/src/Snippets.java new file mode 100644 index 0000000..fdd87f3 --- /dev/null +++ b/structured-concurrency-final/src/Snippets.java @@ -0,0 +1,86 @@ +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Joiner; +import java.util.concurrent.StructuredTaskScope.Subtask; + +/** The short fragments quoted in the article, as one compiling file (see output/12-snippets-compile.txt). */ +public class Snippets { + record Response(String user, int order) {} + + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + String findUser() throws InterruptedException { return "Alice"; } + int fetchOrder() throws InterruptedException { return 42; } + + class ExecutorVersion { + // region unstructured + Response handle() throws ExecutionException, InterruptedException { + Future user = executor.submit(() -> findUser()); + Future order = executor.submit(() -> fetchOrder()); + + String theUser = user.get(); // join findUser + int theOrder = order.get(); // join fetchOrder + return new Response(theUser, theOrder); + } + // endregion unstructured + } + + class SequentialVersion { + // region sequential + Response handle() throws IOException, InterruptedException { + String theUser = findUser(); + int theOrder = fetchOrder(); + return new Response(theUser, theOrder); + } + // endregion sequential + } + + // region runConcurrently + List runConcurrently(Collection> tasks) throws ExecutionException, InterruptedException { + try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) { + tasks.forEach(scope::fork); + return scope.join(); // List of results; throws ExecutionException if any task fails + } + } + // endregion runConcurrently + + // region race + T race(Collection> tasks) throws ExecutionException, InterruptedException { + try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulOrThrow())) { + tasks.forEach(scope::fork); + return scope.join(); // result of the first subtask to succeed + } + } + // endregion race + + // region handleWithCatch + Response handleWithCatch() throws InterruptedException { + try (var scope = StructuredTaskScope.open()) { + Subtask user = scope.fork(this::findUser); + Subtask order = scope.fork(this::fetchOrder); + scope.join(); + return new Response(user.get(), order.get()); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + switch (cause) { + case IOException ioe -> System.err.println("I/O failed: " + ioe); + default -> System.err.println("Unexpected: " + cause); + } + throw new IllegalStateException(e); + } + } + // endregion handleWithCatch + + public static void main(String[] args) throws Exception { + var s = new Snippets(); + try { + System.out.println(s.new ExecutorVersion().handle()); + System.out.println(s.new SequentialVersion().handle()); + System.out.println(s.runConcurrently(List.of(() -> 1, () -> 2))); + String winner = s.race(List.>of(() -> "a")); System.out.println(winner); + System.out.println(s.handleWithCatch()); + } finally { + s.executor.close(); + } + } +} diff --git a/structured-concurrency-final/src/StuckClose.java b/structured-concurrency-final/src/StuckClose.java new file mode 100644 index 0000000..14695eb --- /dev/null +++ b/structured-concurrency-final/src/StuckClose.java @@ -0,0 +1,33 @@ +import java.util.concurrent.*; + +/** A subtask that ignores interrupts keeps close() waiting, even though the scope was cancelled at once. */ +public class StuckClose { + + static int stubborn() { + long end = System.nanoTime() + 1_500_000_000L; + while (System.nanoTime() < end) { /* busy loop: never checks the interrupt flag */ } + return 1; + } + + static int cooperative() throws InterruptedException { + Thread.sleep(1_500); // blocking calls like sleep() respond to interrupt + return 1; + } + + static void run(String label, Callable slow) throws Exception { + long t0 = System.nanoTime(); + try (var scope = StructuredTaskScope.open()) { + scope.fork(slow); + scope.fork(() -> { Thread.sleep(50); throw new IllegalStateException("fails at 50 ms"); }); + scope.join(); + } catch (ExecutionException e) { + long ms = (System.nanoTime() - t0) / 1_000_000; + System.out.println(label + ": caught " + e.getCause().getMessage() + "; the try block took >= 1400 ms: " + (ms >= 1400)); + } + } + + public static void main(String[] args) throws Exception { + run("subtask that responds to interrupts", StuckClose::cooperative); + run("subtask that ignores interrupts ", StuckClose::stubborn); + } +} diff --git a/structured-concurrency-final/src/ThreadDump.java b/structured-concurrency-final/src/ThreadDump.java new file mode 100644 index 0000000..c0760cc --- /dev/null +++ b/structured-concurrency-final/src/ThreadDump.java @@ -0,0 +1,53 @@ +import com.sun.management.HotSpotDiagnosticMXBean; +import java.lang.management.ManagementFactory; +import java.nio.file.*; +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Joiner; + +/** The thread dump shows the scope as a container, its owner, and the threads forked in it. */ +public class ThreadDump { + public static void main(String[] args) throws Exception { + if (args.length > 0 && args[0].equals("hold")) { // used by run.sh: hold nested scopes open so jcmd can dump them + try (var scope = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow(), cf -> cf.withName("orders"))) { + scope.fork(() -> { Thread.sleep(6000); return "A"; }); + scope.fork(() -> { // subtask B opens a child scope of its own + try (var child = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow(), cf -> cf.withName("inventory"))) { + child.fork(() -> { Thread.sleep(6000); return "B1"; }); + child.join(); + } + return "B"; + }); + scope.join(); + } + return; + } + Path file = Files.createTempFile("dump", ".json"); Files.delete(file); + try (var scope = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow(), cf -> cf.withName("orders"))) { + for (int i = 0; i < 2; i++) { + int n = i; + scope.fork(() -> { Thread.sleep(1500); return "task-" + n; }); + } + Thread.sleep(300); // let the subtasks start and block + ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class) + .dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON); + scope.join(); + } + String json = Files.readString(file); + int at = json.indexOf("\"container\": \"orders"); + int start = json.lastIndexOf('{', at); + int end = json.indexOf("\"threadCount\"", at); + String block = json.substring(start, end); + // identity hashes, thread ids and timestamps change between runs: mask them so the transcript is stable + block = block.replaceAll("@[0-9a-f]+", "@").replaceAll("\"tid\": \\d+", "\"tid\": ").replaceAll("\"owner\": \\d+", "\"owner\": "); + // keep the lines that show the hierarchy: the scope, its parent and owner, and each forked thread with its own frame + for (String line : block.split("\n")) { + String l = line.trim(); + if (l.startsWith("\"container\"") || l.startsWith("\"parent\"") || l.startsWith("\"owner\"") + || l.startsWith("\"tid\"") || l.startsWith("\"virtual\"") || l.startsWith("\"state\"") + || l.contains("ThreadDump.lambda")) { + System.out.println(l); + } + } + Files.deleteIfExists(file); + } +} diff --git a/structured-concurrency-final/src/Timeouts.java b/structured-concurrency-final/src/Timeouts.java new file mode 100644 index 0000000..eec6c1f --- /dev/null +++ b/structured-concurrency-final/src/Timeouts.java @@ -0,0 +1,61 @@ +import java.time.Duration; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.StructuredTaskScope.Joiner; +import java.util.concurrent.StructuredTaskScope.Subtask; + +/** Deadlines for a whole group of subtasks, and what the Joiner's timeout() method does with them. */ +public class Timeouts { + + static List runConcurrently(Collection> tasks, ThreadFactory factory, Duration timeout) + throws ExecutionException, InterruptedException { + try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(), + cf -> cf.withName("orders").withThreadFactory(factory).withTimeout(timeout))) { + tasks.forEach(scope::fork); + return scope.join(); + } + } + + /** Collects successes; on timeout it hands back what it has instead of failing. */ + static class BestEffort implements Joiner, RuntimeException> { + private final Queue results = new ConcurrentLinkedQueue<>(); + public boolean onComplete(Subtask s) { + if (s.state() == Subtask.State.SUCCESS) results.add(s.get()); + return false; + } + public List result() { return List.copyOf(results); } + public List timeout() { return List.copyOf(results); } // no exception: a partial answer + } + + public static void main(String[] args) throws Exception { + ThreadFactory factory = Thread.ofVirtual().name("order-", 0).factory(); + Callable fast = () -> { Thread.sleep(20); return "fast"; }; + Callable slow = () -> { Thread.sleep(5000); return "slow"; }; + + System.out.println("-- inside the deadline"); + System.out.println(" " + runConcurrently(List.of(fast, fast), factory, Duration.ofSeconds(1))); + + System.out.println("-- deadline 200 ms, one subtask needs 5 s"); + long t0 = System.nanoTime(); + try { + runConcurrently(List.of(fast, slow), factory, Duration.ofMillis(200)); + } catch (ExecutionException e) { + long ms = (System.nanoTime() - t0) / 1_000_000; + System.out.println(" " + e.getClass().getSimpleName() + ", cause = " + e.getCause().getClass().getName()); + System.out.println(" returned within " + (ms < 1000 ? "well under a second" : ms + " ms") + " (not 5 s): the slow subtask was interrupted"); + } + + System.out.println("-- the same deadline with a joiner whose timeout() returns a partial result"); + try (var scope = StructuredTaskScope.open(new BestEffort(), cf -> cf.withTimeout(Duration.ofMillis(200)))) { + scope.fork(fast); scope.fork(slow); + System.out.println(" join() = " + scope.join()); + } + + System.out.println("-- a thread factory decides the subtask thread's name"); + try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow(), cf -> cf.withThreadFactory(Thread.ofVirtual().name("order-", 0).factory()))) { + scope.fork(() -> Thread.currentThread().getName()); + scope.fork(() -> Thread.currentThread().getName()); + System.out.println(" " + scope.join()); + } + } +} diff --git a/structured-concurrency-final/src/Unstructured.java b/structured-concurrency-final/src/Unstructured.java new file mode 100644 index 0000000..cb1ee8b --- /dev/null +++ b/structured-concurrency-final/src/Unstructured.java @@ -0,0 +1,40 @@ +import java.util.concurrent.*; + +/** The ExecutorService version of handle(): findUser() fails, and fetchOrder() keeps running. */ +public class Unstructured { + + static String findUser() throws InterruptedException { + Thread.sleep(50); + throw new IllegalStateException("user service down"); + } + + static int fetchOrder() throws InterruptedException { + try { + Thread.sleep(300); + System.out.println(" fetchOrder() finished, although nobody wants the answer any more"); + return 42; + } catch (InterruptedException e) { + System.out.println(" fetchOrder() was interrupted"); + throw e; + } + } + + public static void main(String[] args) throws Exception { + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + try { + Future user = executor.submit(Unstructured::findUser); + Future order = executor.submit(Unstructured::fetchOrder); + try { + String u = user.get(); // throws after ~50 ms ... + int o = order.get(); + System.out.println(u + " " + o); + } catch (ExecutionException e) { + System.out.println("handle() failed: " + e.getCause()); + } + System.out.println("handle() has returned. Waiting 400 ms to see what the other thread does:"); + Thread.sleep(400); + } finally { + executor.close(); + } + } +}