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 super T, ? extends R, R_X>, UnaryOperator);
+ public static StructuredTaskScope open(StructuredTaskScope$Joiner super T, ? extends R, R_X>);
+ public static StructuredTaskScope open(UnaryOperator);
+ public static StructuredTaskScope open();
+ public abstract StructuredTaskScope$Subtask fork(Callable extends U>);
+ 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 super StructuredTaskScope$Subtask>);
+}
+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 super T, ? extends R>, Function);
+> public static StructuredTaskScope open(StructuredTaskScope$Joiner super T, ? extends R>, UnaryOperator);
+< public default boolean onFork(StructuredTaskScope$Subtask extends T>);
+< public default boolean onComplete(StructuredTaskScope$Subtask extends T>);
+> 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 super T, ? extends R>, UnaryOperator);
+< public static StructuredTaskScope open(StructuredTaskScope$Joiner super T, ? extends R>);
+< public static StructuredTaskScope open();
+> public interface StructuredTaskScope extends AutoCloseable {
+> public static StructuredTaskScope open(StructuredTaskScope$Joiner super T, ? extends R, R_X>, UnaryOperator);
+> public static StructuredTaskScope open(StructuredTaskScope$Joiner super T, ? extends R, R_X>);
+> 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 super StructuredTaskScope$Subtask>);
+
+$ 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