You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/03/22 05:58:41 UTC
(groovy) branch GROOVY-9381_3 updated (d7e1666e4c -> 997752ed4e)
This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a change to branch GROOVY-9381_3
in repository https://gitbox.apache.org/repos/asf/groovy.git
discard d7e1666e4c GROOVY-9381: Add native async/await support
new 997752ed4e GROOVY-9381: Add native async/await support
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (d7e1666e4c)
\
N -- N -- N refs/heads/GROOVY-9381_3 (997752ed4e)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../codehaus/groovy/transform/AsyncApiTest.groovy | 765 ++++++++++++++++++++-
.../groovy/transform/AsyncAwaitSyntaxTest.groovy | 34 +-
.../groovy/transform/AsyncClosureLambdaTest.groovy | 48 +-
.../transform/AsyncContextChannelTest.groovy | 551 ++++++++++++++-
.../groovy/transform/AsyncDeferFlowTest.groovy | 201 +++++-
.../transform/AsyncExceptionHandlingTest.groovy | 165 ++++-
.../groovy/transform/AsyncPatternsTest.groovy | 264 ++++++-
7 files changed, 1991 insertions(+), 37 deletions(-)
(groovy) 01/01: GROOVY-9381: Add native async/await support
Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch GROOVY-9381_3
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 997752ed4ee01fbbfe83e6f1a631baa5a9786551
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Mar 7 20:00:28 2026 +0900
GROOVY-9381: Add native async/await support
---
build.gradle | 2 +
gradle/verification-metadata.xml | 16 +
src/antlr/GroovyLexer.g4 | 3 +
src/antlr/GroovyParser.g4 | 15 +-
src/main/java/groovy/concurrent/AsyncContext.java | 379 ++
src/main/java/groovy/concurrent/AsyncScope.java | 318 ++
src/main/java/groovy/concurrent/AsyncStream.java | 146 +
src/main/java/groovy/concurrent/AwaitResult.java | 124 +
src/main/java/groovy/concurrent/Awaitable.java | 578 +++
.../java/groovy/concurrent/AwaitableAdapter.java | 86 +
.../concurrent/AwaitableAdapterRegistry.java | 350 ++
src/main/java/groovy/concurrent/Channel.java | 409 ++
.../groovy/concurrent/ChannelClosedException.java | 36 +
src/main/java/groovy/transform/Async.java | 134 +
.../apache/groovy/parser/antlr4/AstBuilder.java | 176 +-
.../groovy/parser/antlr4/ModifierManager.java | 22 +-
.../groovy/runtime/async/AbstractAsyncStream.java | 287 ++
.../groovy/runtime/async/AsyncStreamGenerator.java | 279 ++
.../apache/groovy/runtime/async/AsyncSupport.java | 1139 ++++++
.../groovy/runtime/async/FlowPublisherAdapter.java | 392 ++
.../apache/groovy/runtime/async/GroovyPromise.java | 256 ++
.../java/org/codehaus/groovy/ast/ModifierNode.java | 2 +
.../groovy/transform/AsyncASTTransformation.java | 254 ++
.../groovy/transform/AsyncTransformHelper.java | 411 ++
.../services/groovy.concurrent.AwaitableAdapter | 16 +
src/spec/doc/core-async-await.adoc | 1624 ++++++++
src/spec/test/AsyncAwaitSpecTest.groovy | 1828 +++++++++
.../runtime/async/AsyncConcurrencyTest.groovy | 540 +++
.../codehaus/groovy/transform/AsyncApiTest.groovy | 4272 ++++++++++++++++++++
.../groovy/transform/AsyncAwaitSyntaxTest.groovy | 2676 ++++++++++++
.../groovy/transform/AsyncBestPracticesTest.groovy | 1476 +++++++
.../groovy/transform/AsyncClosureLambdaTest.groovy | 774 ++++
.../transform/AsyncContextChannelTest.groovy | 1519 +++++++
.../groovy/transform/AsyncDeferFlowTest.groovy | 802 ++++
.../transform/AsyncExceptionHandlingTest.groovy | 1599 ++++++++
.../transform/AsyncFrameworkIntegrationTest.groovy | 756 ++++
.../groovy/transform/AsyncPatternsTest.groovy | 1898 +++++++++
.../transform/AsyncRuntimeEnhancementTest.groovy | 254 ++
.../groovy/transform/AsyncTransformTest.groovy | 1745 ++++++++
.../groovy/transform/AsyncVirtualThreadTest.groovy | 1160 ++++++
versions.properties | 2 +
41 files changed, 28751 insertions(+), 4 deletions(-)
diff --git a/build.gradle b/build.gradle
index e14b0749ae..749839993d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -118,6 +118,8 @@ dependencies {
testImplementation "com.thoughtworks.qdox:qdox:${versions.qdox}"
testImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${versions.jackson}"
+ testImplementation "io.reactivex.rxjava3:rxjava:${versions.rxjava3}"
+ testImplementation "io.projectreactor:reactor-core:${versions.reactor}"
testFixturesImplementation projects.groovyXml
testFixturesImplementation projects.groovyTest
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 5c952c2e10..82abc5b42d 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -116,6 +116,7 @@
<ignored-key id="C71FB765CD9DE313" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="C7CA19B7B620D787" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="CA80D1F0EB6CA4BA" reason="Key couldn't be downloaded from any key server"/>
+ <ignored-key id="D1031D14464180E0" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="D2151178A123C97F" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="D364ABAA39A47320" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="D7742D58455ECC7C" reason="Key couldn't be downloaded from any key server"/>
@@ -874,6 +875,16 @@
<sha512 value="f220e44fe6b61f8dbb61226f832dfb16a09584384540fd48a4dff5c4de9fee060623f85cbead720dfe776aa25105949e70758a9bb1d9db43f63068d8d22164c9" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="io.projectreactor" name="reactor-core" version="3.7.3">
+ <artifact name="reactor-core-3.7.3.jar">
+ <pgp value="48B086A7D843CFA258E83286928FBF39003C0425"/>
+ </artifact>
+ </component>
+ <component group="io.reactivex.rxjava3" name="rxjava" version="3.1.10">
+ <artifact name="rxjava-3.1.10.jar">
+ <pgp value="E9CC3CD1AE59E851E4DB3FA350FFD7487D34B5B9"/>
+ </artifact>
+ </component>
<component group="jakarta.activation" name="jakarta.activation-api" version="1.2.1">
<artifact name="jakarta.activation-api-1.2.1.jar">
<pgp value="6DD3B8C64EF75253BEB2C53AD908A43FB7EC07AC"/>
@@ -2229,6 +2240,11 @@
<sha512 value="adcc480f68828ffd68d03846be852988b595c2e1bb69224d273578dd6c2ad2773edfe96625a7c00bc40ae0f2d1cac8412eaa54b88cc8e681b0b4c0ee3b082333" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="org.reactivestreams" name="reactive-streams" version="1.0.4">
+ <artifact name="reactive-streams-1.0.4.jar">
+ <sha512 value="cdab6bd156f39106cd6bbfd47df1f4b0a89dc4aa28c68c31ef12a463193c688897e415f01b8d7f0d487b0e6b5bd2f19044bf8605704b024f26d6aa1f4f9a2471" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
+ </artifact>
+ </component>
<component group="org.reflections" name="reflections" version="0.10.2">
<artifact name="reflections-0.10.2.jar">
<pgp value="3F2A008A91D11A7FAC4A0786F13D3E721D56BD54"/>
diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4
index 79ddf78dbd..f1d69c5c95 100644
--- a/src/antlr/GroovyLexer.g4
+++ b/src/antlr/GroovyLexer.g4
@@ -392,6 +392,9 @@ DEF : 'def';
IN : 'in';
TRAIT : 'trait';
THREADSAFE : 'threadsafe'; // reserved keyword
+ASYNC : 'async';
+AWAIT : 'await';
+DEFER : 'defer';
// §3.9 Keywords
BuiltInPrimitiveType
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 88e3b8b713..daccdcdcc3 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -134,6 +134,7 @@ modifier
| VOLATILE
| DEF
| VAR
+ | ASYNC
)
;
@@ -600,7 +601,7 @@ switchStatement
;
loopStatement
- : FOR LPAREN forControl RPAREN nls statement #forStmtAlt
+ : FOR AWAIT? LPAREN forControl RPAREN nls statement #forStmtAlt
| WHILE expressionInPar nls statement #whileStmtAlt
| DO nls statement nls WHILE expressionInPar #doWhileStmtAlt
;
@@ -642,6 +643,8 @@ statement
| continueStatement #continueStmtAlt
| { inSwitchExpressionLevel > 0 }?
yieldStatement #yieldStmtAlt
+ | YIELD RETURN nls expression #yieldReturnStmtAlt
+ | DEFER nls statementExpression #deferStmtAlt
| identifier COLON nls statement #labeledStmtAlt
| assertStatement #assertStmtAlt
| localVariableDeclaration #localVariableDeclarationStmtAlt
@@ -778,6 +781,10 @@ expression
// must come before postfixExpression to resolve the ambiguities between casting and call on parentheses expression, e.g. (int)(1 / 2)
: castParExpression castOperandExpression #castExprAlt
+ // async closure/lambda must come before postfixExpression to resolve the ambiguities between async and method call, e.g. async { ... }
+ | ASYNC nls closureOrLambdaExpression #asyncClosureExprAlt
+ | AWAIT nls (LPAREN expression RPAREN | expression) #awaitExprAlt
+
// qualified names, array expressions, method invocation, post inc/dec
| postfixExpression #postfixExprAlt
@@ -1228,6 +1235,9 @@ identifier
: Identifier
| CapitalizedIdentifier
| AS
+ | ASYNC
+ | AWAIT
+ | DEFER
| IN
| PERMITS
| RECORD
@@ -1246,12 +1256,15 @@ keywords
: ABSTRACT
| AS
| ASSERT
+ | ASYNC
+ | AWAIT
| BREAK
| CASE
| CATCH
| CLASS
| CONST
| CONTINUE
+ | DEFER
| DEF
| DEFAULT
| DO
diff --git a/src/main/java/groovy/concurrent/AsyncContext.java b/src/main/java/groovy/concurrent/AsyncContext.java
new file mode 100644
index 0000000000..a1ff1928b1
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncContext.java
@@ -0,0 +1,379 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+import groovy.lang.Closure;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Lightweight async-local context map for propagating execution-scoped
+ * state across asynchronous boundaries.
+ * <p>
+ * {@code AsyncContext} addresses the common production need for carrying
+ * request correlation IDs, tenant IDs, security principals, trace
+ * metadata, and other small pieces of execution-scoped state across
+ * thread hops introduced by {@code async}/{@code await}.
+ * <p>
+ * The current context is stored per thread. The Groovy async runtime
+ * captures a {@linkplain Snapshot snapshot} when a task or continuation
+ * is scheduled and restores that snapshot when it executes. This
+ * copy-on-spawn model provides two key guarantees:
+ * <ul>
+ * <li>Child tasks inherit the parent's context as it existed at spawn
+ * time.</li>
+ * <li>Child-side mutations remain isolated from the parent and
+ * siblings — no accidental cross-task leaking.</li>
+ * </ul>
+ * <p>
+ * Keys are normalized to non-null {@link String} values. Supplying
+ * {@code null} as a value removes the key.
+ *
+ * @since 6.0.0
+ */
+public final class AsyncContext {
+
+ private static final ThreadLocal<AsyncContext> CURRENT =
+ ThreadLocal.withInitial(AsyncContext::new);
+
+ private final Map<String, Object> values;
+
+ /**
+ * Creates an empty async context.
+ */
+ public AsyncContext() {
+ this.values = new LinkedHashMap<>();
+ }
+
+ /**
+ * Creates a context seeded from the supplied entries.
+ *
+ * @param initialValues initial context entries; may be {@code null}
+ */
+ public AsyncContext(Map<?, ?> initialValues) {
+ this();
+ putAll(initialValues);
+ }
+
+ // Internal constructor that directly adopts a pre-copied map
+ private AsyncContext(LinkedHashMap<String, Object> values, boolean ignored) {
+ this.values = values;
+ }
+
+ /**
+ * Returns the live async context associated with the current thread.
+ *
+ * @return the current async context, never {@code null}
+ */
+ public static AsyncContext current() {
+ return CURRENT.get();
+ }
+
+ /**
+ * Captures an immutable snapshot of the current async context.
+ *
+ * @return the captured snapshot
+ */
+ public static Snapshot capture() {
+ return Snapshot.of(current().copyValues());
+ }
+
+ /**
+ * Executes the supplier with the given snapshot installed as the current
+ * async context, restoring the previous context afterwards.
+ *
+ * @param snapshot the snapshot to install
+ * @param supplier the action to execute
+ * @param <T> the result type
+ * @return the supplier's result
+ */
+ public static <T> T withSnapshot(Snapshot snapshot, Supplier<T> supplier) {
+ Objects.requireNonNull(snapshot, "snapshot must not be null");
+ Objects.requireNonNull(supplier, "supplier must not be null");
+ AsyncContext previous = CURRENT.get();
+ CURRENT.set(new AsyncContext(new LinkedHashMap<>(snapshot.values), true));
+ try {
+ return supplier.get();
+ } finally {
+ CURRENT.set(previous);
+ }
+ }
+
+ /**
+ * Executes the runnable with the given snapshot installed as the current
+ * async context, restoring the previous context afterwards.
+ *
+ * @param snapshot the snapshot to install
+ * @param action the action to execute
+ */
+ public static void withSnapshot(Snapshot snapshot, Runnable action) {
+ withSnapshot(snapshot, () -> {
+ action.run();
+ return null;
+ });
+ }
+
+ /**
+ * Temporarily overlays the current async context with the supplied
+ * entries for the duration of the closure.
+ * <p>
+ * Any modifications performed inside the closure are scoped to that
+ * dynamic extent and are discarded when the closure returns or throws.
+ *
+ * @param entries the entries to overlay; may be {@code null}
+ * @param action the closure to execute
+ * @param <T> the result type
+ * @return the closure's result
+ */
+ public static <T> T with(Map<?, ?> entries, Closure<T> action) {
+ Objects.requireNonNull(action, "action must not be null");
+ Snapshot merged = capture().with(entries);
+ return withSnapshot(merged, () -> action.call());
+ }
+
+ /**
+ * Looks up a value in the current context by key.
+ *
+ * @param key the context key
+ * @return the stored value, or {@code null} if absent
+ */
+ public Object get(String key) {
+ return values.get(normalizeKey(key));
+ }
+
+ /**
+ * Groovy operator support for {@code context['traceId']}.
+ *
+ * @param key the context key
+ * @return the stored value, or {@code null} if absent
+ */
+ public Object getAt(String key) {
+ return get(key);
+ }
+
+ /**
+ * Stores a value in the current context.
+ * <p>
+ * A {@code null} value removes the key.
+ *
+ * @param key the context key
+ * @param value the value to store, or {@code null} to remove the key
+ * @return the previous value associated with the key, or {@code null}
+ */
+ public Object put(String key, Object value) {
+ String normalized = normalizeKey(key);
+ if (value == null) {
+ return values.remove(normalized);
+ }
+ return values.put(normalized, value);
+ }
+
+ /**
+ * Groovy operator support for {@code context['traceId'] = 'abc-123'}.
+ *
+ * @param key the context key
+ * @param value the value to store, or {@code null} to remove the key
+ */
+ public void putAt(String key, Object value) {
+ put(key, value);
+ }
+
+ /**
+ * Removes a key from the context.
+ *
+ * @param key the context key
+ * @return the removed value, or {@code null} if absent
+ */
+ public Object remove(String key) {
+ return values.remove(normalizeKey(key));
+ }
+
+ /**
+ * Adds all entries from the supplied map to this context.
+ * <p>
+ * A {@code null} map is ignored. {@code null} values remove keys.
+ *
+ * @param entries the entries to add
+ */
+ public void putAll(Map<?, ?> entries) {
+ if (entries == null || entries.isEmpty()) {
+ return;
+ }
+ for (Map.Entry<?, ?> entry : entries.entrySet()) {
+ put(normalizeKey(entry.getKey()), entry.getValue());
+ }
+ }
+
+ /**
+ * Removes all entries from this context.
+ */
+ public void clear() {
+ values.clear();
+ }
+
+ /**
+ * Returns {@code true} if the given key is present.
+ *
+ * @param key the context key
+ * @return {@code true} if present
+ */
+ public boolean containsKey(String key) {
+ return values.containsKey(normalizeKey(key));
+ }
+
+ /**
+ * Returns the number of entries in this context.
+ *
+ * @return the entry count
+ */
+ public int size() {
+ return values.size();
+ }
+
+ /**
+ * Returns {@code true} if this context is empty.
+ *
+ * @return {@code true} if empty
+ */
+ public boolean isEmpty() {
+ return values.isEmpty();
+ }
+
+ /**
+ * Returns an immutable copy of this context's entries.
+ *
+ * @return an immutable snapshot of the context
+ */
+ public Map<String, Object> snapshot() {
+ return Collections.unmodifiableMap(copyValues());
+ }
+
+ @Override
+ public String toString() {
+ return "AsyncContext" + values;
+ }
+
+ private Map<String, Object> copyValues() {
+ return new LinkedHashMap<>(values);
+ }
+
+ private static String normalizeKey(Object key) {
+ return Objects.requireNonNull(key, "context key must not be null").toString();
+ }
+
+ /**
+ * Immutable async-context snapshot captured at task or continuation
+ * registration time.
+ *
+ * @since 6.0.0
+ */
+ public static final class Snapshot {
+
+ private final Map<String, Object> values;
+
+ private Snapshot(Map<String, Object> values) {
+ this.values = values;
+ }
+
+ // Creates a snapshot that directly wraps a pre-copied map
+ static Snapshot of(Map<String, Object> preCopied) {
+ return new Snapshot(Collections.unmodifiableMap(preCopied));
+ }
+
+ /**
+ * Returns {@code true} if this snapshot contains the given key.
+ *
+ * @param key the context key
+ * @return {@code true} if present
+ */
+ public boolean containsKey(String key) {
+ return values.containsKey(normalizeKey(key));
+ }
+
+ /**
+ * Returns the captured value for the given key.
+ *
+ * @param key the context key
+ * @return the captured value, or {@code null} if absent
+ */
+ public Object get(String key) {
+ return values.get(normalizeKey(key));
+ }
+
+ /**
+ * Returns {@code true} if this snapshot contains no entries.
+ *
+ * @return {@code true} if empty
+ */
+ public boolean isEmpty() {
+ return values.isEmpty();
+ }
+
+ /**
+ * Returns the number of entries in this snapshot.
+ *
+ * @return the entry count
+ */
+ public int size() {
+ return values.size();
+ }
+
+ /**
+ * Returns the captured entries as an immutable map.
+ *
+ * @return the captured entries
+ */
+ public Map<String, Object> asMap() {
+ return values;
+ }
+
+ /**
+ * Creates a new snapshot by overlaying the supplied entries on top of
+ * this snapshot.
+ *
+ * @param entries the entries to merge; may be {@code null}
+ * @return the merged snapshot
+ */
+ public Snapshot with(Map<?, ?> entries) {
+ if (entries == null || entries.isEmpty()) {
+ return this;
+ }
+ Map<String, Object> merged = new LinkedHashMap<>(values);
+ for (Map.Entry<?, ?> entry : entries.entrySet()) {
+ String key = normalizeKey(entry.getKey());
+ Object value = entry.getValue();
+ if (value == null) {
+ merged.remove(key);
+ } else {
+ merged.put(key, value);
+ }
+ }
+ return Snapshot.of(merged);
+ }
+
+ @Override
+ public String toString() {
+ return "AsyncContext.Snapshot" + values;
+ }
+ }
+}
diff --git a/src/main/java/groovy/concurrent/AsyncScope.java b/src/main/java/groovy/concurrent/AsyncScope.java
new file mode 100644
index 0000000000..083551f34b
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncScope.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+import groovy.lang.Closure;
+import org.apache.groovy.runtime.async.AsyncSupport;
+import org.apache.groovy.runtime.async.GroovyPromise;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+
+/**
+ * Structured concurrency scope that ensures all child tasks complete
+ * (or are cancelled) before the scope exits.
+ *
+ * <h2>Design philosophy</h2>
+ * <p>{@code AsyncScope} provides a bounded lifetime for async tasks,
+ * following the structured concurrency model described in
+ * <a href="https://openjdk.org/jeps/453">JEP 453</a>. Unlike
+ * fire-and-forget {@link AsyncSupport#executeAsync}, tasks launched
+ * within a scope are guaranteed to complete before the scope closes.
+ * This prevents:
+ * <ul>
+ * <li>Orphaned tasks that outlive their logical parent</li>
+ * <li>Resource leaks from uncollected async work</li>
+ * <li>Silent failures from unobserved exceptions</li>
+ * </ul>
+ *
+ * <h2>Failure policy</h2>
+ * <p>By default, the scope uses a <b>fail-fast</b> policy: when any child
+ * task completes exceptionally, all sibling tasks are cancelled immediately.
+ * The first failure becomes the primary exception; subsequent failures are
+ * added as {@linkplain Throwable#addSuppressed(Throwable) suppressed}
+ * exceptions.</p>
+ *
+ * <h2>Usage in Groovy</h2>
+ * <pre>
+ * import groovy.concurrent.AsyncScope
+ * import groovy.concurrent.Awaitable
+ *
+ * def results = AsyncScope.withScope { scope ->
+ * def userTask = scope.async { fetchUser(id) }
+ * def orderTask = scope.async { fetchOrders(id) }
+ * return [user: await(userTask), orders: await(orderTask)]
+ * }
+ * // Both tasks guaranteed complete here
+ * </pre>
+ *
+ * <h2>Thread safety</h2>
+ * <p>All public methods are thread-safe. The child task list uses
+ * {@link CopyOnWriteArrayList} for safe concurrent iteration during
+ * cancellation. The {@link #closed} flag uses {@link AtomicBoolean}
+ * with CAS for exactly-once close semantics.</p>
+ *
+ * @see Awaitable
+ * @see AsyncSupport
+ * @since 6.0.0
+ */
+public class AsyncScope implements AutoCloseable {
+
+ private static final ThreadLocal<AsyncScope> CURRENT = new ThreadLocal<>();
+
+ private final List<CompletableFuture<?>> children = new CopyOnWriteArrayList<>();
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+ private final Executor executor;
+ private final boolean failFast;
+
+ /**
+ * Creates a new scope with the given executor and fail-fast policy.
+ *
+ * @param executor the executor for child tasks; must not be {@code null}
+ * @param failFast if {@code true}, cancel all siblings when any child fails
+ */
+ public AsyncScope(Executor executor, boolean failFast) {
+ Objects.requireNonNull(executor, "executor must not be null");
+ this.executor = executor;
+ this.failFast = failFast;
+ }
+
+ /**
+ * Creates a new scope with the given executor and fail-fast enabled.
+ *
+ * @param executor the executor for child tasks; must not be {@code null}
+ */
+ public AsyncScope(Executor executor) {
+ this(executor, true);
+ }
+
+ /**
+ * Creates a new scope with the default async executor and fail-fast enabled.
+ */
+ public AsyncScope() {
+ this(AsyncSupport.getExecutor(), true);
+ }
+
+ /**
+ * Returns the structured async scope currently bound to this thread, or
+ * {@code null} if execution is not inside {@link #withScope(Closure)} or
+ * a child launched from such a scope.
+ *
+ * @return the current scope, or {@code null}
+ */
+ public static AsyncScope current() {
+ return CURRENT.get();
+ }
+
+ /**
+ * Executes the supplier with the given scope installed as the current
+ * structured scope, restoring the previous binding afterwards.
+ *
+ * @param scope the scope to install; may be {@code null}
+ * @param supplier the action to execute
+ * @param <T> the result type
+ * @return the supplier's result
+ */
+ public static <T> T withCurrent(AsyncScope scope, Supplier<T> supplier) {
+ Objects.requireNonNull(supplier, "supplier must not be null");
+ AsyncScope previous = CURRENT.get();
+ if (scope == null) {
+ CURRENT.remove();
+ } else {
+ CURRENT.set(scope);
+ }
+ try {
+ return supplier.get();
+ } finally {
+ if (previous == null) {
+ CURRENT.remove();
+ } else {
+ CURRENT.set(previous);
+ }
+ }
+ }
+
+ /**
+ * Void overload of {@link #withCurrent(AsyncScope, Supplier)}.
+ *
+ * @param scope the scope to install; may be {@code null}
+ * @param action the action to execute
+ */
+ public static void withCurrent(AsyncScope scope, Runnable action) {
+ withCurrent(scope, () -> {
+ action.run();
+ return null;
+ });
+ }
+
+ /**
+ * Launches a child task within this scope. The task's lifetime is
+ * bound to the scope: when the scope is closed, all incomplete child
+ * tasks are cancelled. The child inherits a snapshot of the current
+ * {@link AsyncContext}, but any child-side context mutations remain
+ * isolated from the parent and siblings.
+ *
+ * @param body the async body to execute
+ * @param <T> the result type
+ * @return an {@link Awaitable} representing the child task
+ * @throws IllegalStateException if the scope has already been closed
+ */
+ @SuppressWarnings("unchecked")
+ public <T> Awaitable<T> async(Closure<T> body) {
+ Objects.requireNonNull(body, "body must not be null");
+ if (closed.get()) {
+ throw new IllegalStateException("AsyncScope is closed — cannot launch new tasks");
+ }
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ CompletableFuture<T> cf = CompletableFuture.supplyAsync(() -> {
+ try {
+ return withCurrent(this, () -> AsyncContext.withSnapshot(contextSnapshot, () -> body.call()));
+ } catch (CompletionException ce) {
+ throw ce;
+ } catch (Throwable t) {
+ throw new CompletionException(t);
+ }
+ }, executor);
+ children.add(cf);
+ if (failFast) {
+ cf.whenComplete((v, err) -> {
+ if (err != null && !closed.get()) {
+ cancelAll();
+ }
+ });
+ }
+ return GroovyPromise.of(cf);
+ }
+
+ /**
+ * Returns the number of child tasks launched within this scope.
+ *
+ * @return the child task count
+ */
+ public int getChildCount() {
+ return children.size();
+ }
+
+ /**
+ * Cancels all child tasks. Idempotent — safe to call multiple times.
+ * <p>
+ * Cancels each child via {@link CompletableFuture#cancel(boolean)}.
+ * Does <em>not</em> close the scope — the scope remains open so that
+ * {@link #close()} can still join all children and collect errors.
+ */
+ public void cancelAll() {
+ for (CompletableFuture<?> child : children) {
+ child.cancel(true);
+ }
+ }
+
+ /**
+ * Waits for all child tasks to complete, then closes the scope.
+ * <p>
+ * If any child failed, the first failure is rethrown with subsequent
+ * failures as {@linkplain Throwable#addSuppressed(Throwable) suppressed}
+ * exceptions. Cancelled tasks are silently ignored.
+ * <p>
+ * This method is idempotent: only the first invocation waits for
+ * children; subsequent calls are no-ops.
+ */
+ @Override
+ public void close() {
+ if (!closed.compareAndSet(false, true)) return;
+ Throwable firstError = null;
+ for (CompletableFuture<?> child : children) {
+ try {
+ child.join();
+ } catch (CancellationException ignored) {
+ // Cancelled tasks are silently ignored
+ } catch (CompletionException e) {
+ Throwable cause = AsyncSupport.deepUnwrap(e);
+ if (cause instanceof CancellationException) {
+ continue;
+ }
+ if (firstError == null) {
+ firstError = cause;
+ } else {
+ firstError.addSuppressed(cause);
+ }
+ } catch (Exception e) {
+ if (firstError == null) {
+ firstError = e;
+ } else {
+ firstError.addSuppressed(e);
+ }
+ }
+ }
+ if (firstError != null) {
+ if (firstError instanceof RuntimeException re) throw re;
+ if (firstError instanceof Error err) throw err;
+ throw new RuntimeException(firstError);
+ }
+ }
+
+ /**
+ * Convenience method that creates a scope, executes the given closure
+ * within it, and ensures the scope is closed on exit.
+ * <p>
+ * The closure receives the {@code AsyncScope} as its argument and can
+ * launch child tasks via {@link #async(Closure)}. The scope is
+ * automatically closed (and all children awaited) when the closure
+ * returns or throws.
+ *
+ * <pre>
+ * def result = AsyncScope.withScope { scope ->
+ * def a = scope.async { computeA() }
+ * def b = scope.async { computeB() }
+ * return [await(a), await(b)]
+ * }
+ * </pre>
+ *
+ * @param body the closure to execute within the scope
+ * @param <T> the result type
+ * @return the closure's return value
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T withScope(Closure<T> body) {
+ return withScope(AsyncSupport.getExecutor(), body);
+ }
+
+ /**
+ * Convenience method that creates a scope with the given executor,
+ * executes the closure, and ensures the scope is closed on exit.
+ *
+ * @param executor the executor for child tasks
+ * @param body the closure to execute within the scope
+ * @param <T> the result type
+ * @return the closure's return value
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T withScope(Executor executor, Closure<T> body) {
+ Objects.requireNonNull(body, "body must not be null");
+ try (AsyncScope scope = new AsyncScope(executor)) {
+ return withCurrent(scope, () -> body.call(scope));
+ }
+ }
+}
diff --git a/src/main/java/groovy/concurrent/AsyncStream.java b/src/main/java/groovy/concurrent/AsyncStream.java
new file mode 100644
index 0000000000..5c2176dfe2
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncStream.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+/**
+ * Asynchronous iteration abstraction for producing and consuming
+ * values that arrive over time.
+ * <p>
+ * Used with the {@code for await} syntax:
+ * <pre>
+ * for await (item in asyncStream) {
+ * process(item)
+ * }
+ * </pre>
+ * <p>
+ * An {@code AsyncStream} can be produced in several ways:
+ * <ul>
+ * <li>Using {@code yield return} inside an {@code async} method or closure
+ * to create a generator-style stream</li>
+ * <li>Adapting JDK {@link java.util.concurrent.Flow.Publisher} instances
+ * (supported out of the box by the built-in adapter)</li>
+ * <li>Viewing a {@link Channel} as a stream via {@link Channel#asStream()} —
+ * each {@code moveNext()} call maps to a {@code receive()}, and
+ * {@link ChannelClosedException} is translated to end-of-stream.
+ * This conversion is registered automatically, so {@code for await (item in channel)}
+ * works out of the box.</li>
+ * <li>Adapting third-party reactive types (Reactor {@code Flux}, RxJava
+ * {@code Observable}) via {@link AwaitableAdapter}</li>
+ * </ul>
+ *
+ * @param <T> the element type
+ * @see AwaitableAdapter
+ * @see AwaitableAdapterRegistry
+ * @since 6.0.0
+ */
+public interface AsyncStream<T> extends AutoCloseable {
+
+ /**
+ * Asynchronously advances to the next element. Returns an {@link Awaitable}
+ * that completes with {@code true} if an element is available, or
+ * {@code false} if the stream is exhausted.
+ */
+ Awaitable<Boolean> moveNext();
+
+ /**
+ * Returns the current element. Must only be called after {@link #moveNext()}
+ * has completed with {@code true}.
+ */
+ T getCurrent();
+
+ /**
+ * Closes the stream and releases any associated resources.
+ * <p>
+ * The default implementation is a no-op. Implementations that bridge to
+ * generators, publishers, or other resource-owning sources may override
+ * this to propagate cancellation upstream. Compiler-generated
+ * {@code for await} loops invoke {@code close()} automatically from a
+ * {@code finally} block, including on early {@code break}, {@code return},
+ * and exceptional exit.
+ *
+ * @since 6.0.0
+ */
+ @Override
+ default void close() {
+ }
+
+ /**
+ * Converts the given source to an {@code AsyncStream}.
+ * <p>
+ * If the source is already an {@code AsyncStream}, it is returned as-is.
+ * Otherwise, the {@link AwaitableAdapterRegistry} is consulted to find a
+ * suitable adapter. Built-in adapters handle {@link Iterable} and
+ * {@link java.util.Iterator}; the auto-discovered {@code FlowPublisherAdapter}
+ * handles {@link java.util.concurrent.Flow.Publisher}; third-party frameworks
+ * can register additional adapters via the registry.
+ * <p>
+ * This is the recommended entry point for converting external collection or
+ * reactive types to {@code AsyncStream}:
+ * <pre>
+ * AsyncStream<String> stream = AsyncStream.from(myList)
+ * AsyncStream<Integer> stream2 = AsyncStream.from(myFlowPublisher)
+ * </pre>
+ *
+ * @param source the source object; must not be {@code null}
+ * @param <T> the element type
+ * @return an async stream backed by the source
+ * @throws IllegalArgumentException if {@code source} is {@code null}
+ * or no adapter supports the source type
+ * @see AwaitableAdapterRegistry#toAsyncStream(Object)
+ * @since 6.0.0
+ */
+ @SuppressWarnings("unchecked")
+ static <T> AsyncStream<T> from(Object source) {
+ return AwaitableAdapterRegistry.toAsyncStream(source);
+ }
+
+ /**
+ * Returns an empty {@code AsyncStream} that completes immediately.
+ */
+ @SuppressWarnings("unchecked")
+ static <T> AsyncStream<T> empty() {
+ return (AsyncStream<T>) EMPTY;
+ }
+
+ /**
+ * Cached awaitable for {@code moveNext()} returning {@code true}.
+ * Eliminates per-call allocation on the hot path. Shared by all
+ * {@code AsyncStream} implementations (e.g. via
+ * {@link org.apache.groovy.runtime.async.AbstractAsyncStream AbstractAsyncStream}).
+ */
+ Awaitable<Boolean> MOVE_NEXT_TRUE = Awaitable.of(Boolean.TRUE);
+
+ /**
+ * Cached awaitable for {@code moveNext()} returning {@code false}.
+ * Eliminates per-call allocation on the stream-end path.
+ */
+ Awaitable<Boolean> MOVE_NEXT_FALSE = Awaitable.of(Boolean.FALSE);
+
+ /**
+ * Singleton empty stream instance.
+ * <p>
+ * This is an implementation detail backing {@link #empty()}.
+ * User code should call {@code AsyncStream.empty()} rather than
+ * referencing this field directly.
+ */
+ AsyncStream<Object> EMPTY = new AsyncStream<>() {
+ @Override public Awaitable<Boolean> moveNext() { return MOVE_NEXT_FALSE; }
+ @Override public Object getCurrent() { return null; }
+ };
+}
diff --git a/src/main/java/groovy/concurrent/AwaitResult.java b/src/main/java/groovy/concurrent/AwaitResult.java
new file mode 100644
index 0000000000..44d39e5704
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitResult.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * Represents the outcome of an asynchronous computation that may have
+ * succeeded or failed. Returned by
+ * {@link Awaitable#allSettled(Object...) Awaitable.allSettled()} to
+ * report each individual task's result without short-circuiting on
+ * failure.
+ * <p>
+ * An {@code AwaitResult} is either a {@linkplain #isSuccess() success}
+ * carrying a value, or a {@linkplain #isFailure() failure} carrying a
+ * {@link Throwable}.
+ *
+ * @param <T> the value type
+ * @since 6.0.0
+ */
+public final class AwaitResult<T> {
+
+ private final T value;
+ private final Throwable error;
+ private final boolean success;
+
+ private AwaitResult(T value, Throwable error, boolean success) {
+ this.value = value;
+ this.error = error;
+ this.success = success;
+ }
+
+ /**
+ * Creates a successful result with the given value.
+ *
+ * @param value the computation result (may be {@code null})
+ * @param <T> the value type
+ * @return a success result wrapping the value
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> AwaitResult<T> success(Object value) {
+ return new AwaitResult<>((T) value, null, true);
+ }
+
+ /**
+ * Creates a failure result with the given exception.
+ *
+ * @param error the exception that caused the failure; must not be {@code null}
+ * @param <T> the value type (never actually used, since the result is a failure)
+ * @return a failure result wrapping the exception
+ * @throws NullPointerException if {@code error} is {@code null}
+ */
+ public static <T> AwaitResult<T> failure(Throwable error) {
+ return new AwaitResult<>(null, Objects.requireNonNull(error), false);
+ }
+
+ /** Returns {@code true} if this result represents a successful completion. */
+ public boolean isSuccess() {
+ return success;
+ }
+
+ /** Returns {@code true} if this result represents a failed completion. */
+ public boolean isFailure() {
+ return !success;
+ }
+
+ /**
+ * Returns the value if successful.
+ *
+ * @return the computation result
+ * @throws IllegalStateException if this result represents a failure
+ */
+ public T getValue() {
+ if (!success) throw new IllegalStateException("Cannot get value from a failed result");
+ return value;
+ }
+
+ /**
+ * Returns the exception if failed.
+ *
+ * @return the exception that caused the failure
+ * @throws IllegalStateException if this result represents a success
+ */
+ public Throwable getError() {
+ if (success) throw new IllegalStateException("Cannot get error from a successful result");
+ return error;
+ }
+
+ /**
+ * Returns the value if successful, or applies the given function to
+ * the error to produce a fallback value.
+ */
+ public T getOrElse(Function<Throwable, ? extends T> fallback) {
+ return success ? value : fallback.apply(error);
+ }
+
+ /**
+ * Returns a human-readable representation of this result:
+ * {@code AwaitResult.Success[value]} or {@code AwaitResult.Failure[error]}.
+ */
+ @Override
+ public String toString() {
+ return success
+ ? "AwaitResult.Success[" + value + "]"
+ : "AwaitResult.Failure[" + error + "]";
+ }
+}
diff --git a/src/main/java/groovy/concurrent/Awaitable.java b/src/main/java/groovy/concurrent/Awaitable.java
new file mode 100644
index 0000000000..37488abd2f
--- /dev/null
+++ b/src/main/java/groovy/concurrent/Awaitable.java
@@ -0,0 +1,578 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+import groovy.lang.Closure;
+import org.apache.groovy.runtime.async.AsyncSupport;
+import org.apache.groovy.runtime.async.GroovyPromise;
+
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Core abstraction for asynchronous computations in Groovy.
+ * <p>
+ * {@code Awaitable} represents a computation that may not have completed
+ * yet. It serves as both an instance type (the result of {@code async}
+ * methods and the input to {@code await}) and a static API surface for
+ * combinators, factories, and configuration.
+ * <p>
+ * <b>Static combinators</b> (all return {@code Awaitable}, suitable for use
+ * with {@code await}):
+ * <ul>
+ * <li>{@link #all(Object...) Awaitable.all(a, b, c)} — waits for all
+ * tasks to complete, returning their results in order</li>
+ * <li>{@link #any(Object...) Awaitable.any(a, b)} — returns the result
+ * of the first task to complete</li>
+ * <li>{@link #allSettled(Object...) Awaitable.allSettled(a, b)} — waits
+ * for all tasks to settle (succeed or fail), returning an
+ * {@link AwaitResult} list</li>
+ * <li>{@link #delay(long) Awaitable.delay(ms)} — completes after a
+ * non-blocking delay</li>
+ * <li>{@link #orTimeoutMillis(Object, long) Awaitable.orTimeoutMillis(task, ms)} —
+ * fails with {@link java.util.concurrent.TimeoutException} if the task
+ * does not complete in time</li>
+ * <li>{@link #completeOnTimeoutMillis(Object, Object, long)
+ * Awaitable.completeOnTimeoutMillis(task, fallback, ms)} —
+ * uses a fallback value when the timeout expires</li>
+ * </ul>
+ * <p>
+ * <b>Static factories and conversion:</b>
+ * <ul>
+ * <li>{@link #from(Object) Awaitable.from(source)} — converts any supported
+ * async type (CompletableFuture, CompletionStage, Future, Flow.Publisher, etc.)
+ * to an {@code Awaitable}</li>
+ * <li>{@link #of(Object) Awaitable.of(value)} — wraps an already-available
+ * value in a completed {@code Awaitable}</li>
+ * <li>{@link #failed(Throwable) Awaitable.failed(error)} — wraps an
+ * exception in an immediately-failed {@code Awaitable}</li>
+ * <li>{@link #go(Closure) Awaitable.go { ... }} — lightweight task spawn</li>
+ * <li>{@link #channel()} / {@link #channel(int)} — buffered or unbuffered channels</li>
+ * </ul>
+ * <p>
+ * <b>Instance continuations</b> provide ergonomic composition without exposing
+ * raw {@link CompletableFuture} APIs:
+ * <ul>
+ * <li>{@link #then(Function)} and {@link #thenCompose(Function)} for success chaining</li>
+ * <li>{@link #thenAccept(Consumer)} for side-effecting continuations</li>
+ * <li>{@link #exceptionally(Function)}, {@link #whenComplete(BiConsumer)},
+ * and {@link #handle(BiFunction)} for failure/completion handling</li>
+ * <li>{@link #orTimeout(long, TimeUnit)} and
+ * {@link #completeOnTimeout(Object, long, TimeUnit)} for deadline composition</li>
+ * </ul>
+ * <p>
+ * Third-party frameworks (RxJava, Reactor, etc.) can integrate by registering
+ * an {@link AwaitableAdapter} via {@link AwaitableAdapterRegistry}.
+ * <p>
+ * The default implementation, {@link GroovyPromise}, delegates to
+ * {@link CompletableFuture} but users never need to depend on that detail.
+ *
+ * @param <T> the result type
+ * @see GroovyPromise
+ * @see AwaitableAdapter
+ * @since 6.0.0
+ */
+public interface Awaitable<T> {
+
+ /**
+ * Blocks until the computation completes and returns the result.
+ *
+ * @return the computed result
+ * @throws InterruptedException if the calling thread is interrupted while waiting
+ * @throws ExecutionException if the computation completed exceptionally
+ */
+ T get() throws InterruptedException, ExecutionException;
+
+ /**
+ * Blocks until the computation completes or the timeout expires.
+ *
+ * @param timeout the maximum time to wait
+ * @param unit the time unit of the timeout argument
+ * @return the computed result
+ * @throws InterruptedException if the calling thread is interrupted while waiting
+ * @throws ExecutionException if the computation completed exceptionally
+ * @throws TimeoutException if the wait timed out
+ */
+ T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
+
+ /**
+ * Returns {@code true} if the computation has completed (normally,
+ * exceptionally, or via cancellation).
+ *
+ * @return {@code true} if complete
+ */
+ boolean isDone();
+
+ /**
+ * Attempts to cancel the computation. If the computation has not yet started
+ * or is still running, it will be cancelled with a {@link CancellationException}.
+ *
+ * @return {@code true} if the computation was successfully cancelled
+ */
+ boolean cancel();
+
+ /**
+ * Returns {@code true} if the computation was cancelled before completing normally.
+ *
+ * @return {@code true} if cancelled
+ */
+ boolean isCancelled();
+
+ /**
+ * Returns {@code true} if this computation completed exceptionally
+ * (including cancellation).
+ *
+ * @return {@code true} if completed with an error or cancellation
+ */
+ boolean isCompletedExceptionally();
+
+ /**
+ * Returns a new {@code Awaitable} whose result is obtained by applying the
+ * given function to this awaitable's result when it completes.
+ *
+ * @param fn the mapping function
+ * @param <U> the type of the mapped result
+ * @return a new awaitable holding the mapped result
+ */
+ <U> Awaitable<U> then(Function<? super T, ? extends U> fn);
+
+ /**
+ * Returns a new {@code Awaitable} produced by applying the given async
+ * function to this awaitable's result, flattening the nested {@code Awaitable}.
+ * This is the monadic {@code flatMap} operation for awaitables.
+ *
+ * @param fn the async mapping function that returns an {@code Awaitable}
+ * @param <U> the type of the inner awaitable's result
+ * @return a new awaitable holding the inner result
+ */
+ <U> Awaitable<U> thenCompose(Function<? super T, ? extends Awaitable<U>> fn);
+
+ /**
+ * Returns a new {@code Awaitable} that, when this one completes normally,
+ * invokes the given consumer and completes with {@code null}.
+ * <p>
+ * Useful at API boundaries where you need to attach logging, metrics, or
+ * other side effects without blocking for the result.
+ *
+ * @param action the side-effecting consumer to invoke on success
+ * @return a new awaitable that completes after the action runs
+ * @since 6.0.0
+ */
+ default Awaitable<Void> thenAccept(Consumer<? super T> action) {
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ return GroovyPromise.of(toCompletableFuture().thenAccept(value ->
+ AsyncContext.withSnapshot(contextSnapshot, () -> {
+ action.accept(value);
+ return null;
+ })));
+ }
+
+ /**
+ * Returns a new {@code Awaitable} that, if this one completes exceptionally,
+ * applies the given function to the exception to produce a recovery value.
+ * The throwable passed to the function is deeply unwrapped to strip JDK
+ * wrapper layers.
+ *
+ * @param fn the recovery function
+ * @return a new awaitable that recovers from failures
+ */
+ Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn);
+
+ /**
+ * Returns a new {@code Awaitable} that invokes the given action when this
+ * computation completes, regardless of success or failure.
+ * <p>
+ * The supplied throwable is transparently unwrapped so handlers see the
+ * original failure rather than {@link ExecutionException} /
+ * {@link java.util.concurrent.CompletionException} wrappers.
+ *
+ * @param action the completion callback receiving the result or failure
+ * @return a new awaitable that completes with the original result
+ * @since 6.0.0
+ */
+ default Awaitable<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) {
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ return GroovyPromise.of(toCompletableFuture().whenComplete((value, error) ->
+ AsyncContext.withSnapshot(contextSnapshot, () -> {
+ action.accept(value, error == null ? null : AsyncSupport.deepUnwrap(error));
+ return null;
+ })));
+ }
+
+ /**
+ * Returns a new {@code Awaitable} that handles both the successful and the
+ * exceptional completion paths in a single continuation.
+ * <p>
+ * The supplied throwable is transparently unwrapped so the handler sees the
+ * original failure. This provides a single place for success/failure
+ * projection, combining both paths in one callback.
+ *
+ * @param fn the handler receiving either the value or the failure
+ * @param <U> the projected result type
+ * @return a new awaitable holding the handler's result
+ * @since 6.0.0
+ */
+ default <U> Awaitable<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) {
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ return GroovyPromise.of(toCompletableFuture().handle((value, error) ->
+ AsyncContext.withSnapshot(contextSnapshot,
+ () -> fn.apply(value, error == null ? null : AsyncSupport.deepUnwrap(error)))));
+ }
+
+ /**
+ * Returns a new {@code Awaitable} that fails with {@link TimeoutException}
+ * if this computation does not complete within the specified milliseconds.
+ * <p>
+ * Unlike {@link #get(long, TimeUnit)}, this is a non-blocking, composable
+ * timeout combinator: it returns another {@code Awaitable} that can itself
+ * be awaited, chained, or passed to {@link #all(Object...)} / {@link #any(Object...)}.
+ *
+ * @param timeoutMillis the timeout duration in milliseconds
+ * @return a new awaitable with timeout semantics
+ * @since 6.0.0
+ */
+ default Awaitable<T> orTimeoutMillis(long timeoutMillis) {
+ return Awaitable.orTimeout(this, timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Returns a new {@code Awaitable} that fails with {@link TimeoutException}
+ * if this computation does not complete within the specified duration.
+ *
+ * @param duration the timeout duration
+ * @param unit the time unit
+ * @return a new awaitable with timeout semantics
+ * @since 6.0.0
+ */
+ default Awaitable<T> orTimeout(long duration, TimeUnit unit) {
+ return Awaitable.orTimeout(this, duration, unit);
+ }
+
+ /**
+ * Returns a new {@code Awaitable} that completes with the supplied fallback
+ * value if this computation does not finish before the timeout expires.
+ *
+ * @param fallback the value to use when the timeout expires
+ * @param timeoutMillis the timeout duration in milliseconds
+ * @return a new awaitable that yields either the original result or the fallback
+ * @since 6.0.0
+ */
+ default Awaitable<T> completeOnTimeoutMillis(T fallback, long timeoutMillis) {
+ return Awaitable.completeOnTimeout(this, fallback, timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Returns a new {@code Awaitable} that completes with the supplied fallback
+ * value if this computation does not finish before the timeout expires.
+ *
+ * @param fallback the value to use when the timeout expires
+ * @param duration the timeout duration
+ * @param unit the time unit
+ * @return a new awaitable that yields either the original result or the fallback
+ * @since 6.0.0
+ */
+ default Awaitable<T> completeOnTimeout(T fallback, long duration, TimeUnit unit) {
+ return Awaitable.completeOnTimeout(this, fallback, duration, unit);
+ }
+
+ /**
+ * Converts this {@code Awaitable} to a JDK {@link CompletableFuture}
+ * for interoperability with APIs that require it.
+ *
+ * @return a {@code CompletableFuture} representing this computation
+ */
+ CompletableFuture<T> toCompletableFuture();
+
+ // ---- Static factories ----
+
+ /**
+ * Converts the given source to an {@code Awaitable}.
+ * <p>
+ * If the source is already an {@code Awaitable}, it is returned as-is.
+ * Otherwise, the {@link AwaitableAdapterRegistry} is consulted to find a
+ * suitable adapter. Built-in adapters handle {@link CompletableFuture},
+ * {@link java.util.concurrent.CompletionStage}, {@link java.util.concurrent.Future},
+ * and {@link java.util.concurrent.Flow.Publisher}; third-party frameworks
+ * can register additional adapters via the registry.
+ * <p>
+ * This is the recommended entry point for converting external async types
+ * to {@code Awaitable}:
+ * <pre>
+ * Awaitable<String> aw = Awaitable.from(someCompletableFuture)
+ * Awaitable<Integer> aw2 = Awaitable.from(someReactorMono)
+ * </pre>
+ *
+ * @param source the source object; must not be {@code null}
+ * @param <T> the result type
+ * @return an awaitable backed by the source
+ * @throws IllegalArgumentException if {@code source} is {@code null}
+ * or no adapter supports the source type
+ * @see AwaitableAdapterRegistry#toAwaitable(Object)
+ * @since 6.0.0
+ */
+ static <T> Awaitable<T> from(Object source) {
+ return AwaitableAdapterRegistry.toAwaitable(source);
+ }
+
+ /**
+ * Returns an already-completed {@code Awaitable} with the given value.
+ * Useful for returning a pre-computed result from an API that requires
+ * an {@code Awaitable} return type.
+ *
+ * @param value the result value (may be {@code null})
+ * @param <T> the result type
+ * @return a completed awaitable
+ */
+ static <T> Awaitable<T> of(T value) {
+ return new GroovyPromise<>(CompletableFuture.completedFuture(value));
+ }
+
+ /**
+ * Returns an already-failed {@code Awaitable} with the given exception.
+ * Useful for signaling a synchronous error from an API that returns
+ * {@code Awaitable}.
+ */
+ static <T> Awaitable<T> failed(Throwable error) {
+ return new GroovyPromise<>(CompletableFuture.failedFuture(error));
+ }
+
+ /**
+ * Spawns a lightweight task.
+ * <p>
+ * Inside an {@link AsyncScope}, the new task is attached to the current
+ * structured scope. Outside a scope it behaves like a detached async task.
+ *
+ * @param closure the task body
+ * @param <T> the result type
+ * @return an awaitable representing the spawned task
+ * @since 6.0.0
+ */
+ static <T> Awaitable<T> go(Closure<T> closure) {
+ return AsyncSupport.go(closure);
+ }
+
+ /**
+ * Creates an unbuffered channel.
+ *
+ * @param <T> the payload type
+ * @return a new unbuffered channel
+ * @since 6.0.0
+ */
+ static <T> Channel<T> channel() {
+ return AsyncSupport.channel();
+ }
+
+ /**
+ * Creates a channel with the given buffer capacity.
+ *
+ * @param capacity the buffer capacity
+ * @param <T> the payload type
+ * @return a new channel
+ * @since 6.0.0
+ */
+ static <T> Channel<T> channel(int capacity) {
+ return AsyncSupport.channel(capacity);
+ }
+
+ // ---- Combinators ----
+
+ /**
+ * Returns an {@code Awaitable} that completes when all given sources
+ * complete, with a list of their results in order.
+ * <p>
+ * Unlike blocking APIs, this returns immediately and the caller should
+ * {@code await} the result:
+ * <pre>
+ * def results = await Awaitable.all(task1, task2, task3)
+ * </pre>
+ *
+ * @param sources the awaitables, futures, or adapted objects to wait for
+ * @return an awaitable that resolves to a list of results
+ */
+ static Awaitable<List<Object>> all(Object... sources) {
+ return AsyncSupport.allAsync(sources);
+ }
+
+ /**
+ * Returns an {@code Awaitable} that completes when the first of the given
+ * sources completes, with the winner's result.
+ * <pre>
+ * def winner = await Awaitable.any(task1, task2)
+ * </pre>
+ *
+ * @param sources the awaitables to race
+ * @return an awaitable that resolves to the first completed result
+ */
+ @SuppressWarnings("unchecked")
+ static <T> Awaitable<T> any(Object... sources) {
+ return AsyncSupport.anyAsync(sources);
+ }
+
+ /**
+ * Returns an {@code Awaitable} that completes when all given sources
+ * have settled (succeeded or failed), with a list of {@link AwaitResult}
+ * objects describing each outcome.
+ * <p>
+ * Never throws for individual failures; they are captured in the result list.
+ * <pre>
+ * def results = await Awaitable.allSettled(task1, task2)
+ * results.each { println it.success ? it.value : it.error }
+ * </pre>
+ *
+ * @param sources the awaitables to settle
+ * @return an awaitable that resolves to a list of settled results
+ */
+ static Awaitable<List<AwaitResult<Object>>> allSettled(Object... sources) {
+ return AsyncSupport.allSettledAsync(sources);
+ }
+
+ // ---- Delay ----
+
+ /**
+ * Returns an {@code Awaitable} that completes after the specified delay.
+ * Does not block any thread; the delay is handled by a scheduled executor.
+ * <pre>
+ * await Awaitable.delay(1000) // pause for 1 second
+ * </pre>
+ *
+ * @param milliseconds the delay in milliseconds (must be ≥ 0)
+ * @return an awaitable that completes after the delay
+ */
+ static Awaitable<Void> delay(long milliseconds) {
+ return AsyncSupport.delay(milliseconds);
+ }
+
+ /**
+ * Returns an {@code Awaitable} that completes after the specified delay.
+ *
+ * @param duration the delay duration (must be ≥ 0)
+ * @param unit the time unit
+ * @return an awaitable that completes after the delay
+ */
+ static Awaitable<Void> delay(long duration, TimeUnit unit) {
+ return AsyncSupport.delay(duration, unit);
+ }
+
+ // ---- Timeout combinators ----
+
+ /**
+ * Adapts the given source to an {@code Awaitable} and applies a non-blocking
+ * fail-fast timeout. Returns a new awaitable that fails with
+ * {@link TimeoutException} if the source does not complete before the
+ * deadline elapses.
+ * <p>
+ * The source may be a Groovy {@link Awaitable}, a JDK
+ * {@link CompletableFuture}/{@link java.util.concurrent.CompletionStage},
+ * or any type supported by {@link AwaitableAdapterRegistry}. This provides
+ * a concise timeout combinator that returns another awaitable rather than
+ * requiring structural timeout blocks.
+ *
+ * @param source the async source to time out
+ * @param timeoutMillis the timeout duration in milliseconds
+ * @param <T> the result type
+ * @return a new awaitable with timeout semantics
+ * @since 6.0.0
+ */
+ static <T> Awaitable<T> orTimeoutMillis(Object source, long timeoutMillis) {
+ return AsyncSupport.orTimeout(source, timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Adapts the given source and applies a non-blocking fail-fast timeout
+ * with explicit {@link TimeUnit}.
+ *
+ * @param source the async source to time out
+ * @param duration the timeout duration
+ * @param unit the time unit
+ * @param <T> the result type
+ * @return a new awaitable with timeout semantics
+ * @since 6.0.0
+ */
+ static <T> Awaitable<T> orTimeout(Object source, long duration, TimeUnit unit) {
+ return AsyncSupport.orTimeout(source, duration, unit);
+ }
+
+ /**
+ * Adapts the given source and returns a new awaitable that yields the
+ * supplied fallback value if the timeout expires first.
+ *
+ * @param source the async source to wait for
+ * @param fallback the fallback value to use on timeout
+ * @param timeoutMillis the timeout duration in milliseconds
+ * @param <T> the result type
+ * @return a new awaitable yielding either the original result or the fallback
+ * @since 6.0.0
+ */
+ static <T> Awaitable<T> completeOnTimeoutMillis(Object source, T fallback, long timeoutMillis) {
+ return AsyncSupport.completeOnTimeout(source, fallback, timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Adapts the given source and returns a new awaitable that yields the
+ * supplied fallback value if the timeout expires first.
+ *
+ * @param source the async source to wait for
+ * @param fallback the fallback value to use on timeout
+ * @param duration the timeout duration
+ * @param unit the time unit
+ * @param <T> the result type
+ * @return a new awaitable yielding either the original result or the fallback
+ * @since 6.0.0
+ */
+ static <T> Awaitable<T> completeOnTimeout(Object source, T fallback, long duration, TimeUnit unit) {
+ return AsyncSupport.completeOnTimeout(source, fallback, duration, unit);
+ }
+
+ // ---- Executor configuration ----
+
+ /**
+ * Returns the current executor used for {@code async} operations.
+ * On JDK 21+, this defaults to a virtual-thread-per-task executor.
+ * On earlier JDKs, a cached daemon thread pool is used.
+ */
+ static Executor getExecutor() {
+ return AsyncSupport.getExecutor();
+ }
+
+ /**
+ * Sets the executor to use for {@code async} operations.
+ * Pass {@code null} to reset to the default executor.
+ *
+ * @param executor the executor to use, or {@code null} for default
+ */
+ static void setExecutor(Executor executor) {
+ AsyncSupport.setExecutor(executor);
+ }
+
+ /**
+ * Returns {@code true} if the running JVM supports virtual threads (JDK 21+).
+ */
+ static boolean isVirtualThreadsAvailable() {
+ return AsyncSupport.isVirtualThreadsAvailable();
+ }
+}
diff --git a/src/main/java/groovy/concurrent/AwaitableAdapter.java b/src/main/java/groovy/concurrent/AwaitableAdapter.java
new file mode 100644
index 0000000000..148440a595
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitableAdapter.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+/**
+ * Service Provider Interface (SPI) for adapting third-party asynchronous types
+ * to Groovy's {@link Awaitable} and {@link AsyncStream} abstractions.
+ * <p>
+ * Implementations are discovered automatically via {@link java.util.ServiceLoader}.
+ * To register an adapter, create a file
+ * {@code META-INF/services/groovy.concurrent.AwaitableAdapter} containing the
+ * fully-qualified class name of your implementation.
+ * <p>
+ * <b>Example – RxJava adapter:</b>
+ * <pre>
+ * public class RxJavaAwaitableAdapter implements AwaitableAdapter {
+ * public boolean supportsAwaitable(Class<?> type) {
+ * return io.reactivex.rxjava3.core.Single.class.isAssignableFrom(type);
+ * }
+ * public <T> Awaitable<T> toAwaitable(Object source) {
+ * Single<T> single = (Single<T>) source;
+ * return new org.apache.groovy.runtime.async.GroovyPromise<>(single.toCompletionStage().toCompletableFuture());
+ * }
+ * // ... supportsAsyncStream / toAsyncStream for Observable, Flowable, etc.
+ * }
+ * </pre>
+ *
+ * @see AwaitableAdapterRegistry
+ * @since 6.0.0
+ */
+public interface AwaitableAdapter {
+
+ /**
+ * Returns {@code true} if this adapter can convert instances of the given
+ * type to {@link Awaitable}.
+ */
+ boolean supportsAwaitable(Class<?> type);
+
+ /**
+ * Converts the given source object to an {@link Awaitable}.
+ * Called only when {@link #supportsAwaitable} returned {@code true}.
+ */
+ <T> Awaitable<T> toAwaitable(Object source);
+
+ /**
+ * Returns {@code true} if this adapter can convert instances of the given
+ * type to {@link AsyncStream}. Defaults to {@code false}; override if
+ * this adapter supports multi-value stream conversion (e.g., for Reactor
+ * {@code Flux} or RxJava {@code Observable}).
+ *
+ * @param type the source class to check
+ * @return {@code true} if this adapter can produce an {@code AsyncStream}
+ */
+ default boolean supportsAsyncStream(Class<?> type) {
+ return false;
+ }
+
+ /**
+ * Converts the given source object to an {@link AsyncStream}.
+ * Called only when {@link #supportsAsyncStream} returned {@code true}.
+ *
+ * @param source the source object (e.g., a Reactor {@code Flux})
+ * @param <T> the element type
+ * @return an async stream backed by the source
+ * @throws UnsupportedOperationException if not overridden (default)
+ */
+ default <T> AsyncStream<T> toAsyncStream(Object source) {
+ throw new UnsupportedOperationException("AsyncStream conversion not supported by " + getClass().getName());
+ }
+}
diff --git a/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java b/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java
new file mode 100644
index 0000000000..e3921763d9
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java
@@ -0,0 +1,350 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+import org.apache.groovy.runtime.async.GroovyPromise;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.ServiceLoader;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+
+/**
+ * Central registry for {@link AwaitableAdapter} instances.
+ * <p>
+ * On class-load, adapters are discovered via {@link ServiceLoader} from
+ * {@code META-INF/services/groovy.concurrent.AwaitableAdapter}. A built-in
+ * adapter is always present as the lowest-priority fallback, handling:
+ * <ul>
+ * <li>{@link CompletableFuture} and {@link CompletionStage}</li>
+ * <li>{@link Future} (adapted via a blocking wrapper)</li>
+ * </ul>
+ * <p>
+ * JDK {@link java.util.concurrent.Flow.Publisher} support is provided by
+ * the separately registered
+ * {@link org.apache.groovy.runtime.async.FlowPublisherAdapter}, which is
+ * auto-discovered via ServiceLoader. Third-party frameworks (Reactor,
+ * RxJava, etc.) may register their own higher-priority adapters.
+ * <p>
+ * Additional adapters can be registered at runtime via {@link #register}.
+ *
+ * @see AwaitableAdapter
+ * @since 6.0.0
+ */
+public class AwaitableAdapterRegistry {
+
+ private static final List<AwaitableAdapter> ADAPTERS = new CopyOnWriteArrayList<>();
+
+ /**
+ * Per-class adapter cache for {@link #toAwaitable(Object)}.
+ * Uses {@link ClassValue} for lock-free, GC-friendly per-class memoization
+ * that does not prevent class unloading — superior to
+ * {@code ConcurrentHashMap<Class<?>, V>} which holds strong references
+ * that can cause ClassLoader leaks in container environments.
+ * <p>
+ * Rebuilt via volatile reference swap when adapters are registered or
+ * removed (an extremely low-frequency operation).
+ */
+ private static volatile ClassValue<AwaitableAdapter> awaitableCache = buildAwaitableCache();
+
+ /**
+ * Per-class adapter cache for {@link #toAsyncStream(Object)}.
+ * Same caching strategy as {@link #awaitableCache}.
+ */
+ private static volatile ClassValue<AwaitableAdapter> streamCache = buildStreamCache();
+
+ /**
+ * Optional executor supplier for blocking Future adaptation, to avoid
+ * starving the common pool. Defaults to null; when set, the provided
+ * executor is used instead of {@code CompletableFuture.runAsync}'s default.
+ */
+ private static volatile Executor blockingExecutor;
+
+ static {
+ // SPI-discovered adapters
+ for (AwaitableAdapter adapter : ServiceLoader.load(AwaitableAdapter.class)) {
+ ADAPTERS.add(adapter);
+ }
+ // Built-in fallback (lowest priority)
+ ADAPTERS.add(new BuiltInAdapter());
+ }
+
+ private AwaitableAdapterRegistry() { }
+
+ /**
+ * Registers an adapter with higher priority than existing ones.
+ * <p>
+ * Invalidates the per-class adapter caches so that subsequent lookups
+ * re-evaluate adapter priority order.
+ *
+ * @param adapter the adapter to register; must not be {@code null}
+ * @return an {@link AutoCloseable} that, when closed, removes this adapter
+ * from the registry. Useful for test isolation.
+ */
+ public static AutoCloseable register(AwaitableAdapter adapter) {
+ Objects.requireNonNull(adapter, "adapter must not be null");
+ ADAPTERS.add(0, adapter);
+ invalidateCaches();
+ return () -> { ADAPTERS.remove(adapter); invalidateCaches(); };
+ }
+
+ /**
+ * Removes the given adapter from the registry.
+ * <p>
+ * Invalidates the per-class adapter caches so that subsequent lookups
+ * no longer consider the removed adapter.
+ *
+ * @param adapter the adapter to remove
+ * @return {@code true} if the adapter was found and removed
+ */
+ public static boolean unregister(AwaitableAdapter adapter) {
+ Objects.requireNonNull(adapter, "adapter must not be null");
+ boolean removed = ADAPTERS.remove(adapter);
+ if (removed) {
+ invalidateCaches();
+ }
+ return removed;
+ }
+
+ /**
+ * Sets the executor used for blocking {@link Future#get()} adaptation.
+ * When non-null, this executor is used instead of
+ * {@link CompletableFuture#runAsync(Runnable)}'s default executor to avoid
+ * pool starvation when many blocking futures are being adapted
+ * simultaneously.
+ *
+ * @param executor the executor to use, or {@code null} to use the default
+ */
+ public static void setBlockingExecutor(Executor executor) {
+ blockingExecutor = executor;
+ }
+
+ /**
+ * Converts the given source to an {@link Awaitable}.
+ * If the source is already an {@code Awaitable}, it is returned as-is.
+ * <p>
+ * Uses a per-class {@link ClassValue} cache to avoid repeated linear
+ * scans of the adapter list on the hot path. The first lookup for a
+ * given class performs a linear scan; subsequent lookups for the same
+ * class are O(1).
+ * <p>
+ * <b>Tip:</b> user code should generally prefer {@link Awaitable#from(Object)},
+ * which delegates to this method but is more discoverable from the
+ * {@code Awaitable} type itself.
+ *
+ * @param source the source object; must not be {@code null}
+ * @param <T> the result type
+ * @return an awaitable backed by the source
+ * @throws IllegalArgumentException if {@code source} is {@code null}
+ * or no adapter supports the source type
+ * @see Awaitable#from(Object)
+ */
+ @SuppressWarnings("unchecked")
+ static <T> Awaitable<T> toAwaitable(Object source) {
+ if (source == null) {
+ throw new IllegalArgumentException("Cannot convert null to Awaitable");
+ }
+ if (source instanceof Awaitable) return (Awaitable<T>) source;
+ Class<?> type = source.getClass();
+ AwaitableAdapter adapter = awaitableCache.get(type);
+ if (adapter != null) {
+ return adapter.toAwaitable(source);
+ }
+ throw new IllegalArgumentException(
+ "No AwaitableAdapter found for type: " + type.getName()
+ + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader.");
+ }
+
+ /**
+ * Converts the given source to an {@link AsyncStream}.
+ * If the source is already an {@code AsyncStream}, it is returned as-is.
+ * {@link Channel} instances are handled as a built-in special case via
+ * {@link Channel#asStream()}, enabling transparent {@code for await}
+ * iteration over channels.
+ * <p>
+ * Uses a per-class {@link ClassValue} cache to avoid repeated linear
+ * scans of the adapter list on the hot path.
+ * <p>
+ * <b>Tip:</b> user code should generally prefer {@link AsyncStream#from(Object)},
+ * which delegates to this method but is more discoverable from the
+ * {@code AsyncStream} type itself.
+ *
+ * @param source the source object; must not be {@code null}
+ * @param <T> the element type
+ * @return an async stream backed by the source
+ * @throws IllegalArgumentException if {@code source} is {@code null}
+ * or no adapter supports the source type
+ * @see AsyncStream#from(Object)
+ */
+ @SuppressWarnings("unchecked")
+ static <T> AsyncStream<T> toAsyncStream(Object source) {
+ if (source == null) {
+ throw new IllegalArgumentException("Cannot convert null to AsyncStream");
+ }
+ if (source instanceof AsyncStream) return (AsyncStream<T>) source;
+ if (source instanceof Channel) return ((Channel<T>) source).asStream();
+ Class<?> type = source.getClass();
+ AwaitableAdapter adapter = streamCache.get(type);
+ if (adapter != null) {
+ return adapter.toAsyncStream(source);
+ }
+ throw new IllegalArgumentException(
+ "No AsyncStream adapter found for type: " + type.getName()
+ + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader.");
+ }
+
+ // ---- Cache management ------------------------------------------------
+
+ private static ClassValue<AwaitableAdapter> buildAwaitableCache() {
+ return new ClassValue<>() {
+ @Override
+ protected AwaitableAdapter computeValue(Class<?> type) {
+ for (AwaitableAdapter adapter : ADAPTERS) {
+ if (adapter.supportsAwaitable(type)) {
+ return adapter;
+ }
+ }
+ return null;
+ }
+ };
+ }
+
+ private static ClassValue<AwaitableAdapter> buildStreamCache() {
+ return new ClassValue<>() {
+ @Override
+ protected AwaitableAdapter computeValue(Class<?> type) {
+ for (AwaitableAdapter adapter : ADAPTERS) {
+ if (adapter.supportsAsyncStream(type)) {
+ return adapter;
+ }
+ }
+ return null;
+ }
+ };
+ }
+
+ /**
+ * Rebuilds the per-class adapter caches after adapter registration
+ * changes. Uses volatile reference swap — safe because
+ * {@link ClassValue} instances are immutable once constructed, and
+ * concurrent readers of the old cache see a consistent (if stale)
+ * snapshot until the new cache is published.
+ */
+ private static void invalidateCaches() {
+ awaitableCache = buildAwaitableCache();
+ streamCache = buildStreamCache();
+ }
+
+ /**
+ * Built-in adapter handling JDK {@link CompletableFuture}, {@link CompletionStage},
+ * {@link Future}, and {@link Iterable}/{@link Iterator} (for async stream bridging).
+ * <p>
+ * {@link CompletionStage} support enables seamless integration with frameworks
+ * that return {@code CompletionStage} (e.g., Spring's async APIs, Reactor's
+ * {@code Mono.toFuture()}, etc.) without any additional adapter registration.
+ */
+ private static class BuiltInAdapter implements AwaitableAdapter {
+
+ @Override
+ public boolean supportsAwaitable(Class<?> type) {
+ return CompletionStage.class.isAssignableFrom(type)
+ || Future.class.isAssignableFrom(type);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> Awaitable<T> toAwaitable(Object source) {
+ if (source instanceof CompletionStage) {
+ return new GroovyPromise<>(((CompletionStage<T>) source).toCompletableFuture());
+ }
+ if (source instanceof Future) {
+ Future<T> future = (Future<T>) source;
+ CompletableFuture<T> cf = new CompletableFuture<>();
+ if (future.isDone()) {
+ completeFrom(cf, future);
+ } else {
+ Executor exec = blockingExecutor;
+ if (exec != null) {
+ CompletableFuture.runAsync(() -> completeFrom(cf, future), exec);
+ } else {
+ CompletableFuture.runAsync(() -> completeFrom(cf, future));
+ }
+ }
+ return new GroovyPromise<>(cf);
+ }
+ throw new IllegalArgumentException("Cannot convert to Awaitable: " + source.getClass());
+ }
+
+ @Override
+ public boolean supportsAsyncStream(Class<?> type) {
+ return Iterable.class.isAssignableFrom(type)
+ || Iterator.class.isAssignableFrom(type);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> AsyncStream<T> toAsyncStream(Object source) {
+ final Iterator<T> iterator;
+ if (source instanceof Iterable) {
+ iterator = ((Iterable<T>) source).iterator();
+ } else if (source instanceof Iterator) {
+ iterator = (Iterator<T>) source;
+ } else {
+ throw new IllegalArgumentException("Cannot convert to AsyncStream: " + source.getClass());
+ }
+ return new AsyncStream<T>() {
+ private T current;
+
+ @Override
+ public Awaitable<Boolean> moveNext() {
+ boolean hasNext = iterator.hasNext();
+ if (hasNext) current = iterator.next();
+ return Awaitable.of(hasNext);
+ }
+
+ @Override
+ public T getCurrent() {
+ return current;
+ }
+ };
+ }
+
+ private static <T> void completeFrom(CompletableFuture<T> cf, Future<T> future) {
+ try {
+ cf.complete(future.get());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ CancellationException ce = new CancellationException("Interrupted while adapting Future");
+ ce.initCause(e);
+ cf.completeExceptionally(ce);
+ } catch (ExecutionException e) {
+ cf.completeExceptionally(e.getCause() != null ? e.getCause() : e);
+ } catch (Exception e) {
+ cf.completeExceptionally(e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/groovy/concurrent/Channel.java b/src/main/java/groovy/concurrent/Channel.java
new file mode 100644
index 0000000000..63e3dcfb9d
--- /dev/null
+++ b/src/main/java/groovy/concurrent/Channel.java
@@ -0,0 +1,409 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+import org.apache.groovy.runtime.async.GroovyPromise;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Asynchronous channel with optional buffering for inter-task communication.
+ * <p>
+ * A channel coordinates producers and consumers without exposing explicit
+ * locks. With a capacity of {@code 0}, sends rendezvous directly with
+ * receivers (unbuffered semantics). With a positive capacity, values are
+ * buffered until the channel fills, after which senders wait until a
+ * receiver makes room.
+ * <p>
+ * Channel operations return {@link Awaitable} values so they compose
+ * naturally with Groovy's {@code await} and {@link Awaitable#any(Object...)}
+ * APIs.
+ * <p>
+ * Channels also integrate with Groovy's {@code for await} syntax via
+ * the {@link #asStream()} method, which provides a read-only
+ * {@link AsyncStream} view that yields received values until the channel
+ * is closed:
+ * <pre>
+ * for await (item in channel) {
+ * process(item)
+ * }
+ * </pre>
+ * <p>
+ * {@code null} payloads are rejected to keep a clear distinction between
+ * "no value yet" and an actual payload. If the channel is closed, pending
+ * senders fail immediately and future receivers drain any buffered items
+ * before failing with {@link ChannelClosedException}.
+ *
+ * @param <T> the payload type
+ * @since 6.0.0
+ */
+public final class Channel<T> {
+
+ private final ReentrantLock lock = new ReentrantLock(true);
+ private final Deque<T> buffer = new ArrayDeque<>();
+ private final Deque<PendingSender<T>> waitingSenders = new ArrayDeque<>();
+ private final Deque<CompletableFuture<T>> waitingReceivers = new ArrayDeque<>();
+ private final int capacity;
+
+ private boolean closed;
+
+ /**
+ * Creates an unbuffered channel.
+ */
+ public Channel() {
+ this(0);
+ }
+
+ /**
+ * Creates a channel with the given buffer capacity.
+ *
+ * @param capacity the buffer capacity; must not be negative
+ */
+ public Channel(int capacity) {
+ if (capacity < 0) {
+ throw new IllegalArgumentException("channel capacity must not be negative: " + capacity);
+ }
+ this.capacity = capacity;
+ }
+
+ /**
+ * Returns this channel's configured buffer capacity.
+ *
+ * @return the capacity
+ */
+ public int getCapacity() {
+ return capacity;
+ }
+
+ /**
+ * Returns the number of currently buffered elements.
+ *
+ * @return the buffered element count
+ */
+ public int getBufferedSize() {
+ lock.lock();
+ try {
+ return buffer.size();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Returns {@code true} if the channel has been closed.
+ *
+ * @return {@code true} if closed
+ */
+ public boolean isClosed() {
+ lock.lock();
+ try {
+ return closed;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Sends a value through the channel.
+ * <p>
+ * The returned {@link Awaitable} completes when the value has either been
+ * handed off to a receiver or enqueued in the channel buffer.
+ *
+ * @param value the value to send; must not be {@code null}
+ * @return an awaitable that completes when the send succeeds
+ */
+ public Awaitable<Void> send(T value) {
+ Objects.requireNonNull(value, "channel does not support null values");
+ CompletableFuture<Void> completion = new CompletableFuture<>();
+ PendingSender<T> pendingSender = new PendingSender<>(value, completion);
+
+ lock.lock();
+ try {
+ if (closed) {
+ completion.completeExceptionally(closedForSend());
+ } else if (deliverToWaitingReceiver(value)) {
+ completion.complete(null);
+ } else if (buffer.size() < capacity) {
+ buffer.addLast(value);
+ completion.complete(null);
+ } else {
+ waitingSenders.addLast(pendingSender);
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ if (!completion.isDone()) {
+ completion.whenComplete((ignored, error) -> {
+ if (error != null || completion.isCancelled()) {
+ removePendingSender(pendingSender);
+ }
+ });
+ }
+
+ return GroovyPromise.of(completion);
+ }
+
+ /**
+ * Receives the next value from the channel.
+ * <p>
+ * If the channel is buffered, buffered values are consumed first. If the
+ * channel is closed and drained, the returned awaitable fails with
+ * {@link ChannelClosedException}.
+ *
+ * @return an awaitable that yields the next value
+ */
+ public Awaitable<T> receive() {
+ CompletableFuture<T> completion = new CompletableFuture<>();
+
+ lock.lock();
+ try {
+ T buffered = pollBuffer();
+ if (buffered != null) {
+ completion.complete(buffered);
+ } else {
+ PendingSender<T> sender = pollPendingSender();
+ if (sender != null) {
+ sender.completion.complete(null);
+ completion.complete(sender.value);
+ } else if (closed) {
+ completion.completeExceptionally(closedForReceive());
+ } else {
+ waitingReceivers.addLast(completion);
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ if (!completion.isDone()) {
+ completion.whenComplete((ignored, error) -> {
+ if (error != null || completion.isCancelled()) {
+ removePendingReceiver(completion);
+ }
+ });
+ }
+
+ return GroovyPromise.of(completion);
+ }
+
+ /**
+ * Closes the channel.
+ * <p>
+ * Closing is idempotent. Buffered values remain receivable. Any pending
+ * senders fail immediately, and receivers waiting after the buffer is
+ * drained fail with {@link ChannelClosedException}.
+ *
+ * @return {@code true} if this invocation closed the channel,
+ * {@code false} if it had already been closed
+ */
+ public boolean close() {
+ lock.lock();
+ try {
+ if (closed) {
+ return false;
+ }
+ closed = true;
+
+ drainBufferToReceivers();
+
+ while (!waitingReceivers.isEmpty()) {
+ waitingReceivers.removeFirst().completeExceptionally(closedForReceive());
+ }
+
+ while (!waitingSenders.isEmpty()) {
+ waitingSenders.removeFirst().completion.completeExceptionally(closedForSend());
+ }
+
+ return true;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Returns a read-only {@link AsyncStream} view of this channel.
+ * <p>
+ * The returned stream yields values received from the channel via
+ * {@link #receive()}. When the channel is closed and all buffered
+ * values are drained, the stream signals completion
+ * ({@code moveNext()} returns {@code false}).
+ * <p>
+ * This enables natural {@code for await} iteration:
+ * <pre>
+ * for await (item in channel) {
+ * process(item)
+ * }
+ * </pre>
+ * <p>
+ * <b>Ownership:</b> the stream view does not own the channel — calling
+ * {@code close()} on the stream is a no-op. The producer is responsible
+ * for closing the channel when done sending. This matches Go's semantics
+ * where the receiver does not close the channel.
+ *
+ * @return an async stream view of this channel
+ */
+ public AsyncStream<T> asStream() {
+ return new ChannelAsyncStream<>(this);
+ }
+
+ @Override
+ public String toString() {
+ lock.lock();
+ try {
+ return "Channel{capacity=" + capacity
+ + ", buffered=" + buffer.size()
+ + ", waitingSenders=" + waitingSenders.size()
+ + ", waitingReceivers=" + waitingReceivers.size()
+ + ", closed=" + closed
+ + '}';
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private boolean deliverToWaitingReceiver(T value) {
+ while (!waitingReceivers.isEmpty()) {
+ CompletableFuture<T> receiver = waitingReceivers.removeFirst();
+ if (receiver.complete(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void drainBufferToReceivers() {
+ while (!waitingReceivers.isEmpty() && !buffer.isEmpty()) {
+ CompletableFuture<T> receiver = waitingReceivers.removeFirst();
+ if (receiver.complete(buffer.peekFirst())) {
+ buffer.removeFirst();
+ }
+ }
+ }
+
+ private T pollBuffer() {
+ if (buffer.isEmpty()) {
+ return null;
+ }
+ T value = buffer.removeFirst();
+ refillBufferFromWaitingSenders();
+ return value;
+ }
+
+ private void refillBufferFromWaitingSenders() {
+ while (buffer.size() < capacity) {
+ PendingSender<T> sender = pollPendingSender();
+ if (sender == null) {
+ return;
+ }
+ buffer.addLast(sender.value);
+ sender.completion.complete(null);
+ }
+ }
+
+ private PendingSender<T> pollPendingSender() {
+ while (!waitingSenders.isEmpty()) {
+ PendingSender<T> sender = waitingSenders.removeFirst();
+ if (!sender.completion.isDone()) {
+ return sender;
+ }
+ }
+ return null;
+ }
+
+ private void removePendingSender(PendingSender<T> sender) {
+ lock.lock();
+ try {
+ waitingSenders.remove(sender);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private void removePendingReceiver(CompletableFuture<T> receiver) {
+ lock.lock();
+ try {
+ waitingReceivers.remove(receiver);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private static ChannelClosedException closedForSend() {
+ return new ChannelClosedException("channel is closed for send");
+ }
+
+ private static ChannelClosedException closedForReceive() {
+ return new ChannelClosedException("channel is closed");
+ }
+
+ private static final class PendingSender<T> {
+ private final T value;
+ private final CompletableFuture<Void> completion;
+
+ private PendingSender(T value, CompletableFuture<Void> completion) {
+ this.value = value;
+ this.completion = completion;
+ }
+ }
+
+ /**
+ * Read-only {@link AsyncStream} view over a channel.
+ * <p>
+ * Each {@link #moveNext()} call issues a {@link Channel#receive()};
+ * {@link ChannelClosedException} is translated to end-of-stream
+ * ({@code false}). The stream does not own the channel, so its
+ * {@link #close()} is a no-op.
+ */
+ private static final class ChannelAsyncStream<T> implements AsyncStream<T> {
+ private final Channel<T> channel;
+ private volatile T current;
+
+ ChannelAsyncStream(Channel<T> channel) {
+ this.channel = channel;
+ }
+
+ @Override
+ public Awaitable<Boolean> moveNext() {
+ return channel.receive().handle((value, error) -> {
+ if (error != null) {
+ if (error instanceof ChannelClosedException) {
+ return false;
+ }
+ if (error instanceof RuntimeException re) throw re;
+ if (error instanceof Error e) throw e;
+ throw new RuntimeException(error);
+ }
+ current = value;
+ return true;
+ });
+ }
+
+ @Override
+ public T getCurrent() {
+ return current;
+ }
+
+ // No-op: the stream view does not own the channel
+ }
+}
diff --git a/src/main/java/groovy/concurrent/ChannelClosedException.java b/src/main/java/groovy/concurrent/ChannelClosedException.java
new file mode 100644
index 0000000000..e71fd02138
--- /dev/null
+++ b/src/main/java/groovy/concurrent/ChannelClosedException.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+/**
+ * Indicates that a {@link Channel} operation was attempted after the channel
+ * had been closed.
+ *
+ * @since 6.0.0
+ */
+public class ChannelClosedException extends IllegalStateException {
+
+ public ChannelClosedException(String message) {
+ super(message);
+ }
+
+ public ChannelClosedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/groovy/transform/Async.java b/src/main/java/groovy/transform/Async.java
new file mode 100644
index 0000000000..ddfccbee4b
--- /dev/null
+++ b/src/main/java/groovy/transform/Async.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Method annotation to make a method execute asynchronously and return an
+ * {@link groovy.concurrent.Awaitable Awaitable}.
+ * <p>
+ * When applied to a method, the {@code @Async} transformation will:
+ * <ul>
+ * <li>Change the method's return type to {@code Awaitable<T>} (where {@code T} is the original return type)</li>
+ * <li>Execute the method body asynchronously via {@link org.apache.groovy.runtime.async.AsyncSupport#executeAsync AsyncSupport.executeAsync}</li>
+ * <li>Transform any {@code await(expr)} calls within the method to use {@link org.apache.groovy.runtime.async.AsyncSupport#await AsyncSupport.await}</li>
+ * <li>For generator methods containing {@code yield return}, return
+ * {@link groovy.concurrent.AsyncStream AsyncStream<T>} instead</li>
+ * <li>If the method contains {@code defer} statements, wrap the body
+ * in try-finally to execute deferred actions in LIFO order on exit</li>
+ * </ul>
+ * <p>
+ * The {@code await(future)} pattern inside an {@code @Async} method blocks the async thread until the
+ * given future completes and returns the unwrapped result. This provides a sequential programming model
+ * over asynchronous operations.
+ * <p>
+ * <em>Example usage:</em>
+ * <pre>
+ * import groovy.transform.Async
+ * import groovy.concurrent.Awaitable
+ *
+ * class DataService {
+ *
+ * {@code @}Async
+ * def fetchUser(long id) {
+ * def profile = await(fetchProfile(id))
+ * def orders = await(fetchOrders(id))
+ * return [profile: profile, orders: orders]
+ * }
+ *
+ * {@code @}Async
+ * Map fetchProfile(long id) {
+ * return [name: "User$id"]
+ * }
+ *
+ * {@code @}Async
+ * List fetchOrders(long id) {
+ * return ["order1", "order2"]
+ * }
+ * }
+ *
+ * def service = new DataService()
+ * def awaitable = service.fetchUser(1) // returns Awaitable
+ * def result = awaitable.get() // blocks until complete
+ * assert result.profile.name == 'User1'
+ * assert result.orders.size() == 2
+ * </pre>
+ * <p>
+ * For ad-hoc async execution without annotating a method, use the {@code async}
+ * closure syntax. Note that {@code async { ... }} returns a {@code Closure}
+ * that must be explicitly called:
+ * <pre>
+ * def task = async {
+ * def data = await fetchFromRemote()
+ * return process(data)
+ * }
+ * def result = task().get() // call the closure, then get the result
+ * </pre>
+ * <p>
+ * <b>Custom Executor:</b> By default, tasks run on virtual threads (JDK 21+) or
+ * a cached daemon thread pool whose maximum size is controlled by the system
+ * property {@code groovy.async.parallelism} (JDK < 21). You can specify a custom
+ * executor by providing the name of a field in the class:
+ * <pre>
+ * class MyService {
+ * private final Executor myExecutor = Executors.newFixedThreadPool(10)
+ *
+ * {@code @}Async(executor = "myExecutor")
+ * def fetchData() {
+ * return await(remoteCall())
+ * }
+ * }
+ * </pre>
+ * <p>
+ * <b>Restrictions:</b>
+ * <ul>
+ * <li>Cannot be applied to abstract methods</li>
+ * <li>Cannot be applied to constructors</li>
+ * <li>Cannot be applied to methods that already return an async type
+ * ({@code Awaitable}, {@code AsyncStream}, {@code CompletableFuture},
+ * {@code CompletionStage}, or {@code Future})</li>
+ * </ul>
+ *
+ * @see groovy.concurrent.Awaitable
+ * @see groovy.concurrent.AsyncStream
+ * @since 6.0.0
+ */
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.METHOD)
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.AsyncASTTransformation")
+public @interface Async {
+ /**
+ * Optional name of a field in the declaring class that holds an
+ * {@link java.util.concurrent.Executor Executor} to use for async execution.
+ * If empty (the default), uses the default executor (virtual threads on
+ * JDK 21+, or a cached daemon thread pool controlled by the system
+ * property {@code groovy.async.parallelism}).
+ *
+ * @return the executor field name, or empty string for the default executor
+ */
+ String executor() default "";
+}
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index ff07bc7fb1..adabfaaa38 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -131,6 +131,7 @@ import org.codehaus.groovy.runtime.StringGroovyMethods;
import org.codehaus.groovy.syntax.Numbers;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.syntax.Types;
+import org.codehaus.groovy.transform.AsyncTransformHelper;
import org.objectweb.asm.Opcodes;
import java.io.BufferedReader;
@@ -465,7 +466,12 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
@Override
- public ForStatement visitForStmtAlt(final ForStmtAltContext ctx) {
+ public Statement visitForStmtAlt(final ForStmtAltContext ctx) {
+ // 'for await' async iteration
+ if (ctx.AWAIT() != null) {
+ return visitForAwait(ctx);
+ }
+
Function<Statement, ForStatement> maker = this.visitForControl(ctx.forControl());
Statement loopBody = this.unpackStatement((Statement) this.visit(ctx.statement()));
@@ -473,6 +479,58 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return configureAST(maker.apply(loopBody), ctx);
}
+ /**
+ * Transforms {@code for await (item in source) { ... }} into a while-loop
+ * over an {@link groovy.concurrent.AsyncStream}: the source expression is
+ * adapted via {@code AsyncSupport.toAsyncStream()}, then repeatedly polled
+ * with {@code moveNext()} / {@code getCurrent()}.
+ * <p>
+ * Variable modifiers (e.g. {@code final}) from the enhanced-for declaration
+ * are applied to the synthesised loop variable, consistent with the
+ * standard {@code for (... in ...)} handling in
+ * {@link #visitEnhancedForControl}.
+ */
+ private Statement visitForAwait(final ForStmtAltContext ctx) {
+ ForControlContext forCtrl = ctx.forControl();
+ EnhancedForControlContext enhCtrl = forCtrl.enhancedForControl();
+ if (enhCtrl == null) {
+ throw createParsingFailedException("for await requires enhanced for syntax: for await (item in source)", ctx);
+ }
+
+ ClassNode varType = enhCtrl.type() != null ? this.visitType(enhCtrl.type()) : ClassHelper.dynamicType();
+ String varName = this.visitIdentifier(enhCtrl.identifier());
+ Expression source = (Expression) this.visit(enhCtrl.expression());
+ Statement loopBody = this.unpackStatement((Statement) this.visit(ctx.statement()));
+
+ // Apply variable modifiers (e.g. final) to the loop variable
+ VariableExpression loopVar = varX(varName, varType);
+ ModifierManager modifierManager = new ModifierManager(this,
+ this.visitVariableModifiersOpt(enhCtrl.variableModifiersOpt()));
+ modifierManager.processVariableExpression(loopVar);
+
+ String streamVar = "$__asyncStream__" + (asyncStreamCounter++);
+
+ // def $__asyncStream__N = AsyncSupport.toAsyncStream(source)
+ Expression toStreamCall = AsyncTransformHelper.buildToAsyncStreamCall(source);
+ ExpressionStatement streamDecl = new ExpressionStatement(declX(varX(streamVar), toStreamCall));
+
+ // while (AsyncSupport.await($__asyncStream__N.moveNext()))
+ Expression moveNextCall = callX(varX(streamVar), "moveNext");
+ Expression awaitCall = AsyncTransformHelper.buildAwaitCall(moveNextCall);
+ BooleanExpression condition = new BooleanExpression(awaitCall);
+
+ // def <varName> = $__asyncStream__N.getCurrent()
+ Expression getCurrentCall = callX(varX(streamVar), "getCurrent");
+ ExpressionStatement getItemStmt = new ExpressionStatement(declX(loopVar, getCurrentCall));
+
+ BlockStatement whileBody = block(getItemStmt, loopBody);
+ WhileStatement whileStmt = new WhileStatement(condition, whileBody);
+ Expression closeCall = AsyncTransformHelper.buildCloseStreamCall(varX(streamVar));
+ TryCatchStatement tryFinally = new TryCatchStatement(whileStmt, block(new ExpressionStatement(closeCall)));
+
+ return configureAST(block(streamDecl, tryFinally), ctx);
+ }
+
@Override
public Function<Statement, ForStatement> visitForControl(final ForControlContext ctx) {
// e.g. `for (var e : x) { }` and `for (e in x) { }`
@@ -865,6 +923,40 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return configureAST(this.visitYieldStatement(ctx.yieldStatement()), ctx);
}
+ @Override
+ public ExpressionStatement visitYieldReturnStmtAlt(final YieldReturnStmtAltContext ctx) {
+ if (asyncContextDepth == 0) {
+ throw createParsingFailedException(
+ "`yield return` can only be used inside an async method or async closure", ctx);
+ }
+ Expression expr = (Expression) this.visit(ctx.expression());
+ Expression yieldCall = AsyncTransformHelper.buildYieldReturnCall(expr);
+ return configureAST(new ExpressionStatement(yieldCall), ctx);
+ }
+
+ @Override
+ public ExpressionStatement visitDeferStmtAlt(final DeferStmtAltContext ctx) {
+ if (asyncContextDepth == 0) {
+ throw createParsingFailedException(
+ "`defer` can only be used inside an async method or async closure", ctx);
+ }
+ Expression action;
+ ExpressionStatement stmtExprStmt = (ExpressionStatement) this.visit(ctx.statementExpression());
+ Expression expr = stmtExprStmt.getExpression();
+ if (expr instanceof ClosureExpression) {
+ // Already a closure/lambda — use directly as the defer action
+ action = expr;
+ } else {
+ // Wrap the statement expression in a closure: { -> expr }
+ ClosureExpression wrapper = closureX(Parameter.EMPTY_ARRAY, block(stmtExprStmt));
+ wrapper.setSourcePosition(stmtExprStmt);
+ action = wrapper;
+ }
+ // Emit: AsyncSupport.defer($__deferScope__, action)
+ Expression deferCall = AsyncTransformHelper.buildDeferCall(action);
+ return configureAST(new ExpressionStatement(deferCall), ctx);
+ }
+
@Override
public ContinueStatement visitContinueStatement(final ContinueStatementContext ctx) {
if (switchExpressionRuleContextStack.peek() instanceof SwitchExpressionContext) {
@@ -1201,6 +1293,10 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
throw createParsingFailedException("only sealed type declarations should have `permits` clause", ctx);
}
+ if (modifierManager.containsAny(ASYNC)) {
+ throw createParsingFailedException("modifier `async` is not allowed for type declarations", modifierManager.get(ASYNC).get());
+ }
+
int modifiers = modifierManager.getClassModifiersOpValue();
boolean syntheticPublic = ((modifiers & Opcodes.ACC_SYNTHETIC) != 0);
@@ -1685,8 +1781,15 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
Parameter[] parameters = this.visitFormalParameters(ctx.formalParameters());
ClassNode[] exceptions = this.visitQualifiedClassNameList(ctx.qualifiedClassNameList());
+ boolean isAsync = modifierManager.containsAny(ASYNC);
+ if (isAsync) asyncContextDepth++;
anonymousInnerClassesDefinedInMethodStack.push(new LinkedList<>());
- Statement code = this.visitMethodBody(ctx.methodBody());
+ Statement code;
+ try {
+ code = this.visitMethodBody(ctx.methodBody());
+ } finally {
+ if (isAsync) asyncContextDepth--;
+ }
List<InnerClassNode> anonymousInnerClassList = anonymousInnerClassesDefinedInMethodStack.pop();
MethodNode methodNode;
@@ -1699,6 +1802,12 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
} else { // script method declaration
methodNode = createScriptMethodNode(modifierManager, methodName, returnType, parameters, exceptions, code);
}
+
+ // Inject @Async annotation for methods declared with the 'async' keyword modifier
+ if (isAsync) {
+ methodNode.addAnnotation(new AnnotationNode(ClassHelper.make(groovy.transform.Async.class)));
+ }
+
anonymousInnerClassList.forEach(e -> e.setEnclosingMethod(methodNode));
methodNode.setGenericsTypes(this.visitTypeParameters(ctx.typeParameters()));
@@ -1923,6 +2032,10 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
asBoolean(ctx.modifiers()) ? this.visitModifiers(ctx.modifiers()) : Collections.emptyList()
);
+ if (modifierManager.containsAny(ASYNC)) {
+ throw createParsingFailedException("modifier `async` is not allowed for variable declarations", modifierManager.get(ASYNC).get());
+ }
+
if (asBoolean(ctx.typeNamePairs())) { // e.g. def (int a, int b) = [1, 2]
return this.createMultiAssignmentDeclarationListStatement(ctx, modifierManager);
}
@@ -2877,6 +2990,61 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
throw createParsingFailedException("Unsupported unary expression: " + ctx.getText(), ctx);
}
+ @Override
+ public Expression visitAwaitExprAlt(final AwaitExprAltContext ctx) {
+ Expression expr = (Expression) this.visit(ctx.expression());
+ return configureAST(
+ AsyncTransformHelper.buildAwaitCall(expr),
+ ctx);
+ }
+
+ @Override
+ public Expression visitAsyncClosureExprAlt(final AsyncClosureExprAltContext ctx) {
+ asyncContextDepth++;
+ ClosureExpression closure;
+ try {
+ closure = this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression());
+ } finally {
+ asyncContextDepth--;
+ }
+ boolean hasUserParams = closure.getParameters() != null && closure.getParameters().length > 0;
+ boolean hasYieldReturn = AsyncTransformHelper.containsYieldReturn(closure.getCode());
+ boolean hasDefer = AsyncTransformHelper.containsDefer(closure.getCode());
+
+ // If defer is present, wrap the closure body in try-finally with defer scope
+ if (hasDefer) {
+ Statement wrappedBody = AsyncTransformHelper.wrapWithDeferScope(closure.getCode());
+ ClosureExpression newClosure = new ClosureExpression(closure.getParameters(), wrappedBody);
+ newClosure.setVariableScope(closure.getVariableScope());
+ newClosure.setSourcePosition(closure);
+ closure = newClosure;
+ }
+
+ if (hasYieldReturn) {
+ // Inject synthetic $__asyncGen__ as first parameter — rebuild closure
+ Parameter genParam = AsyncTransformHelper.createGenParam();
+ Parameter[] existingParams = closure.getParameters();
+ Parameter[] newParams;
+ if (hasUserParams) {
+ newParams = new Parameter[existingParams.length + 1];
+ newParams[0] = genParam;
+ System.arraycopy(existingParams, 0, newParams, 1, existingParams.length);
+ } else {
+ newParams = new Parameter[]{genParam};
+ }
+ ClosureExpression genClosure = new ClosureExpression(newParams, closure.getCode());
+ genClosure.setVariableScope(closure.getVariableScope());
+ genClosure.setSourcePosition(closure);
+ // Transform yieldReturn(expr) → yieldReturn($__asyncGen__, expr)
+ AsyncTransformHelper.injectGenParamIntoYieldReturnCalls(genClosure.getCode(), genParam);
+ return configureAST(AsyncTransformHelper.buildWrapAsyncGeneratorCall(
+ new ArgumentListExpression(genClosure)), ctx);
+ } else {
+ return configureAST(AsyncTransformHelper.buildWrapAsyncCall(
+ new ArgumentListExpression(closure)), ctx);
+ }
+ }
+
@Override
public CastExpression visitCastExprAlt(final CastExprAltContext ctx) {
Expression expr = (Expression) this.visit(ctx.expression());
@@ -4727,6 +4895,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
private final GroovyLangParser parser;
private final GroovydocManager groovydocManager;
private final TryWithResourcesASTTransformation tryWithResourcesASTTransformation;
+ private int asyncStreamCounter;
private final List<ClassNode> classNodeList = new ArrayList<>();
private final Deque<ClassNode> classNodeStack = new ArrayDeque<>();
@@ -4738,6 +4907,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
private int visitingClosureCount;
private int visitingAssertStatementCount;
private int visitingArrayInitializerCount;
+ /** Tracks nesting depth of async contexts (async methods and async closures)
+ * to validate that {@code defer} is only used within an async body. */
+ private int asyncContextDepth;
private static final int SLL_THRESHOLD = SystemUtil.getIntegerSafe("groovy.antlr4.sll.threshold", -1);
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java b/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
index 4d86449b8d..b84dd808a8 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
@@ -37,6 +37,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.ABSTRACT;
+import static org.apache.groovy.parser.antlr4.GroovyLangParser.ASYNC;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.FINAL;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.NATIVE;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.STATIC;
@@ -47,7 +48,7 @@ import static org.apache.groovy.parser.antlr4.GroovyLangParser.VOLATILE;
*/
class ModifierManager {
private static final Map<Class, List<Integer>> INVALID_MODIFIERS_MAP = Maps.of(
- ConstructorNode.class, Arrays.asList(STATIC, FINAL, ABSTRACT, NATIVE),
+ ConstructorNode.class, Arrays.asList(STATIC, FINAL, ABSTRACT, NATIVE, ASYNC),
MethodNode.class, Arrays.asList(VOLATILE/*, TRANSIENT*/) // Transient is left open for properties for legacy reasons but should be removed before ClassCompletionVerifier runs (CLASSGEN)
);
private AstBuilder astBuilder;
@@ -163,6 +164,7 @@ class ModifierManager {
}
public Parameter processParameter(Parameter parameter) {
+ rejectAsyncModifier("parameter declarations");
modifierNodeList.forEach(e -> {
parameter.setModifiers(parameter.getModifiers() | e.getOpcode());
@@ -191,6 +193,7 @@ class ModifierManager {
}
public VariableExpression processVariableExpression(VariableExpression ve) {
+ rejectAsyncModifier("variable declarations");
modifierNodeList.forEach(e -> {
ve.setModifiers(ve.getModifiers() | e.getOpcode());
@@ -200,6 +203,23 @@ class ModifierManager {
return ve;
}
+ /**
+ * Rejects the {@code async} modifier when used in an unsupported context
+ * (e.g. parameter or local variable declarations). The grammar allows
+ * {@code async} as a general modifier, but semantically it is only valid
+ * on method declarations. This check prevents silent acceptance.
+ *
+ * @param target human-readable description of the disallowed context
+ */
+ private void rejectAsyncModifier(String target) {
+ Optional<ModifierNode> asyncModifier = get(ASYNC);
+ if (asyncModifier.isPresent()) {
+ throw astBuilder.createParsingFailedException(
+ "modifier `async` is only allowed for method declarations, not for " + target,
+ asyncModifier.get());
+ }
+ }
+
public <T extends AnnotatedNode> T attachAnnotations(T node) {
this.getAnnotations().forEach(node::addAnnotation);
diff --git a/src/main/java/org/apache/groovy/runtime/async/AbstractAsyncStream.java b/src/main/java/org/apache/groovy/runtime/async/AbstractAsyncStream.java
new file mode 100644
index 0000000000..2e70a12848
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AbstractAsyncStream.java
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.runtime.async;
+
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.Awaitable;
+
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Template base class for queue-based {@link AsyncStream} implementations.
+ *
+ * <p>This class implements the
+ * <a href="https://en.wikipedia.org/wiki/Template_method_pattern">Template Method</a>
+ * pattern, centralising the signal dispatch logic, lifecycle management, and
+ * interrupt handling that is common to all queue-based async streams.
+ * Concrete subclasses only need to supply a {@link BlockingQueue} and override
+ * a small number of hook methods to customise behaviour.</p>
+ *
+ * <h2>Signal protocol</h2>
+ * <p>Elements flowing through the queue are wrapped in one of three signal types:</p>
+ * <ul>
+ * <li>{@link ValueSignal} — carries a data element (may wrap {@code null})</li>
+ * <li>{@link ErrorSignal} — carries a {@link Throwable} to propagate</li>
+ * <li>{@link #COMPLETE} — singleton sentinel indicating normal end-of-stream</li>
+ * </ul>
+ * <p>The template's {@link #moveNext()} dispatches on these signals with a fixed
+ * sequence: value → set current + {@link #afterValueConsumed()} + return {@code true};
+ * error → set closed + sneaky-throw; complete → set closed + return {@code false}.</p>
+ *
+ * <h2>Hook methods (override points)</h2>
+ * <table>
+ * <caption>Hook methods and their defaults</caption>
+ * <tr><th>Hook</th><th>Default</th><th>Typical override</th></tr>
+ * <tr><td>{@link #beforeTake()}</td><td>return {@code MOVE_NEXT_FALSE} if closed</td>
+ * <td>thread registration, double-check, or drain check</td></tr>
+ * <tr><td>{@link #afterValueConsumed()}</td><td>no-op</td>
+ * <td>request more items from upstream (back-pressure)</td></tr>
+ * <tr><td>{@link #afterMoveNext()}</td><td>no-op</td>
+ * <td>unregister consumer thread</td></tr>
+ * <tr><td>{@link #onMoveNextInterrupted(InterruptedException)}</td>
+ * <td>set closed, restore interrupt, throw {@link CancellationException}</td>
+ * <td>return {@code MOVE_NEXT_FALSE} if already closed</td></tr>
+ * <tr><td>{@link #onClose()}</td><td><em>abstract</em></td>
+ * <td>interrupt threads, cancel subscriptions, drain queue</td></tr>
+ * </table>
+ *
+ * <h2>Thread safety</h2>
+ * <p>The {@link #closed} flag is an {@link AtomicBoolean} shared between the
+ * producer (subclass-managed) and consumer ({@code moveNext()}) sides.
+ * The {@link #close()} method uses CAS to guarantee exactly-once semantics.
+ * The {@link #current} field is {@code volatile} for safe cross-thread visibility.</p>
+ *
+ * <p>This class is an internal implementation detail and should not be referenced
+ * directly by user code.</p>
+ *
+ * @param <T> the element type
+ * @see AsyncStreamGenerator
+ * @see FlowPublisherAdapter
+ * @since 6.0.0
+ */
+public abstract class AbstractAsyncStream<T> implements AsyncStream<T> {
+
+ // ---- Unified signal types ----
+
+ /**
+ * Wraps a data element for transport through the signal queue.
+ * The wrapper is necessary because the queue element type is {@code Object},
+ * and the actual value may be {@code null}.
+ */
+ protected record ValueSignal(Object value) { }
+
+ /**
+ * Wraps an error for transport through the signal queue.
+ * When dispatched by {@link #moveNext()}, the wrapped throwable is
+ * re-thrown via {@link AsyncSupport#sneakyThrow(Throwable)}.
+ */
+ protected record ErrorSignal(Throwable error) { }
+
+ /**
+ * Singleton sentinel indicating normal stream completion.
+ * Identity comparison ({@code ==}) is used in the dispatch logic.
+ */
+ protected static final Object COMPLETE = new Object();
+
+ // ---- Shared state ----
+
+ /** The signal queue bridging producer and consumer. */
+ protected final BlockingQueue<Object> queue;
+
+ /** Lifecycle flag: set exactly once when the stream is closed. */
+ protected final AtomicBoolean closed = new AtomicBoolean(false);
+
+ /** Most recently consumed value, set by {@link #moveNext()} on value signals. */
+ private volatile T current;
+
+ /**
+ * @param queue the blocking queue used for producer→consumer signal delivery;
+ * must not be {@code null}
+ */
+ protected AbstractAsyncStream(BlockingQueue<Object> queue) {
+ this.queue = Objects.requireNonNull(queue, "queue");
+ }
+
+ // ---- Template method: moveNext ----
+
+ /**
+ * Template method implementing the {@link AsyncStream} iteration protocol.
+ *
+ * <p>Execution sequence:</p>
+ * <ol>
+ * <li>{@link #beforeTake()} — may short-circuit with an early return</li>
+ * <li>{@code queue.take()} — blocks until a signal is available</li>
+ * <li>Signal dispatch: value / error / complete</li>
+ * <li>{@link #afterMoveNext()} — always runs (finally block)</li>
+ * </ol>
+ *
+ * <p>If {@code queue.take()} throws {@link InterruptedException},
+ * {@link #onMoveNextInterrupted(InterruptedException)} handles it.</p>
+ *
+ * @return an {@code Awaitable<Boolean>} — {@code true} if a new element is
+ * available via {@link #getCurrent()}, {@code false} if the stream
+ * is exhausted or closed
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public final Awaitable<Boolean> moveNext() {
+ Awaitable<Boolean> earlyReturn = beforeTake();
+ if (earlyReturn != null) {
+ return earlyReturn;
+ }
+ try {
+ Object signal = queue.take();
+
+ if (signal instanceof ValueSignal vs) {
+ current = (T) vs.value;
+ afterValueConsumed();
+ return MOVE_NEXT_TRUE;
+ }
+ if (signal instanceof ErrorSignal es) {
+ closed.set(true);
+ Throwable cause = es.error;
+ if (cause instanceof Error err) throw err;
+ throw AsyncSupport.sneakyThrow(cause);
+ }
+ // COMPLETE sentinel — end-of-stream
+ closed.set(true);
+ return MOVE_NEXT_FALSE;
+ } catch (InterruptedException e) {
+ return onMoveNextInterrupted(e);
+ } finally {
+ afterMoveNext();
+ }
+ }
+
+ // ---- Final implementations ----
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns the most recently consumed element, set by the last successful
+ * {@link #moveNext()} call.
+ *
+ * @return the current element, or {@code null} before the first successful
+ * {@code moveNext()} call
+ */
+ @Override
+ public final T getCurrent() {
+ return current;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Atomically marks this stream as closed via CAS on the {@link #closed}
+ * flag, then delegates to {@link #onClose()} for subclass-specific cleanup.
+ * Idempotent: only the first invocation triggers {@code onClose()}.
+ */
+ @Override
+ public final void close() {
+ if (!closed.compareAndSet(false, true)) {
+ return;
+ }
+ onClose();
+ }
+
+ // ---- Abstract methods ----
+
+ /**
+ * Subclass-specific cleanup, called exactly once from {@link #close()}.
+ * Typical actions include interrupting blocked threads, cancelling
+ * upstream subscriptions, and draining the queue.
+ */
+ protected abstract void onClose();
+
+ // ---- Hook methods with defaults ----
+
+ /**
+ * Pre-take hook invoked at the start of {@link #moveNext()}.
+ * <p>
+ * Return a non-{@code null} {@link Awaitable} to short-circuit
+ * {@code moveNext()} without blocking on the queue. Return {@code null}
+ * to proceed with the normal take-and-dispatch sequence.
+ * <p>
+ * The default implementation returns {@link #MOVE_NEXT_FALSE} when
+ * the stream is closed, and {@code null} otherwise.
+ *
+ * @return an early-return value, or {@code null} to continue
+ */
+ protected Awaitable<Boolean> beforeTake() {
+ return closed.get() ? MOVE_NEXT_FALSE : null;
+ }
+
+ /**
+ * Post-value hook invoked after a {@link ValueSignal} has been consumed
+ * and the {@link #current} field updated.
+ * <p>
+ * Subclasses may use this to signal demand to an upstream source
+ * (e.g. {@code Subscription.request(1)} for reactive streams).
+ * <p>
+ * The default implementation is a no-op.
+ */
+ protected void afterValueConsumed() { }
+
+ /**
+ * Finally hook invoked after every {@link #moveNext()} attempt,
+ * regardless of outcome (value, error, completion, or interrupt).
+ * <p>
+ * Subclasses may use this to unregister the consumer thread.
+ * <p>
+ * The default implementation is a no-op.
+ */
+ protected void afterMoveNext() { }
+
+ /**
+ * Interrupt handler invoked when {@code queue.take()} throws
+ * {@link InterruptedException} inside {@link #moveNext()}.
+ * <p>
+ * The default implementation sets {@link #closed} to {@code true},
+ * restores the interrupt flag, and throws a {@link CancellationException}.
+ * Subclasses may override to return {@link #MOVE_NEXT_FALSE} instead
+ * of throwing (e.g. when an external {@code close()} caused the interrupt).
+ *
+ * @param e the interrupt exception
+ * @return an {@link Awaitable} to return from {@code moveNext()}, or
+ * the method may throw instead
+ */
+ protected Awaitable<Boolean> onMoveNextInterrupted(InterruptedException e) {
+ closed.set(true);
+ Thread.currentThread().interrupt();
+ throw newCancellationException("Interrupted during moveNext", e);
+ }
+
+ // ---- Utilities for subclasses ----
+
+ /**
+ * Creates a {@link CancellationException} with the given message and cause.
+ *
+ * @param message the detail message
+ * @param cause the interrupt that triggered the cancellation
+ * @return a new {@code CancellationException}
+ */
+ protected static CancellationException newCancellationException(String message, InterruptedException cause) {
+ CancellationException ce = new CancellationException(message);
+ ce.initCause(cause);
+ return ce;
+ }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java b/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java
new file mode 100644
index 0000000000..e01d5e67f6
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.runtime.async;
+
+import groovy.concurrent.Awaitable;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A producer/consumer implementation of {@link groovy.concurrent.AsyncStream AsyncStream}
+ * backed by a {@link SynchronousQueue}, used by {@code async} methods that
+ * contain {@code yield return} statements.
+ * <p>
+ * Extends {@link AbstractAsyncStream} which provides the common
+ * {@link #moveNext()}/{@link #getCurrent()}/{@link #close()} template.
+ * This class adds the producer-side API ({@link #yield}, {@link #complete},
+ * {@link #error}) and overrides the following hooks:
+ *
+ * <ul>
+ * <li>{@link #beforeTake()} — enforces single-consumer semantics via
+ * {@link #consumerThread} CAS + double-check of the {@link #closed} flag</li>
+ * <li>{@link #afterMoveNext()} — unregisters the consumer thread</li>
+ * <li>{@link #onMoveNextInterrupted(InterruptedException)} — returns
+ * {@code MOVE_NEXT_FALSE} if the stream was already closed (cooperative
+ * cancellation), otherwise throws {@link CancellationException}</li>
+ * <li>{@link #onClose()} — interrupts both producer and consumer threads</li>
+ * </ul>
+ *
+ * <h2>Back-pressure</h2>
+ * Uses a {@link SynchronousQueue} to provide natural back-pressure:
+ * the producer thread blocks at each {@code yield return} until the
+ * consumer has consumed the previous element, providing natural one-at-a-time
+ * delivery semantics.
+ *
+ * <h2>Cooperative cancellation via thread tracking</h2>
+ * The {@link #producerThread} and {@link #consumerThread} fields track the
+ * threads currently blocked inside {@link SynchronousQueue#put} (producer)
+ * and {@link SynchronousQueue#take} (consumer). This tracking is essential
+ * because {@code SynchronousQueue} operations block <em>indefinitely</em>
+ * when the counterpart is absent:
+ * <ul>
+ * <li>If the consumer exits the {@code for await} loop early (via
+ * {@code break}, {@code return}, or an exception), the producer may
+ * still be blocked in {@code queue.put()}. Without a reference to the
+ * producer thread, there is no way to unblock it, resulting in a
+ * <b>permanent thread leak</b>.</li>
+ * <li>Conversely, if the producer finishes but the consumer is still
+ * blocked in {@code queue.take()} (e.g. an external cancellation
+ * closed the stream), the consumer thread would also leak.</li>
+ * </ul>
+ * When {@link #close()} is called (typically from the compiler-generated
+ * {@code finally} block of a {@code for await} loop), it atomically sets the
+ * {@link #closed} flag and then interrupts both the producer and consumer
+ * threads (if any), causing their blocking {@code SynchronousQueue} operations
+ * to throw {@link InterruptedException}. The interrupted methods detect the
+ * closed state and exit gracefully — the producer's {@link #yield} throws
+ * {@link java.util.concurrent.CancellationException}, while the consumer's
+ * {@link #moveNext()} returns {@code Awaitable.of(false)}.
+ * <p>
+ * A <em>double-check</em> pattern in {@link #moveNext()} closes a TOCTOU race
+ * window: the {@link #closed} flag is re-checked <em>after</em> registering the
+ * consumer thread, ensuring that an externally-triggered {@code close()} cannot
+ * slip between the initial check and the registration, which would otherwise
+ * leave the consumer blocked forever in {@code queue.take()}.
+ * <p>
+ * The {@link #attachProducer}/{@link #detachProducer} lifecycle methods are
+ * called by the async runtime (in {@link AsyncSupport}) to register and
+ * unregister the producer thread. The consumer thread is tracked
+ * automatically inside {@link #moveNext()}.
+ * <p>
+ * This class is an internal implementation detail and should not be referenced
+ * directly by user code.
+ *
+ * @param <T> the element type
+ * @since 6.0.0
+ */
+public class AsyncStreamGenerator<T> extends AbstractAsyncStream<T> {
+
+ private final AtomicReference<Thread> producerThread = new AtomicReference<>();
+ private final AtomicReference<Thread> consumerThread = new AtomicReference<>();
+
+ public AsyncStreamGenerator() {
+ super(new SynchronousQueue<>());
+ }
+
+ /**
+ * Registers the given thread as the producer for this stream.
+ * Called by the async runtime immediately after the producer
+ * thread starts, <em>before</em> the generator body executes.
+ * <p>
+ * If the stream has already been {@linkplain #close() closed} by the
+ * time this method runs, the thread is immediately interrupted so
+ * that the generator body can exit promptly.
+ *
+ * @param thread the producer thread to register
+ */
+ void attachProducer(Thread thread) {
+ producerThread.set(thread);
+ if (closed.get()) {
+ thread.interrupt();
+ }
+ }
+
+ /**
+ * Unregisters the given thread as the producer for this stream.
+ * Called from a {@code finally} block after the generator body
+ * completes (normally or exceptionally).
+ *
+ * @param thread the producer thread to unregister
+ */
+ void detachProducer(Thread thread) {
+ producerThread.compareAndSet(thread, null);
+ }
+
+ /**
+ * Produces the next element. Called from the generator body when
+ * a {@code yield return expr} statement is executed. Blocks until
+ * the consumer is ready.
+ */
+ public void yield(Object value) {
+ if (closed.get()) {
+ throw streamClosed(null);
+ }
+ try {
+ queue.put(new ValueSignal(value));
+ } catch (InterruptedException e) {
+ if (closed.get()) {
+ throw streamClosed(e);
+ }
+ Thread.currentThread().interrupt();
+ throw newCancellationException("Interrupted during yield", e);
+ }
+ }
+
+ /**
+ * Signals that the generator has completed (no more elements).
+ * <p>
+ * If the blocking {@code queue.put()} is interrupted, a best-effort
+ * non-blocking {@code queue.offer()} is attempted. If that also fails
+ * (no consumer is currently blocked in {@code take()}), the stream is
+ * force-closed to prevent the consumer from blocking indefinitely on a
+ * subsequent {@link #moveNext()} call. This defensive close ensures no
+ * thread leak occurs even under unexpected interrupt timing.
+ */
+ public void complete() {
+ putTerminalSignal(COMPLETE);
+ }
+
+ /**
+ * Signals that the generator failed with an exception.
+ * <p>
+ * If the blocking {@code queue.put()} is interrupted, a best-effort
+ * non-blocking {@code queue.offer()} is attempted. If that also fails,
+ * the stream is force-closed (same rationale as {@link #complete()}).
+ * The original error is not propagated to the consumer in this edge case;
+ * instead the consumer sees a clean stream closure — this is acceptable
+ * because the interrupt itself indicates an external cancellation.
+ */
+ public void error(Throwable t) {
+ putTerminalSignal(new ErrorSignal(t != null ? t : new NullPointerException("null error in generator")));
+ }
+
+ /**
+ * Shared logic for {@link #complete()} and {@link #error(Throwable)}:
+ * attempts a blocking {@code put()}, falling back to non-blocking
+ * {@code offer()} on interrupt, and force-closing if both fail.
+ */
+ private void putTerminalSignal(Object signal) {
+ if (closed.get()) {
+ return;
+ }
+ try {
+ queue.put(signal);
+ } catch (InterruptedException e) {
+ if (!closed.get()) {
+ Thread.currentThread().interrupt();
+ if (!queue.offer(signal)) {
+ close();
+ }
+ }
+ }
+ }
+
+ // ---- Template method overrides ----
+
+ /**
+ * Enforces single-consumer semantics via CAS on {@link #consumerThread},
+ * with a double-check of the {@link #closed} flag to close the TOCTOU
+ * race window (see class-level javadoc).
+ */
+ @Override
+ protected Awaitable<Boolean> beforeTake() {
+ if (closed.get()) {
+ return MOVE_NEXT_FALSE;
+ }
+ Thread ct = Thread.currentThread();
+ if (!consumerThread.compareAndSet(null, ct)) {
+ Thread existing = consumerThread.get();
+ if (existing != ct) {
+ throw new IllegalStateException(
+ "AsyncStream does not support concurrent consumers. "
+ + "Current consumer: " + existing);
+ }
+ }
+ // Double-check: if close() raced between the first closed check and
+ // consumerThread CAS, the consumer reference was not yet visible to
+ // close(), so no interrupt was delivered.
+ if (closed.get()) {
+ consumerThread.compareAndSet(ct, null);
+ return MOVE_NEXT_FALSE;
+ }
+ return null;
+ }
+
+ /**
+ * Unregisters the consumer thread after every {@link #moveNext()} attempt.
+ */
+ @Override
+ protected void afterMoveNext() {
+ consumerThread.compareAndSet(Thread.currentThread(), null);
+ }
+
+ /**
+ * If the stream was already closed (cooperative cancellation), returns
+ * {@code MOVE_NEXT_FALSE}; otherwise restores the interrupt flag and
+ * throws {@link CancellationException}.
+ */
+ @Override
+ protected Awaitable<Boolean> onMoveNextInterrupted(InterruptedException e) {
+ if (closed.get()) {
+ return MOVE_NEXT_FALSE;
+ }
+ Thread.currentThread().interrupt();
+ throw newCancellationException("Interrupted during moveNext", e);
+ }
+
+ /**
+ * Interrupts both producer and consumer threads (if any) to unblock
+ * pending {@link SynchronousQueue} operations. A thread is never
+ * self-interrupted.
+ */
+ @Override
+ protected void onClose() {
+ Thread producer = producerThread.getAndSet(null);
+ if (producer != null && producer != Thread.currentThread()) {
+ producer.interrupt();
+ }
+ Thread consumer = consumerThread.getAndSet(null);
+ if (consumer != null && consumer != Thread.currentThread()) {
+ consumer.interrupt();
+ }
+ }
+
+ private static CancellationException streamClosed(InterruptedException cause) {
+ CancellationException ce = new CancellationException("Async stream was closed");
+ if (cause != null) {
+ ce.initCause(cause);
+ }
+ return ce;
+ }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java b/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java
new file mode 100644
index 0000000000..20f13ba991
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java
@@ -0,0 +1,1139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.runtime.async;
+
+import groovy.concurrent.AsyncContext;
+import groovy.concurrent.AsyncScope;
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.AwaitResult;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.Channel;
+import groovy.lang.Closure;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Internal runtime support for the {@code async}/{@code await} language feature
+ * and the {@link groovy.transform.Async @Async} annotation.
+ * <p>
+ * This class contains the actual implementation invoked by compiler-generated
+ * code. User code should prefer the static methods on
+ * {@link groovy.concurrent.Awaitable} (e.g., {@code Awaitable.all()},
+ * {@code Awaitable.delay()}) for combinators and configuration.
+ * <p>
+ * Key responsibilities:
+ * <ul>
+ * <li><b>Async execution</b> — {@code executeAsync()}, {@code executeAsyncVoid()},
+ * {@code wrapAsync()}, and {@code go()} run closures on the configured
+ * executor while preserving the current {@link AsyncContext}</li>
+ * <li><b>Await</b> — all {@code await()} overloads use
+ * {@link groovy.concurrent.Awaitable#from(Object) Awaitable.from()} so
+ * that third-party async types
+ * (RxJava {@code Single}, Reactor {@code Mono}, etc.) are supported
+ * transparently once an adapter is registered</li>
+ * <li><b>Async generators</b> — {@code generateAsyncStream()} manages the
+ * producer/consumer bridge for methods using {@code yield return}</li>
+ * <li><b>Async stream conversion</b> — {@code toAsyncStream()} adapts
+ * collections, arrays, and adapter-supported types for {@code for await}</li>
+ * <li><b>Async stream cleanup</b> — {@code closeStream()} lets compiler-generated
+ * {@code for await} loops release resources and propagate early-exit
+ * cancellation upstream</li>
+ * <li><b>Defer</b> — {@code createDeferScope()}, {@code defer()}, and
+ * {@code executeDeferScope()} implement deferred cleanup with
+ * LIFO execution, async cleanup awareness, and exception suppression</li>
+ * <li><b>Channels</b> — {@code channel()} creates asynchronous channels that
+ * compose with {@link Awaitable#any(Object...)} / {@code select()}</li>
+ * <li><b>Delay</b> — {@code delay()} provides non-blocking delays using a
+ * shared {@link java.util.concurrent.ScheduledExecutorService}</li>
+ * <li><b>Timeouts</b> — {@code orTimeout()} and {@code completeOnTimeout()} apply
+ * non-blocking deadlines while preserving the {@link Awaitable} abstraction</li>
+ * </ul>
+ * <p>
+ * <b>Thread pool configuration</b>
+ * <ul>
+ * <li>On JDK 21+ the default executor is a virtual-thread-per-task executor
+ * obtained via {@code Executors.newVirtualThreadPerTaskExecutor()}.
+ * Virtual threads make blocking within {@code await()} essentially free:
+ * the JVM parks the virtual thread and releases the underlying carrier
+ * thread, achieving efficiency comparable to state-machine-based
+ * approaches without requiring compiler-level control-flow rewriting.</li>
+ * <li>On earlier JDKs the fallback is a cached (elastic) daemon thread pool
+ * whose maximum size is controlled by the system property
+ * {@code groovy.async.parallelism} (default: {@code 256}). Idle threads
+ * are reaped after 60 seconds. A cached pool is used instead of a
+ * fixed pool to prevent thread starvation when async tasks block
+ * (e.g., in generators or nested {@code await} calls). A
+ * {@link java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
+ * CallerRunsPolicy} is used so that task submission never fails
+ * with {@link java.util.concurrent.RejectedExecutionException} when
+ * the pool is saturated.</li>
+ * <li>The executor can be overridden at any time via {@link #setExecutor}.</li>
+ * </ul>
+ * <p>
+ * <b>Exception handling</b> follows a transparency principle: the
+ * <em>original</em> exception is rethrown without being wrapped
+ * in an {@link java.util.concurrent.ExecutionException ExecutionException} or
+ * {@link java.util.concurrent.CompletionException CompletionException}.
+ *
+ * @see groovy.concurrent.Awaitable
+ * @see groovy.transform.Async
+ * @since 6.0.0
+ */
+public class AsyncSupport {
+
+ /**
+ * {@code true} if the running JVM supports virtual threads (JDK 21+).
+ * Detected once at class-load time via {@link MethodHandle} reflection.
+ */
+ private static final boolean VIRTUAL_THREADS_AVAILABLE;
+
+ private static final Executor VIRTUAL_THREAD_EXECUTOR;
+
+ /**
+ * Maximum thread pool size when virtual threads are unavailable.
+ * Configurable via the system property {@code groovy.async.parallelism}.
+ * Defaults to {@code 256} to prevent runaway thread creation while still
+ * allowing ample concurrency for blocking async operations.
+ */
+ private static final int FALLBACK_MAX_THREADS;
+
+ private static final Executor FALLBACK_EXECUTOR;
+
+ static {
+ Executor vtExecutor = null;
+ boolean vtAvailable = false;
+ try {
+ MethodHandle mh = MethodHandles.lookup().findStatic(
+ Executors.class, "newVirtualThreadPerTaskExecutor",
+ MethodType.methodType(ExecutorService.class));
+ vtExecutor = (Executor) mh.invoke();
+ vtAvailable = true;
+ } catch (Throwable ignored) {
+ // JDK < 21 — virtual threads not available
+ }
+ VIRTUAL_THREAD_EXECUTOR = vtExecutor;
+ VIRTUAL_THREADS_AVAILABLE = vtAvailable;
+
+ FALLBACK_MAX_THREADS = org.apache.groovy.util.SystemUtil.getIntegerSafe(
+ "groovy.async.parallelism", 256);
+ if (!VIRTUAL_THREADS_AVAILABLE) {
+ // Use a cached thread pool (threads created on demand, idle threads
+ // reaped after 60 s) to avoid thread starvation. Async/await relies
+ // on blocking operations (SynchronousQueue in generators, Future.get
+ // in await) — a fixed-size pool can deadlock when all threads block.
+ // CallerRunsPolicy provides graceful degradation when all threads
+ // are busy: the submitting thread runs the task itself, preventing
+ // RejectedExecutionException at the cost of temporary back-pressure.
+ FALLBACK_EXECUTOR = new ThreadPoolExecutor(
+ 0, FALLBACK_MAX_THREADS,
+ 60L, TimeUnit.SECONDS,
+ new SynchronousQueue<>(),
+ r -> {
+ Thread t = new Thread(r);
+ t.setDaemon(true);
+ t.setName("groovy-async-" + t.getId());
+ return t;
+ },
+ new ThreadPoolExecutor.CallerRunsPolicy());
+ } else {
+ FALLBACK_EXECUTOR = null;
+ }
+ }
+
+ private static volatile Executor defaultExecutor =
+ VIRTUAL_THREADS_AVAILABLE ? VIRTUAL_THREAD_EXECUTOR : FALLBACK_EXECUTOR;
+
+ private AsyncSupport() { }
+
+ // ---- await overloads ------------------------------------------------
+
+ /**
+ * Awaits the result of an {@link Awaitable}.
+ * <p>
+ * Blocks the calling thread until the computation completes and returns
+ * its result. If the computation failed, the original exception is
+ * rethrown transparently (even if it is a checked exception). If the
+ * waiting thread is interrupted, an {@link java.util.concurrent.CancellationException}
+ * is thrown and the interrupt flag is restored.
+ *
+ * @param awaitable the computation to await
+ * @param <T> the result type
+ * @return the computed result
+ */
+ public static <T> T await(Awaitable<T> awaitable) {
+ try {
+ return awaitable.get();
+ } catch (ExecutionException e) {
+ throw rethrowUnwrapped(e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ CancellationException ce = new CancellationException("Interrupted while awaiting");
+ ce.initCause(e);
+ throw ce;
+ }
+ }
+
+ /**
+ * Awaits the result of a {@link CompletableFuture}.
+ * Uses {@link CompletableFuture#join()} for non-interruptible waiting.
+ *
+ * @param future the future to await
+ * @param <T> the result type
+ * @return the computed result
+ */
+ public static <T> T await(CompletableFuture<T> future) {
+ try {
+ return future.join();
+ } catch (CompletionException e) {
+ throw rethrowUnwrapped(e);
+ } catch (CancellationException e) {
+ throw e;
+ }
+ }
+
+ /**
+ * Awaits the result of a {@link CompletionStage} by converting it to a
+ * {@link CompletableFuture} first.
+ *
+ * @param stage the completion stage to await
+ * @param <T> the result type
+ * @return the computed result
+ */
+ public static <T> T await(CompletionStage<T> stage) {
+ return await(stage.toCompletableFuture());
+ }
+
+ /**
+ * Awaits the result of a {@link Future}. If the future is a
+ * {@link CompletableFuture}, delegates to the more efficient
+ * {@link #await(CompletableFuture)} overload.
+ *
+ * @param future the future to await
+ * @param <T> the result type
+ * @return the computed result
+ */
+ public static <T> T await(Future<T> future) {
+ if (future instanceof CompletableFuture<T> cf) {
+ return await(cf);
+ }
+ try {
+ return future.get();
+ } catch (ExecutionException e) {
+ throw rethrowUnwrapped(e);
+ } catch (CancellationException e) {
+ throw e;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ CancellationException ce = new CancellationException("Interrupted while awaiting future");
+ ce.initCause(e);
+ throw ce;
+ }
+ }
+
+ /**
+ * Awaits an arbitrary object by adapting it to {@link Awaitable} via
+ * {@link Awaitable#from(Object)}. This is the fallback overload called
+ * by the {@code await} expression when the compile-time type is not one
+ * of the other supported types. Returns {@code null} for a {@code null}
+ * argument.
+ *
+ * @param source the object to await
+ * @param <T> the result type
+ * @return the computed result, or {@code null} if {@code source} is {@code null}
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T await(Object source) {
+ if (source == null) return null;
+ if (source instanceof Awaitable) return await((Awaitable<T>) source);
+ if (source instanceof CompletableFuture) return await((CompletableFuture<T>) source);
+ if (source instanceof CompletionStage) return await((CompletionStage<T>) source);
+ if (source instanceof Future) return await((Future<T>) source);
+ if (source instanceof Closure) return awaitClosure((Closure<?>) source);
+ return await(Awaitable.<T>from(source));
+ }
+
+ /**
+ * Handles the case where a {@link Closure} is passed to {@code await}.
+ * <p>
+ * An {@code async} closure or lambda must be explicitly called before
+ * awaiting. For example, {@code await(myClosure())} is correct, while
+ * {@code await(myClosure)} is not. This method always throws an
+ * {@link IllegalArgumentException} with guidance on the correct usage.
+ *
+ * @param closure the closure that was incorrectly passed to await
+ * @param <T> the result type (never actually returned)
+ * @return never returns
+ * @throws IllegalArgumentException always
+ */
+ @SuppressWarnings("unchecked")
+ private static <T> T awaitClosure(Closure<?> closure) {
+ throw new IllegalArgumentException(
+ "Cannot await a Closure directly. "
+ + "Call the closure first, then await the result: "
+ + "await(myClosure()) or await(myClosure(args))");
+ }
+
+ // ---- async ----------------------------------------------------------
+
+ /**
+ * Executes the given closure asynchronously on the specified executor,
+ * returning an {@link Awaitable} that completes with the closure's return
+ * value. Used by {@link groovy.transform.Async @Async} methods.
+ * <p>
+ * Captures the current {@link AsyncContext} at submission time and
+ * restores that snapshot when the closure runs.
+ *
+ * @param closure the closure to execute
+ * @param executor the executor on which to run the closure
+ * @param <T> the result type
+ * @return an awaitable representing the pending computation
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Awaitable<T> executeAsync(Closure<T> closure, Executor executor) {
+ Objects.requireNonNull(closure, "closure must not be null");
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ Executor targetExecutor = executor != null ? executor : defaultExecutor;
+ return GroovyPromise.of(CompletableFuture.supplyAsync(() ->
+ AsyncContext.withSnapshot(contextSnapshot, () -> {
+ try {
+ return closure.call();
+ } catch (Throwable t) {
+ throw wrapForFuture(t);
+ }
+ }), targetExecutor));
+ }
+
+ /**
+ * Void variant of {@link #executeAsync(Closure, Executor)} for
+ * {@link groovy.transform.Async @Async} methods whose return type is
+ * {@code void}.
+ * <p>
+ * Captures and restores the current {@link AsyncContext} in the same way
+ * as {@link #executeAsync(Closure, Executor)}.
+ *
+ * @param closure the closure to execute
+ * @param executor the executor on which to run the closure
+ * @return an awaitable that completes when the closure finishes
+ */
+ public static Awaitable<Void> executeAsyncVoid(Closure<?> closure, Executor executor) {
+ Objects.requireNonNull(closure, "closure must not be null");
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ Executor targetExecutor = executor != null ? executor : defaultExecutor;
+ return GroovyPromise.of(CompletableFuture.runAsync(() ->
+ AsyncContext.withSnapshot(contextSnapshot, () -> {
+ try {
+ closure.call();
+ } catch (Throwable t) {
+ throw wrapForFuture(t);
+ }
+ }), targetExecutor));
+ }
+
+ /**
+ * Executes the given closure asynchronously using the default executor,
+ * returning an {@link Awaitable}. This method is used internally by the
+ * {@link groovy.transform.Async @Async} annotation transformation and
+ * the {@code async} method modifier. For the {@code async { ... }}
+ * closure expression syntax, see {@link #wrapAsync(Closure)}.
+ *
+ * @param closure the closure to execute
+ * @param <T> the result type
+ * @return an awaitable representing the pending computation
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Awaitable<T> async(Closure<T> closure) {
+ return executeAsync(closure, defaultExecutor);
+ }
+
+ /**
+ * Task spawn primitive.
+ * <p>
+ * When called inside {@link AsyncScope#withScope(groovy.lang.Closure)},
+ * the new task is registered as a structured child of the current scope.
+ * Outside a scope it behaves like a lightweight detached async task.
+ *
+ * @param closure the task body
+ * @param <T> the result type
+ * @return an awaitable representing the spawned task
+ */
+ public static <T> Awaitable<T> go(Closure<T> closure) {
+ AsyncScope currentScope = AsyncScope.current();
+ if (currentScope != null) {
+ return currentScope.async(closure);
+ }
+ return executeAsync(closure, defaultExecutor);
+ }
+
+ /**
+ * Wraps a closure so that each invocation executes the body asynchronously
+ * and returns an {@link Awaitable}. This is the runtime entry point for
+ * the {@code async { ... }} expression syntax. The returned closure must
+ * be explicitly called to start the async computation. Each invocation
+ * captures the caller's {@link AsyncContext} and restores it when the
+ * wrapped closure executes:
+ * <pre>
+ * def task = async { expensiveWork() }
+ * def result = await(task()) // explicit call required
+ * </pre>
+ *
+ * @param closure the closure to wrap
+ * @param <T> the result type
+ * @return a new closure whose calls produce awaitables
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Closure<Awaitable<T>> wrapAsync(Closure<T> closure) {
+ Objects.requireNonNull(closure, "closure must not be null");
+ return new Closure<Awaitable<T>>(closure.getOwner(), closure.getThisObject()) {
+ @SuppressWarnings("unused")
+ public Awaitable<T> doCall(Object... args) {
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ return GroovyPromise.of(CompletableFuture.supplyAsync(() ->
+ AsyncContext.withSnapshot(contextSnapshot, () -> {
+ try {
+ return closure.call(args);
+ } catch (Throwable t) {
+ throw wrapForFuture(t);
+ }
+ }), defaultExecutor));
+ }
+ };
+ }
+
+ /**
+ * Wraps a generator closure (one containing {@code yield return} statements)
+ * so that each invocation returns an {@link AsyncStream} producing the
+ * yielded elements. This is the runtime entry point for all async
+ * generator closures ({@code async { yield return ... }}). The returned
+ * closure must be explicitly called to start the generation. As with
+ * ordinary async closures, each invocation captures the caller's
+ * {@link AsyncContext} snapshot:
+ * <pre>
+ * def gen = async { yield return 1; yield return 2 }
+ * for await (item in gen()) { println item }
+ * </pre>
+ *
+ * @param closure the generator closure to wrap
+ * @param <T> the element type
+ * @return a new closure whose calls produce async streams
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Closure<AsyncStream<T>> wrapAsyncGenerator(Closure<?> closure) {
+ Objects.requireNonNull(closure, "closure must not be null");
+ return new Closure<AsyncStream<T>>(closure.getOwner(), closure.getThisObject()) {
+ @SuppressWarnings("unused")
+ public AsyncStream<T> doCall(Object... args) {
+ AsyncStreamGenerator<T> gen = new AsyncStreamGenerator<>();
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ CompletableFuture.runAsync(() -> AsyncContext.withSnapshot(contextSnapshot, () -> {
+ gen.attachProducer(Thread.currentThread());
+ try {
+ Object[] allArgs = new Object[args.length + 1];
+ allArgs[0] = gen;
+ System.arraycopy(args, 0, allArgs, 1, args.length);
+ closure.call(allArgs);
+ gen.complete();
+ } catch (Throwable t) {
+ gen.error(t);
+ } finally {
+ gen.detachProducer(Thread.currentThread());
+ }
+ }), defaultExecutor);
+ return gen;
+ }
+ };
+ }
+
+ // ---- awaitAll / awaitAny / awaitAllSettled ---------------------------
+
+ /**
+ * Waits for all given awaitables to complete and returns their results
+ * as a list, preserving order. If any awaitable fails, the exception is
+ * rethrown immediately.
+ * <p>
+ * Returns an empty list if {@code awaitables} is {@code null} or empty.
+ *
+ * @param awaitables the awaitables (or futures, completion stages, etc.);
+ * elements must not be {@code null}
+ * @return a list of results in the same order as the arguments
+ * @throws IllegalArgumentException if any element is {@code null}
+ */
+ @SuppressWarnings("unchecked")
+ public static List<Object> awaitAll(Object... awaitables) {
+ if (awaitables == null || awaitables.length == 0) {
+ return new ArrayList<>();
+ }
+ CompletableFuture<?>[] futures = toCompletableFutures(awaitables, "awaitAll");
+ try {
+ CompletableFuture.allOf(futures).join();
+ } catch (CompletionException e) {
+ throw rethrowUnwrapped(e);
+ }
+ List<Object> results = new ArrayList<>(futures.length);
+ for (CompletableFuture<?> f : futures) {
+ results.add(f.join());
+ }
+ return results;
+ }
+
+ /**
+ * Waits for any one of the given awaitables to complete and returns
+ * its result.
+ *
+ * @param awaitables the awaitables to race; must not be {@code null},
+ * empty, or contain {@code null} elements
+ * @return the result of the first awaitable that completes
+ * @throws IllegalArgumentException if {@code awaitables} is {@code null},
+ * empty, or contains {@code null} elements
+ */
+ @SuppressWarnings("unchecked")
+ public static Object awaitAny(Object... awaitables) {
+ if (awaitables == null || awaitables.length == 0) {
+ throw new IllegalArgumentException("awaitAny requires at least one awaitable");
+ }
+ CompletableFuture<?>[] futures = toCompletableFutures(awaitables, "awaitAny");
+ try {
+ return CompletableFuture.anyOf(futures).join();
+ } catch (CompletionException e) {
+ throw rethrowUnwrapped(e);
+ }
+ }
+
+ /**
+ * Waits for all given awaitables to settle (complete either successfully
+ * or with an exception) and returns a list of {@link AwaitResult} objects.
+ * Never throws for individual failures; failures are captured in the result list.
+ * <p>
+ * Returns an empty list if {@code awaitables} is {@code null} or empty.
+ *
+ * @param awaitables the awaitables to settle; elements must not be {@code null}
+ * @return a list of results in the same order as the arguments
+ * @throws IllegalArgumentException if any element is {@code null}
+ */
+ @SuppressWarnings("unchecked")
+ public static List<AwaitResult<Object>> awaitAllSettled(Object... awaitables) {
+ if (awaitables == null || awaitables.length == 0) {
+ return new ArrayList<>();
+ }
+ CompletableFuture<?>[] futures = toCompletableFutures(awaitables, "awaitAllSettled");
+ CompletableFuture.allOf(
+ Arrays.stream(futures)
+ .map(f -> f.handle((v, t) -> null))
+ .toArray(CompletableFuture[]::new)
+ ).join();
+
+ return getAwaitResults(futures);
+ }
+
+ /** Converts an arbitrary source to a {@link CompletableFuture} for internal use. */
+ private static CompletableFuture<?> toCompletableFuture(Object source) {
+ if (source instanceof CompletableFuture<?> cf) return cf;
+ if (source instanceof Awaitable<?> a) return a.toCompletableFuture();
+ if (source instanceof CompletionStage<?> cs) return cs.toCompletableFuture();
+ return Awaitable.from(source).toCompletableFuture();
+ }
+
+ /**
+ * Validates that no element is {@code null} and converts all elements to
+ * {@link CompletableFuture}s in one pass. Used by combinator methods
+ * ({@code awaitAll}, {@code awaitAny}, {@code awaitAllSettled}, and their
+ * non-blocking {@code Async} variants) to eliminate duplicate
+ * null-checking and conversion loops.
+ *
+ * @param sources the source objects to validate and convert
+ * @param callerName the method name for error messages
+ * @return an array of completable futures corresponding to the sources
+ * @throws IllegalArgumentException if any element is {@code null}
+ */
+ private static CompletableFuture<?>[] toCompletableFutures(Object[] sources, String callerName) {
+ CompletableFuture<?>[] futures = new CompletableFuture<?>[sources.length];
+ for (int i = 0; i < sources.length; i++) {
+ if (sources[i] == null) {
+ throw new IllegalArgumentException(callerName + ": element at index " + i + " is null");
+ }
+ futures[i] = toCompletableFuture(sources[i]);
+ }
+ return futures;
+ }
+
+ // ---- non-blocking combinators (return Awaitable) --------------------
+
+ /**
+ * Non-blocking variant of {@link #awaitAll(Object...)} that returns an
+ * {@link Awaitable} instead of blocking. Used by
+ * {@link Awaitable#all(Object...)}.
+ */
+ public static Awaitable<List<Object>> allAsync(Object... sources) {
+ if (sources == null || sources.length == 0) {
+ return Awaitable.of(new ArrayList<>());
+ }
+ CompletableFuture<?>[] futures = toCompletableFutures(sources, "Awaitable.all");
+ CompletableFuture<List<Object>> combined = CompletableFuture.allOf(futures)
+ .thenApply(v -> {
+ List<Object> results = new ArrayList<>(futures.length);
+ for (CompletableFuture<?> f : futures) {
+ results.add(f.join());
+ }
+ return results;
+ });
+ return GroovyPromise.of(combined);
+ }
+
+ /**
+ * Non-blocking variant of {@link #awaitAny(Object...)} that returns an
+ * {@link Awaitable} instead of blocking. Used by
+ * {@link Awaitable#any(Object...)}.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Awaitable<T> anyAsync(Object... sources) {
+ if (sources == null || sources.length == 0) {
+ throw new IllegalArgumentException("Awaitable.any requires at least one source");
+ }
+ CompletableFuture<?>[] futures = toCompletableFutures(sources, "Awaitable.any");
+ return GroovyPromise.of((CompletableFuture<T>) CompletableFuture.anyOf(futures));
+ }
+
+ /**
+ * Non-blocking variant of {@link #awaitAllSettled(Object...)} that returns an
+ * {@link Awaitable} instead of blocking. Used by
+ * {@link Awaitable#allSettled(Object...)}.
+ */
+ public static Awaitable<List<AwaitResult<Object>>> allSettledAsync(Object... sources) {
+ if (sources == null || sources.length == 0) {
+ return Awaitable.of(new ArrayList<>());
+ }
+ CompletableFuture<?>[] futures = toCompletableFutures(sources, "Awaitable.allSettled");
+ // Wait for all to settle (handle converts failures to non-exceptional completions)
+ CompletableFuture<List<AwaitResult<Object>>> combined = CompletableFuture.allOf(
+ Arrays.stream(futures)
+ .map(f -> f.handle((v, t) -> null))
+ .toArray(CompletableFuture[]::new)
+ ).thenApply(v -> getAwaitResults(futures));
+ return GroovyPromise.of(combined);
+ }
+
+ private static List<AwaitResult<Object>> getAwaitResults(CompletableFuture<?>[] futures) {
+ List<AwaitResult<Object>> results = new ArrayList<>(futures.length);
+ for (CompletableFuture<?> f : futures) {
+ try {
+ results.add(AwaitResult.success(f.join()));
+ } catch (CompletionException e) {
+ results.add(AwaitResult.failure(deepUnwrap(e)));
+ } catch (CancellationException e) {
+ results.add(AwaitResult.failure(e));
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Applies fail-fast timeout semantics to the given source.
+ * Returns an awaitable that fails with {@link TimeoutException} if the
+ * source does not complete before the timeout elapses.
+ * <p>
+ * The timeout does <em>not</em> cancel the original source automatically.
+ * This mirrors value-level race composition and keeps cancellation explicit.
+ *
+ * @param source the async source to time out
+ * @param timeout the timeout duration
+ * @param unit the timeout unit
+ * @param <T> the result type
+ * @return an awaitable that fails on timeout
+ * @throws IllegalArgumentException if any argument is invalid
+ * @since 6.0.0
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Awaitable<T> orTimeout(Object source, long timeout, TimeUnit unit) {
+ validateTimeoutArguments(source, timeout, unit, "Awaitable.orTimeout");
+ CompletableFuture<T> sourceFuture = (CompletableFuture<T>) toCompletableFuture(source);
+ CompletableFuture<T> result = new CompletableFuture<>();
+ TimeoutException te = new TimeoutException(
+ "Timed out after " + timeout + " " + unit.name().toLowerCase(Locale.ROOT));
+ scheduleTimeoutRace(sourceFuture, result, () -> result.completeExceptionally(te), timeout, unit);
+ return GroovyPromise.of(result);
+ }
+
+ /**
+ * Millisecond shortcut for {@link #orTimeout(Object, long, TimeUnit)}.
+ *
+ * @param source the async source to time out
+ * @param timeoutMillis timeout in milliseconds
+ * @param <T> result type
+ * @return an awaitable that fails on timeout
+ * @since 6.0.0
+ */
+ public static <T> Awaitable<T> orTimeoutMillis(Object source, long timeoutMillis) {
+ return orTimeout(source, timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Applies fallback-on-timeout semantics to the given source.
+ * Returns an awaitable that completes with the supplied fallback value if
+ * the source has not completed before the timeout elapses.
+ *
+ * @param source the async source to wait for
+ * @param fallback fallback value when timeout elapses first
+ * @param timeout the timeout duration
+ * @param unit the timeout unit
+ * @param <T> the result type
+ * @return an awaitable that yields either source result or fallback value
+ * @throws IllegalArgumentException if any argument is invalid
+ * @since 6.0.0
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Awaitable<T> completeOnTimeout(Object source, T fallback, long timeout, TimeUnit unit) {
+ validateTimeoutArguments(source, timeout, unit, "Awaitable.completeOnTimeout");
+ CompletableFuture<T> sourceFuture = (CompletableFuture<T>) toCompletableFuture(source);
+ CompletableFuture<T> result = new CompletableFuture<>();
+ scheduleTimeoutRace(sourceFuture, result, () -> result.complete(fallback), timeout, unit);
+ return GroovyPromise.of(result);
+ }
+
+ /**
+ * Millisecond shortcut for {@link #completeOnTimeout(Object, Object, long, TimeUnit)}.
+ *
+ * @param source the async source to wait for
+ * @param fallback fallback value when timeout elapses first
+ * @param timeoutMillis timeout in milliseconds
+ * @param <T> result type
+ * @return an awaitable that yields either source result or fallback
+ * @since 6.0.0
+ */
+ public static <T> Awaitable<T> completeOnTimeoutMillis(Object source, T fallback, long timeoutMillis) {
+ return completeOnTimeout(source, fallback, timeoutMillis, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Shared logic for {@link #orTimeout} and {@link #completeOnTimeout}:
+ * schedules the timeout action and wires up source completion to cancel
+ * the timer and propagate the result or error.
+ */
+ private static <T> void scheduleTimeoutRace(
+ CompletableFuture<T> sourceFuture,
+ CompletableFuture<T> result,
+ Runnable onTimeout,
+ long duration, TimeUnit unit) {
+ ScheduledFuture<?> timeoutTask = DELAY_SCHEDULER.schedule(onTimeout, duration, unit);
+ sourceFuture.whenComplete((value, error) -> {
+ timeoutTask.cancel(false);
+ if (error != null) {
+ result.completeExceptionally(deepUnwrap(error));
+ } else {
+ result.complete(value);
+ }
+ });
+ }
+
+ private static void validateTimeoutArguments(Object source, long duration, TimeUnit unit, String methodName) {
+ if (source == null) {
+ throw new IllegalArgumentException(methodName + ": source must not be null");
+ }
+ if (unit == null) {
+ throw new IllegalArgumentException(methodName + ": TimeUnit must not be null");
+ }
+ if (duration < 0) {
+ throw new IllegalArgumentException(methodName + ": duration must not be negative: " + duration);
+ }
+ }
+
+ // ---- delay ----------------------------------------------------------
+
+ /**
+ * Shared single-thread scheduler for {@link #delay} operations.
+ * Uses a daemon thread so it does not prevent JVM shutdown.
+ */
+ private static final ScheduledExecutorService DELAY_SCHEDULER =
+ Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "groovy-async-delay");
+ t.setDaemon(true);
+ return t;
+ });
+
+ /**
+ * Returns an {@link Awaitable} that completes (with {@code null}) after
+ * the specified delay.
+ * <p>
+ * This is a non-blocking delay: the calling thread is not blocked.
+ * Use {@code await delay(ms)} inside an {@code async} context to pause
+ * the async workflow for the given duration without blocking a thread.
+ *
+ * @param milliseconds the delay in milliseconds (must be ≥ 0)
+ * @return an awaitable that completes after the delay
+ * @throws IllegalArgumentException if {@code milliseconds} is negative
+ */
+ public static Awaitable<Void> delay(long milliseconds) {
+ return delay(milliseconds, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Returns an {@link Awaitable} that completes (with {@code null}) after
+ * the specified delay in the given time unit.
+ *
+ * @param duration the delay duration (must be ≥ 0)
+ * @param unit the time unit (must not be {@code null})
+ * @return an awaitable that completes after the delay
+ * @throws IllegalArgumentException if {@code duration} is negative or
+ * {@code unit} is {@code null}
+ */
+ public static Awaitable<Void> delay(long duration, TimeUnit unit) {
+ if (unit == null) {
+ throw new IllegalArgumentException("TimeUnit must not be null");
+ }
+ if (duration < 0) {
+ throw new IllegalArgumentException("delay duration must not be negative: " + duration);
+ }
+ if (duration == 0) {
+ return Awaitable.of(null);
+ }
+ CompletableFuture<Void> cf = new CompletableFuture<>();
+ DELAY_SCHEDULER.schedule(() -> cf.complete(null), duration, unit);
+ return GroovyPromise.of(cf);
+ }
+
+ // ---- for-await support ----------------------------------------------
+
+ /**
+ * Converts an arbitrary source to an {@link AsyncStream} via the adapter
+ * registry. Used by compiler-generated code for {@code for await} loops.
+ * Returns {@link AsyncStream#empty()} for {@code null} input to ensure
+ * null-safe iteration.
+ *
+ * @param source the source object, or {@code null}
+ * @param <T> the element type
+ * @return an async stream, never {@code null}
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> AsyncStream<T> toAsyncStream(Object source) {
+ if (source == null) return AsyncStream.empty();
+ if (source instanceof AsyncStream) return (AsyncStream<T>) source;
+ return AsyncStream.from(source);
+ }
+
+ /**
+ * Closes the given async stream if it is non-null.
+ * <p>
+ * Compiler-generated {@code for await} loops call this from a synthetic
+ * {@code finally} block so that early {@code break}, {@code return}, or
+ * exceptional exit reliably releases the underlying stream resources and
+ * propagates cancellation to upstream producers where supported.
+ *
+ * @param stream the stream to close, or {@code null}
+ * @since 6.0.0
+ */
+ public static void closeStream(Object stream) {
+ if (stream instanceof AsyncStream<?> asyncStream) {
+ asyncStream.close();
+ }
+ }
+
+ // ---- yield return / async generator ----------------------------------
+
+ /**
+ * Safety overload: if {@code yield return} is used outside an async
+ * generator context, this single-argument overload produces a clear
+ * error message at runtime.
+ *
+ * @param value ignored — the method always throws
+ * @throws IllegalStateException always
+ */
+ public static void yieldReturn(Object value) {
+ throw new IllegalStateException(
+ "yield return can only be used inside an async generator method or closure");
+ }
+
+ /**
+ * Called by compiler-generated code for {@code yield return expr} statements
+ * inside async generator methods and closures.
+ *
+ * @param generator the {@link AsyncStreamGenerator} instance (injected as a
+ * synthetic parameter by the compiler)
+ * @param value the value to yield to the consumer
+ * @throws IllegalStateException if {@code generator} is {@code null}
+ */
+ public static void yieldReturn(Object generator, Object value) {
+ if (generator == null) {
+ throw new IllegalStateException("yield return used outside of an async generator context");
+ }
+ ((AsyncStreamGenerator<?>) generator).yield(value);
+ }
+
+ /**
+ * Creates an {@link AsyncStream} from a generator closure. The closure
+ * body is executed asynchronously on the default executor; each
+ * {@code yield return} statement produces an element in the stream.
+ * This method is used internally by {@link groovy.transform.Async @Async}
+ * generator methods. For the {@code async { yield return ... }} closure
+ * expression syntax, see {@link #wrapAsyncGenerator(Closure)}.
+ * <p>
+ * The generator body executes with the {@link AsyncContext} snapshot that
+ * was active when generation started.
+ *
+ * @param body the generator closure (receives an {@link AsyncStreamGenerator}
+ * as its single argument)
+ * @param <T> the element type
+ * @return a live async stream backed by the running generator
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> AsyncStream<T> generateAsyncStream(Closure<?> body) {
+ Objects.requireNonNull(body, "body must not be null");
+ AsyncStreamGenerator<T> gen = new AsyncStreamGenerator<>();
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ CompletableFuture.runAsync(() -> AsyncContext.withSnapshot(contextSnapshot, () -> {
+ gen.attachProducer(Thread.currentThread());
+ try {
+ body.call(gen);
+ gen.complete();
+ } catch (Throwable t) {
+ gen.error(t);
+ } finally {
+ gen.detachProducer(Thread.currentThread());
+ }
+ }), defaultExecutor);
+ return gen;
+ }
+
+ /**
+ * Creates an unbuffered channel.
+ *
+ * @param <T> the payload type
+ * @return a new unbuffered channel
+ */
+ public static <T> Channel<T> channel() {
+ return new Channel<>();
+ }
+
+ /**
+ * Creates a channel with the given buffer capacity.
+ *
+ * @param capacity the channel buffer capacity
+ * @param <T> the payload type
+ * @return a new channel
+ */
+ public static <T> Channel<T> channel(int capacity) {
+ return new Channel<>(capacity);
+ }
+
+ // ---- executor configuration -----------------------------------------
+
+ /**
+ * Returns {@code true} if the running JVM supports virtual threads (JDK 21+).
+ */
+ public static boolean isVirtualThreadsAvailable() {
+ return VIRTUAL_THREADS_AVAILABLE;
+ }
+
+ /**
+ * Returns the current executor used for {@code async} operations.
+ *
+ * @return the active executor, never {@code null}
+ */
+ public static Executor getExecutor() {
+ return defaultExecutor;
+ }
+
+ /**
+ * Sets the executor to use for {@code async} operations. Pass {@code null}
+ * to reset to the default executor (virtual thread executor on JDK 21+,
+ * or a cached daemon thread pool whose maximum size is controlled by the
+ * system property {@code groovy.async.parallelism}).
+ *
+ * @param executor the custom executor, or {@code null} to reset
+ */
+ public static void setExecutor(Executor executor) {
+ defaultExecutor = executor != null
+ ? executor
+ : (VIRTUAL_THREADS_AVAILABLE ? VIRTUAL_THREAD_EXECUTOR : FALLBACK_EXECUTOR);
+ }
+
+ // ---- defer support -----------------------------------------------------
+
+ /**
+ * Creates a new defer scope — a LIFO list of {@link Closure} actions
+ * to be executed when the enclosing method returns. Called by
+ * compiler-generated code at the start of methods that contain
+ * {@code defer} statements.
+ *
+ * @return a new, empty defer scope (a mutable list of closures)
+ */
+ public static Deque<Closure<?>> createDeferScope() {
+ return new ArrayDeque<>();
+ }
+
+ /**
+ * Registers a deferred action in the given scope. The action will be
+ * executed in LIFO order when {@link #executeDeferScope} is called
+ * (typically in a {@code finally} block generated by the compiler).
+ * <p>
+ * This method is the runtime entry point for the {@code defer { ... }}
+ * statement, inspired by Go's {@code defer} keyword. The deferred action
+ * is bound to the current {@link AsyncContext} snapshot so that cleanup
+ * observes the same request/trace metadata that was active when the defer
+ * statement was executed.
+ *
+ * @param scope the defer scope (created by {@link #createDeferScope()});
+ * must not be {@code null}
+ * @param action the closure to execute on method exit;
+ * must not be {@code null}
+ * @throws IllegalStateException if {@code scope} is {@code null}
+ * (indicates {@code defer} used outside an async context)
+ * @throws IllegalArgumentException if {@code action} is {@code null}
+ */
+ public static void defer(Deque<Closure<?>> scope, Closure<?> action) {
+ if (scope == null) {
+ throw new IllegalStateException("defer must be used inside an async method or closure");
+ }
+ if (action == null) {
+ throw new IllegalArgumentException("defer action must not be null");
+ }
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ scope.push(new Closure<Object>(action.getOwner(), action.getThisObject()) {
+ @SuppressWarnings("unused")
+ public Object doCall() {
+ return AsyncContext.withSnapshot(contextSnapshot, () -> action.call());
+ }
+ });
+ }
+
+ /**
+ * Executes all deferred actions in the given scope in LIFO order.
+ * Each action is executed independently: if one throws an exception,
+ * subsequent actions still execute. If multiple actions throw, the
+ * first exception is rethrown and subsequent ones are added as
+ * {@linkplain Throwable#addSuppressed(Throwable) suppressed exceptions}.
+ * If a deferred action returns an {@link Awaitable},
+ * {@link CompletionStage}, {@link CompletableFuture}, or {@link Future},
+ * the async cleanup is awaited before moving on to the next deferred
+ * action.
+ * <p>
+ * Called by compiler-generated code in the {@code finally} block of
+ * methods that contain {@code defer} statements.
+ * <p>
+ * A {@code null} or empty scope is treated as a no-op for robustness.
+ *
+ * @param scope the defer scope to execute; may be {@code null}
+ */
+ public static void executeDeferScope(Deque<Closure<?>> scope) {
+ if (scope == null || scope.isEmpty()) return;
+ Throwable firstError = null;
+ while (!scope.isEmpty()) {
+ try {
+ Object result = scope.pop().call();
+ awaitDeferredResult(result);
+ } catch (Throwable t) {
+ if (firstError == null) {
+ firstError = t;
+ } else {
+ firstError.addSuppressed(t);
+ }
+ }
+ }
+ if (firstError != null) {
+ sneakyThrow(firstError);
+ }
+ }
+
+ // ---- internal -------------------------------------------------------
+
+ /**
+ * Deeply unwraps nested exception wrapper layers ({@link CompletionException},
+ * {@link ExecutionException}, {@link UndeclaredThrowableException},
+ * {@link InvocationTargetException}) to find the original root cause.
+ * <p>
+ * A depth limit of 64 is imposed to guard against circular cause chains,
+ * which could otherwise cause an infinite loop.
+ *
+ * @param t the wrapper exception
+ * @return the innermost non-wrapper exception, or the deepest reachable
+ * cause if the depth limit is reached
+ */
+ public static Throwable deepUnwrap(Throwable t) {
+ Throwable cause = t;
+ int depth = 0;
+ while (cause.getCause() != null && depth++ < 64
+ && (cause instanceof CompletionException
+ || cause instanceof ExecutionException
+ || cause instanceof UndeclaredThrowableException
+ || cause instanceof InvocationTargetException)) {
+ cause = cause.getCause();
+ }
+ return cause;
+ }
+
+ /**
+ * Unwraps and rethrows the original exception transparently, bypassing
+ * Java's checked-exception constraints via {@link #sneakyThrow}.
+ */
+ static RuntimeException rethrowUnwrapped(Throwable wrapper) {
+ Throwable cause = deepUnwrap(wrapper);
+ if (cause instanceof Error err) throw err;
+ throw sneakyThrow(cause);
+ }
+
+ /**
+ * Throws the given exception as an unchecked type using type-erasure.
+ * Enables transparent propagation of checked exceptions from async code.
+ */
+ @SuppressWarnings("unchecked")
+ static <T extends Throwable> RuntimeException sneakyThrow(Throwable t) throws T {
+ throw (T) t;
+ }
+
+ /**
+ * Wraps a throwable in a {@link CompletionException} if it is not already one.
+ * Used when rethrowing from within {@link CompletableFuture} lambdas.
+ */
+ static CompletionException wrapForFuture(Throwable t) {
+ if (t instanceof CompletionException ce) return ce;
+ return new CompletionException(t);
+ }
+
+ private static void awaitDeferredResult(Object result) {
+ if (result instanceof Awaitable
+ || result instanceof CompletableFuture
+ || result instanceof CompletionStage
+ || result instanceof Future) {
+ await(result);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/FlowPublisherAdapter.java b/src/main/java/org/apache/groovy/runtime/async/FlowPublisherAdapter.java
new file mode 100644
index 0000000000..e4102642dc
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/FlowPublisherAdapter.java
@@ -0,0 +1,392 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.runtime.async;
+
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.AwaitableAdapter;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Bridges JDK {@link Flow.Publisher} instances into Groovy's
+ * {@link Awaitable}/{@link AsyncStream} world.
+ *
+ * <h2>Registration</h2>
+ * This adapter is auto-discovered by the
+ * {@link groovy.concurrent.AwaitableAdapterRegistry} via the
+ * {@code META-INF/services/groovy.concurrent.AwaitableAdapter} file.
+ * It handles any object that implements {@link Flow.Publisher}.
+ *
+ * <h2>Adaptation modes</h2>
+ * <ul>
+ * <li><b>Single-value</b> ({@code await publisher}):
+ * subscribes, takes the first {@code onNext} item, cancels the
+ * upstream subscription, and completes the returned {@link Awaitable}.</li>
+ * <li><b>Multi-value</b> ({@code for await (item in publisher)}):
+ * wraps the publisher into an {@link AsyncStream} backed by a
+ * bounded {@link LinkedBlockingQueue}, providing natural
+ * back-pressure by requesting one item at a time.</li>
+ * </ul>
+ *
+ * <h2>Thread safety</h2>
+ * <p>All subscriber callbacks ({@code onSubscribe}, {@code onNext},
+ * {@code onError}, {@code onComplete}) are safe for invocation from
+ * any thread. Subscription references use {@link AtomicReference}
+ * for safe publication and race-free cancellation. The close path
+ * uses a CAS on an {@link AtomicBoolean} to guarantee exactly-once
+ * cleanup semantics.</p>
+ *
+ * <h2>Reactive Streams compliance</h2>
+ * <p>This adapter follows the Reactive Streams specification
+ * (JDK {@link Flow} variant) rules:</p>
+ * <ul>
+ * <li>§2.5 — duplicate {@code onSubscribe} cancels the second subscription</li>
+ * <li>§2.13 — {@code null} items in {@code onNext} are rejected immediately</li>
+ * <li>All signals ({@code onNext}, {@code onError}, {@code onComplete}) use
+ * blocking {@code put()} with a non-blocking {@code offer()} fallback
+ * when the publisher thread is interrupted — this prevents both silent
+ * item loss and consumer deadlock even under unexpected interrupts</li>
+ * <li>Terminal callbacks atomically close the stream via a single
+ * {@link AtomicBoolean} flag and clear/cancel the stored subscription.
+ * This makes post-terminal {@code onNext} calls from non-compliant
+ * publishers harmless and releases resources promptly.</li>
+ * <li>Back-pressure is enforced by requesting exactly one item after
+ * each consumed element; demand is signalled <em>before</em>
+ * {@code moveNext()} returns, preventing livelock when producer and
+ * consumer share the same thread pool</li>
+ * </ul>
+ *
+ * @see groovy.concurrent.AwaitableAdapterRegistry
+ * @see AsyncStream
+ * @since 6.0.0
+ */
+public class FlowPublisherAdapter implements AwaitableAdapter {
+
+ /**
+ * Returns {@code true} if the given type is assignable to
+ * {@link Flow.Publisher}, enabling single-value {@code await}.
+ *
+ * @param type the source type to check
+ * @return {@code true} if this adapter can handle the type
+ */
+ @Override
+ public boolean supportsAwaitable(Class<?> type) {
+ return Flow.Publisher.class.isAssignableFrom(type);
+ }
+
+ /**
+ * Returns {@code true} if the given type is assignable to
+ * {@link Flow.Publisher}, enabling multi-value {@code for await}.
+ *
+ * @param type the source type to check
+ * @return {@code true} if this adapter can produce an async stream
+ */
+ @Override
+ public boolean supportsAsyncStream(Class<?> type) {
+ return Flow.Publisher.class.isAssignableFrom(type);
+ }
+
+ /**
+ * Converts a {@link Flow.Publisher} to a single-value {@link Awaitable}
+ * by subscribing and taking the first emitted item.
+ *
+ * @param source the publisher instance
+ * @param <T> the element type
+ * @return an awaitable that resolves to the first emitted value
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> Awaitable<T> toAwaitable(Object source) {
+ return publisherToAwaitable((Flow.Publisher<T>) source);
+ }
+
+ /**
+ * Converts a {@link Flow.Publisher} to a multi-value {@link AsyncStream}
+ * for use with {@code for await} loops.
+ *
+ * @param source the publisher instance
+ * @param <T> the element type
+ * @return an async stream that yields publisher items
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> AsyncStream<T> toAsyncStream(Object source) {
+ return publisherToAsyncStream((Flow.Publisher<T>) source);
+ }
+
+ // ---- Single-value adaptation (await publisher) ----
+
+ /**
+ * Subscribes to the publisher, takes the <em>first</em> emitted item,
+ * cancels the upstream subscription, and returns a completed
+ * {@link Awaitable}.
+ *
+ * <p>If the publisher completes without emitting any item,
+ * the returned awaitable resolves to {@code null}.</p>
+ *
+ * @param publisher the upstream publisher
+ * @param <T> the element type
+ * @return an awaitable that completes with the first emitted value
+ */
+ private <T> Awaitable<T> publisherToAwaitable(Flow.Publisher<T> publisher) {
+ CompletableFuture<T> cf = new CompletableFuture<>();
+ // AtomicReference ensures safe publication of the subscription
+ // across the onSubscribe thread and callback threads (§1.3).
+ AtomicReference<Flow.Subscription> subRef = new AtomicReference<>();
+ // Guard against non-compliant publishers that send multiple signals
+ AtomicBoolean done = new AtomicBoolean(false);
+
+ publisher.subscribe(new Flow.Subscriber<T>() {
+ @Override
+ public void onSubscribe(Flow.Subscription s) {
+ // §2.5: reject duplicate subscriptions
+ if (!subRef.compareAndSet(null, s)) {
+ s.cancel();
+ return;
+ }
+ s.request(1);
+ }
+
+ @Override
+ public void onNext(T item) {
+ // §2.13: null items are spec violations
+ if (item == null) {
+ onError(new NullPointerException(
+ "Flow.Publisher onNext received null (Reactive Streams §2.13)"));
+ return;
+ }
+ if (done.compareAndSet(false, true)) {
+ cf.complete(item);
+ Flow.Subscription sub = subRef.getAndSet(null);
+ if (sub != null) sub.cancel();
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ if (done.compareAndSet(false, true)) {
+ // §2.13 requires non-null, but defend against non-compliant publishers
+ cf.completeExceptionally(t != null ? t
+ : new NullPointerException("onError called with null (Reactive Streams §2.13 violation)"));
+ // Cancel subscription to release resources (idempotent per §3.7)
+ Flow.Subscription sub = subRef.getAndSet(null);
+ if (sub != null) sub.cancel();
+ }
+ }
+
+ @Override
+ public void onComplete() {
+ // Publisher completed before emitting — resolve to null
+ if (done.compareAndSet(false, true)) {
+ cf.complete(null);
+ // Mirror onNext/onError cleanup for prompt resource release.
+ Flow.Subscription sub = subRef.getAndSet(null);
+ if (sub != null) sub.cancel();
+ }
+ }
+ });
+
+ return new GroovyPromise<>(cf);
+ }
+
+ // ---- Multi-value adaptation (for await publisher) ----
+
+ /**
+ * Wraps a {@link Flow.Publisher} into an {@link AsyncStream},
+ * providing a pull-based iteration interface over a push-based source.
+ *
+ * <p>Back-pressure is enforced by requesting exactly one item after
+ * each consumed element. Demand is signalled <em>before</em> the
+ * consumer's {@code moveNext()} awaitable completes, so the publisher
+ * can begin producing the next value while the consumer processes the
+ * current one — this prevents livelock when producer and consumer
+ * share the same thread pool.</p>
+ *
+ * <p><b>Resource management:</b> When the consumer calls
+ * {@link AsyncStream#close()} (e.g. via {@code break} in a
+ * {@code for await} loop), the upstream subscription is cancelled
+ * and a completion sentinel is injected to unblock any pending
+ * {@code moveNext()} call.</p>
+ *
+ * @param publisher the upstream publisher
+ * @param <T> the element type
+ * @return an async stream that yields publisher items
+ */
+ private <T> AsyncStream<T> publisherToAsyncStream(Flow.Publisher<T> publisher) {
+ FlowAsyncStream<T> stream = new FlowAsyncStream<>();
+ publisher.subscribe(stream.newSubscriber());
+ return stream;
+ }
+
+ /**
+ * Named implementation of {@link AsyncStream} that extends
+ * {@link AbstractAsyncStream} to bridge a push-based
+ * {@link Flow.Publisher} into a pull-based async stream.
+ *
+ * <p>Inherits the common {@link AbstractAsyncStream#moveNext() moveNext()}/
+ * {@link AbstractAsyncStream#getCurrent() getCurrent()}/
+ * {@link AbstractAsyncStream#close() close()} template and overrides:</p>
+ * <ul>
+ * <li>{@link #beforeTake()} — drains remaining signals before returning
+ * false (checks {@code closed && queue.isEmpty()})</li>
+ * <li>{@link #afterValueConsumed()} — signals back-pressure demand via
+ * {@code Subscription.request(1)}</li>
+ * <li>{@link #onClose()} — cancels the upstream subscription, drains
+ * the queue, and injects a {@link #COMPLETE} sentinel to unblock
+ * any pending {@code moveNext()}</li>
+ * </ul>
+ *
+ * <p>The internal bounded queue (capacity {@value QUEUE_CAPACITY})
+ * absorbs minor timing jitter. Signals use blocking {@code put()}
+ * for normal delivery with a non-blocking {@code offer()} fallback
+ * when the publisher thread is interrupted — ensuring no items or
+ * terminal events are silently dropped.</p>
+ *
+ * @param <T> the element type
+ */
+ private static final class FlowAsyncStream<T> extends AbstractAsyncStream<T> {
+
+ /**
+ * Queue capacity for the push→pull bridge. With one-at-a-time
+ * demand ({@code request(1)} per consumed element), a well-behaved
+ * publisher will never enqueue more than one value at a time.
+ * A capacity of 2 accommodates the value + a racing terminal
+ * signal without blocking, while keeping memory minimal.
+ */
+ private static final int QUEUE_CAPACITY = 2;
+
+ private final AtomicReference<Flow.Subscription> subRef = new AtomicReference<>();
+
+ FlowAsyncStream() {
+ super(new LinkedBlockingQueue<>(QUEUE_CAPACITY));
+ }
+
+ /**
+ * Creates a new {@link Flow.Subscriber} wired to this stream's
+ * internal queue and lifecycle state.
+ */
+ Flow.Subscriber<T> newSubscriber() {
+ return new Flow.Subscriber<>() {
+ @Override
+ public void onSubscribe(Flow.Subscription s) {
+ // §2.5: reject duplicate subscriptions
+ if (!subRef.compareAndSet(null, s)) {
+ s.cancel();
+ return;
+ }
+ // Double-check: if close() raced before subscription was set,
+ // cancel immediately to avoid a dangling upstream.
+ if (closed.get()) {
+ cancelSubscription();
+ return;
+ }
+ s.request(1);
+ }
+
+ @Override
+ public void onNext(T item) {
+ // §2.13: null items are spec violations
+ if (item == null) {
+ onError(new NullPointerException(
+ "Flow.Publisher onNext received null (Reactive Streams §2.13)"));
+ return;
+ }
+ if (closed.get()) return;
+ try {
+ queue.put(new ValueSignal(item));
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ if (!queue.offer(new ValueSignal(item))) {
+ cancelSubscription();
+ closed.set(true);
+ // Clear the queue to guarantee room for the terminal signal;
+ // without this, a full queue could leave the consumer blocked
+ // in take() with no signal to unblock it.
+ queue.clear();
+ queue.offer(COMPLETE);
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ // §2.13 requires non-null, but defend against non-compliant publishers
+ Throwable cause = t != null ? t
+ : new NullPointerException("onError called with null (Reactive Streams §2.13 violation)");
+ putTerminalSignal(new ErrorSignal(cause));
+ }
+
+ @Override
+ public void onComplete() {
+ putTerminalSignal(COMPLETE);
+ }
+
+ /**
+ * Shared logic for {@code onError()} and {@code onComplete()}:
+ * atomically marks the upstream as closed, cancels the subscription,
+ * and delivers the terminal signal to the consumer queue.
+ */
+ private void putTerminalSignal(Object signal) {
+ if (!closed.compareAndSet(false, true)) return;
+ cancelSubscription();
+ try {
+ queue.put(signal);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ queue.offer(signal);
+ }
+ }
+ };
+ }
+
+ @Override
+ protected Awaitable<Boolean> beforeTake() {
+ return (closed.get() && queue.isEmpty()) ? MOVE_NEXT_FALSE : null;
+ }
+
+ @Override
+ protected void afterValueConsumed() {
+ // Signal demand BEFORE returning so the publisher can begin
+ // producing the next value while the consumer processes this
+ // one — prevents livelock when both share a thread pool.
+ Flow.Subscription sub = subRef.get();
+ if (sub != null) sub.request(1);
+ }
+
+ @Override
+ protected void onClose() {
+ cancelSubscription();
+ // Drain the queue and inject a sentinel to unblock a
+ // concurrent moveNext() that may be blocked in take().
+ queue.clear();
+ queue.offer(COMPLETE);
+ }
+
+ private void cancelSubscription() {
+ Flow.Subscription sub = subRef.getAndSet(null);
+ if (sub != null) sub.cancel();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java b/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
new file mode 100644
index 0000000000..b0e871f17a
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.runtime.async;
+
+import groovy.concurrent.AsyncContext;
+import groovy.concurrent.Awaitable;
+
+import java.util.Objects;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+
+/**
+ * Default {@link Awaitable} implementation backed by a {@link CompletableFuture}.
+ * <p>
+ * This is the concrete type returned by {@code async} methods. It delegates
+ * all operations to an underlying {@code CompletableFuture} while keeping the
+ * public API limited to the {@code Awaitable} contract, thereby decoupling
+ * user code from JDK-specific async APIs.
+ * <p>
+ * This class is an internal implementation detail and should not be referenced
+ * directly by user code. Use the {@link Awaitable} interface instead.
+ *
+ * @param <T> the result type
+ * @see Awaitable
+ * @since 6.0.0
+ */
+public class GroovyPromise<T> implements Awaitable<T> {
+
+ private final CompletableFuture<T> future;
+
+ /**
+ * Creates a new {@code GroovyPromise} wrapping the given {@link CompletableFuture}.
+ *
+ * @param future the backing future; must not be {@code null}
+ * @throws NullPointerException if {@code future} is {@code null}
+ */
+ public GroovyPromise(CompletableFuture<T> future) {
+ this.future = Objects.requireNonNull(future, "future must not be null");
+ }
+
+ /**
+ * Creates a {@code GroovyPromise} wrapping the given {@link CompletableFuture}.
+ */
+ public static <T> GroovyPromise<T> of(CompletableFuture<T> future) {
+ return new GroovyPromise<>(future);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Includes a synchronous completion fast-path: if the underlying
+ * {@link CompletableFuture} is already done, the result is extracted
+ * via {@link CompletableFuture#join()} which avoids the full
+ * park/unpark machinery of {@link CompletableFuture#get()}.
+ * This optimisation provides a synchronous completion fast-path
+ * and eliminates unnecessary thread state transitions
+ * on the hot path where async operations complete before being awaited.
+ * <p>
+ * If the future was cancelled, the original {@link CancellationException} is
+ * unwrapped from the JDK 23+ wrapper for cross-version consistency.
+ */
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ // Fast path: already completed — skip wait queue and thread parking
+ if (future.isDone()) {
+ return getCompleted();
+ }
+ try {
+ return future.get();
+ } catch (CancellationException e) {
+ throw unwrapCancellation(e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Includes a synchronous completion fast-path for already-done futures,
+ * consistent with the zero-argument {@link #get()} overload.
+ * Unwraps JDK 23+ {@link CancellationException} wrappers for consistency.
+ */
+ @Override
+ public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ // Fast path: already completed — skip wait queue and thread parking
+ if (future.isDone()) {
+ return getCompleted();
+ }
+ try {
+ return future.get(timeout, unit);
+ } catch (CancellationException e) {
+ throw unwrapCancellation(e);
+ }
+ }
+
+ /**
+ * Extracts the result from an already-completed future using
+ * {@link CompletableFuture#join()}, which is cheaper than
+ * {@link CompletableFuture#get()} for completed futures because it
+ * bypasses the interruptible wait path.
+ * <p>
+ * Translates {@link CompletionException} to {@link ExecutionException}
+ * to preserve the {@code get()} contract.
+ */
+ private T getCompleted() throws ExecutionException {
+ try {
+ return future.join();
+ } catch (CompletionException e) {
+ throw new ExecutionException(AsyncSupport.deepUnwrap(e));
+ } catch (CancellationException e) {
+ throw unwrapCancellation(e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isDone() {
+ return future.isDone();
+ }
+
+ /**
+ * Attempts to cancel this computation. Delegates to
+ * {@link CompletableFuture#cancel(boolean) CompletableFuture.cancel(true)}.
+ * <p>
+ * <b>Note:</b> {@code CompletableFuture} cancellation sets the future's state
+ * to cancelled but does <em>not</em> reliably interrupt the underlying thread.
+ * Async work already in progress may continue running in the background.
+ * For cooperative cancellation, check {@link Thread#isInterrupted()} in
+ * long-running async bodies.
+ *
+ * @return {@code true} if the future was successfully cancelled
+ */
+ @Override
+ public boolean cancel() {
+ return future.cancel(true);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isCancelled() {
+ return future.isCancelled();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isCompletedExceptionally() {
+ return future.isCompletedExceptionally();
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns a new {@code GroovyPromise} whose result is obtained by applying
+ * the given function to this promise's result. The current
+ * {@link AsyncContext} snapshot is captured when the continuation is
+ * registered and restored when it executes.
+ */
+ @Override
+ public <U> Awaitable<U> then(Function<? super T, ? extends U> fn) {
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ return new GroovyPromise<>(future.thenApply(value ->
+ AsyncContext.withSnapshot(contextSnapshot, () -> fn.apply(value))));
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns a new {@code GroovyPromise} that is the result of composing this
+ * promise with the async function, enabling flat-mapping of awaitables.
+ * The current {@link AsyncContext} snapshot is captured when the
+ * continuation is registered and restored when it executes.
+ */
+ @Override
+ public <U> Awaitable<U> thenCompose(Function<? super T, ? extends Awaitable<U>> fn) {
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ return new GroovyPromise<>(future.thenCompose(value ->
+ AsyncContext.withSnapshot(contextSnapshot,
+ () -> fn.apply(value).toCompletableFuture())));
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns a new {@code GroovyPromise} that handles exceptions thrown by this promise.
+ * The throwable passed to the handler is deeply unwrapped to strip JDK
+ * wrapper layers ({@code CompletionException}, {@code ExecutionException}).
+ * The handler runs with the {@link AsyncContext} snapshot that was active
+ * when the recovery continuation was registered.
+ */
+ @Override
+ public Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn) {
+ AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+ return new GroovyPromise<>(future.exceptionally(t ->
+ AsyncContext.withSnapshot(contextSnapshot, () -> {
+ Throwable cause = AsyncSupport.deepUnwrap(t);
+ return fn.apply(cause);
+ })));
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns the underlying {@link CompletableFuture} for interop with JDK APIs.
+ */
+ @Override
+ public CompletableFuture<T> toCompletableFuture() {
+ return future;
+ }
+
+ /**
+ * JDK 23+ wraps a stored {@link CancellationException} in a new instance
+ * with the generic message {@code "get"} when {@link CompletableFuture#get()}
+ * is called. Unwrap it here so Groovy users consistently observe the
+ * original cancellation message and cause chain across all supported JDKs.
+ */
+ private static CancellationException unwrapCancellation(CancellationException exception) {
+ Throwable cause = exception.getCause();
+ return cause instanceof CancellationException ce ? ce : exception;
+ }
+
+ /**
+ * Returns a human-readable representation showing the promise state:
+ * {@code GroovyPromise{pending}}, {@code GroovyPromise{completed}}, or
+ * {@code GroovyPromise{failed}}.
+ */
+ @Override
+ public String toString() {
+ if (future.isDone()) {
+ if (future.isCompletedExceptionally()) {
+ return "GroovyPromise{failed}";
+ }
+ return "GroovyPromise{completed}";
+ }
+ return "GroovyPromise{pending}";
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
index e76e1b4849..a141ffae12 100644
--- a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Objects;
import static org.apache.groovy.parser.antlr4.GroovyParser.ABSTRACT;
+import static org.apache.groovy.parser.antlr4.GroovyParser.ASYNC;
import static org.apache.groovy.parser.antlr4.GroovyParser.DEF;
import static org.apache.groovy.parser.antlr4.GroovyParser.DEFAULT;
import static org.apache.groovy.parser.antlr4.GroovyParser.FINAL;
@@ -55,6 +56,7 @@ public class ModifierNode extends ASTNode {
public static final int ANNOTATION_TYPE = -999;
public static final Map<Integer, Integer> MODIFIER_OPCODE_MAP = Maps.of(
ANNOTATION_TYPE, 0,
+ ASYNC, 0,
DEF, 0,
VAR, 0,
diff --git a/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java
new file mode 100644
index 0000000000..50aab9776a
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.transform;
+
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.Awaitable;
+import groovy.transform.Async;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.GenericsType;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.VariableScope;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+import static org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafeWithGenerics;
+
+/**
+ * Handles code generation for the {@link Async @Async} annotation.
+ * <p>
+ * Transforms the annotated method so that:
+ * <ol>
+ * <li>{@code await(expr)} calls within the method body are redirected to
+ * the runtime's {@code await()} via {@link AsyncTransformHelper}</li>
+ * <li>The method body is executed asynchronously — or as an async generator
+ * if it contains {@code yield return} — via factory methods on
+ * {@link AsyncTransformHelper}. The runtime preserves
+ * {@link groovy.concurrent.AsyncContext} snapshots across those async
+ * boundaries.</li>
+ * <li>Methods containing {@code defer} statements are wrapped in a
+ * try-finally block that executes deferred actions in LIFO order, with
+ * each deferred closure bound to the async context active at registration
+ * time</li>
+ * <li>The return type becomes {@code Awaitable<T>}
+ * (or {@code AsyncStream<T>} for generators)</li>
+ * </ol>
+ * <p>
+ * All AST node construction for async runtime calls is delegated to
+ * {@link AsyncTransformHelper}, which encapsulates the internal method
+ * names and class references. This class focuses on method-level
+ * validation and structural transformation.
+ * <p>
+ * This transformation runs during the {@link CompilePhase#CANONICALIZATION}
+ * phase — before type resolution, which allows the modified return types to
+ * participate in normal type checking.
+ *
+ * @see Async
+ * @see AsyncTransformHelper
+ * @since 6.0.0
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class AsyncASTTransformation extends AbstractASTTransformation {
+
+ private static final Class<?> MY_CLASS = Async.class;
+ private static final ClassNode MY_TYPE = ClassHelper.make(MY_CLASS);
+ private static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+ private static final ClassNode AWAITABLE_TYPE = ClassHelper.make(Awaitable.class);
+ private static final ClassNode ASYNC_STREAM_TYPE = ClassHelper.make(AsyncStream.class);
+
+ @Override
+ public void visit(ASTNode[] nodes, SourceUnit source) {
+ init(nodes, source);
+ AnnotatedNode parent = (AnnotatedNode) nodes[1];
+ AnnotationNode anno = (AnnotationNode) nodes[0];
+ if (!MY_TYPE.equals(anno.getClassNode())) return;
+
+ if (!(parent instanceof MethodNode mNode)) return;
+
+ // Validate
+ if (mNode.isAbstract()) {
+ addError(MY_TYPE_NAME + " cannot be applied to abstract method '" + mNode.getName() + "'", mNode);
+ return;
+ }
+ if ("<init>".equals(mNode.getName()) || "<clinit>".equals(mNode.getName())) {
+ addError(MY_TYPE_NAME + " cannot be applied to constructors", mNode);
+ return;
+ }
+ ClassNode originalReturnType = mNode.getReturnType();
+ if (AWAITABLE_TYPE.getName().equals(originalReturnType.getName())
+ || ASYNC_STREAM_TYPE.getName().equals(originalReturnType.getName())
+ || "java.util.concurrent.CompletableFuture".equals(originalReturnType.getName())
+ || "java.util.concurrent.CompletionStage".equals(originalReturnType.getName())
+ || "java.util.concurrent.Future".equals(originalReturnType.getName())) {
+ addError(MY_TYPE_NAME + " cannot be applied to a method that already returns an async type", mNode);
+ return;
+ }
+ Statement originalBody = mNode.getCode();
+ if (originalBody == null) return;
+
+ ClassNode cNode = mNode.getDeclaringClass();
+
+ // Resolve executor expression
+ String executorFieldName = getMemberStringValue(anno, "executor");
+ Expression executorExpr;
+ if (executorFieldName != null && !executorFieldName.isEmpty()) {
+ FieldNode field = cNode.getDeclaredField(executorFieldName);
+ if (field == null) {
+ addError(MY_TYPE_NAME + ": executor field '" + executorFieldName
+ + "' not found in class " + cNode.getName(), mNode);
+ return;
+ }
+ if (mNode.isStatic() && !field.isStatic()) {
+ addError(MY_TYPE_NAME + ": executor field '" + executorFieldName
+ + "' must be static for static method '" + mNode.getName() + "'", mNode);
+ return;
+ }
+ executorExpr = varX(executorFieldName);
+ } else {
+ executorExpr = AsyncTransformHelper.buildGetExecutorCall();
+ }
+
+ // Step 1: Transform await() method calls to AsyncSupport.await()
+ new AwaitCallTransformer(source).visitMethod(mNode);
+
+ // Step 2: Check if body contains yield return (AsyncSupport.yieldReturn) calls
+ Statement transformedBody = mNode.getCode();
+ boolean hasYieldReturn = AsyncTransformHelper.containsYieldReturn(transformedBody);
+ boolean hasDefer = AsyncTransformHelper.containsDefer(transformedBody);
+
+ // Step 2a: If defer is present, wrap body in try-finally with defer scope
+ if (hasDefer) {
+ transformedBody = AsyncTransformHelper.wrapWithDeferScope(transformedBody);
+ }
+
+ Parameter[] closureParams;
+ if (hasYieldReturn) {
+ closureParams = new Parameter[]{ AsyncTransformHelper.createGenParam() };
+ } else {
+ closureParams = Parameter.EMPTY_ARRAY;
+ }
+ ClosureExpression closure = new ClosureExpression(closureParams, transformedBody);
+ VariableScope closureScope = new VariableScope(mNode.getVariableScope());
+ for (Parameter p : mNode.getParameters()) {
+ p.setClosureSharedVariable(true);
+ closureScope.putReferencedLocalVariable(p);
+ }
+ closure.setVariableScope(closureScope);
+
+ if (hasYieldReturn) {
+ // Transform yieldReturn(expr) → yieldReturn($__asyncGen__, expr) in closure body
+ AsyncTransformHelper.injectGenParamIntoYieldReturnCalls(transformedBody, closureParams[0]);
+
+ // Async generator: wrap body in AsyncSupport.generateAsyncStream { ... }
+ Expression genCall = AsyncTransformHelper.buildGenerateAsyncStreamCall(closure);
+ mNode.setCode(block(returnS(genCall)));
+
+ // Return type: AsyncStream<T>
+ ClassNode innerType;
+ if (ClassHelper.isPrimitiveVoid(originalReturnType) || ClassHelper.OBJECT_TYPE.equals(originalReturnType)) {
+ innerType = ClassHelper.OBJECT_TYPE;
+ } else if (ClassHelper.isPrimitiveType(originalReturnType)) {
+ innerType = ClassHelper.getWrapper(originalReturnType);
+ } else {
+ innerType = originalReturnType;
+ }
+ ClassNode streamReturnType = makeClassSafeWithGenerics(ASYNC_STREAM_TYPE, new GenericsType(innerType));
+ mNode.setReturnType(streamReturnType);
+ } else {
+ // Regular async: delegate to AsyncSupport.executeAsync/executeAsyncVoid
+ boolean isVoid = ClassHelper.isPrimitiveVoid(originalReturnType);
+ Expression asyncCall;
+ if (isVoid) {
+ asyncCall = AsyncTransformHelper.buildExecuteAsyncVoidCall(closure, executorExpr);
+ } else {
+ asyncCall = AsyncTransformHelper.buildExecuteAsyncCall(closure, executorExpr);
+ }
+ mNode.setCode(block(returnS(asyncCall)));
+
+ // Return type: Awaitable<T>
+ ClassNode innerType;
+ if (isVoid) {
+ innerType = ClassHelper.void_WRAPPER_TYPE;
+ } else if (ClassHelper.isPrimitiveType(originalReturnType)) {
+ innerType = ClassHelper.getWrapper(originalReturnType);
+ } else {
+ innerType = originalReturnType;
+ }
+ ClassNode awaitableReturnType = makeClassSafeWithGenerics(AWAITABLE_TYPE, new GenericsType(innerType));
+ mNode.setReturnType(awaitableReturnType);
+ }
+ }
+
+ /**
+ * Walks the method body and transforms {@code await(expr)} method calls
+ * into {@code AsyncSupport.await(expr)} static calls. With the native
+ * grammar ({@code await expr}), most calls are already compiled to
+ * {@code AsyncSupport.await()} by the parser; this transformer handles
+ * the {@code await(expr)} call-style used in {@link Async @Async}-annotated
+ * methods where {@code await} is parsed as a regular method call.
+ */
+ private static class AwaitCallTransformer extends ClassCodeExpressionTransformer {
+
+ private final SourceUnit sourceUnit;
+
+ AwaitCallTransformer(SourceUnit sourceUnit) {
+ this.sourceUnit = sourceUnit;
+ }
+
+ @Override
+ protected SourceUnit getSourceUnit() {
+ return sourceUnit;
+ }
+
+ @Override
+ public Expression transform(Expression expr) {
+ if (expr == null) return null;
+
+ if (expr instanceof ClosureExpression ce) {
+ ce.getCode().visit(this);
+ return ce;
+ }
+
+ if (expr instanceof MethodCallExpression mce
+ && AsyncTransformHelper.isAwaitMethodName(mce.getMethodAsString())
+ && mce.isImplicitThis()) {
+ Expression transformedArgs = mce.getArguments().transformExpression(this);
+ Expression replacement = AsyncTransformHelper.buildAwaitCall(transformedArgs);
+ replacement.setSourcePosition(expr);
+ return replacement;
+ }
+
+ return expr.transformExpression(this);
+ }
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java b/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
new file mode 100644
index 0000000000..8f0d137c16
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
@@ -0,0 +1,411 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.transform;
+
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.CodeVisitorSupport;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.DeclarationExpression;
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.TryCatchStatement;
+import org.codehaus.groovy.syntax.Token;
+import org.codehaus.groovy.syntax.Types;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+
+/**
+ * Shared AST utilities for the {@code async}/{@code await} language feature.
+ * <p>
+ * Both the ANTLR4 parser ({@code AstBuilder}) and the annotation-driven
+ * {@link AsyncASTTransformation} need to detect, rewrite, and generate AST
+ * nodes for {@code await}, {@code yield return}, {@code defer}, and other
+ * async constructs. This helper class centralises those operations behind
+ * a clean API so that:
+ * <ul>
+ * <li>Internal constants (method names, variable names, type references)
+ * remain encapsulated — callers never reference them directly.</li>
+ * <li>The two code paths (parser and annotation transform) stay in sync
+ * with no duplicated logic.</li>
+ * </ul>
+ * <p>
+ * <b>AST factory methods</b> (e.g.
+ * {@link #buildAwaitCall(Expression)},
+ * {@link #buildYieldReturnCall(Expression)},
+ * {@link #buildDeferCall(Expression)})
+ * construct fully-formed AST nodes that reference the internal
+ * {@code AsyncSupport} runtime class, shielding callers from the underlying
+ * method names and class references.
+ * <p>
+ * <b>Statement scanners</b> ({@link #containsYieldReturn(Statement)},
+ * {@link #containsDefer(Statement)}) detect the presence of generator and
+ * defer constructs without descending into nested
+ * {@link ClosureExpression}s, since each nested {@code async { ... }} closure
+ * manages its own scope independently.
+ * <p>
+ * <b>Rewriting utilities</b>
+ * ({@link #injectGenParamIntoYieldReturnCalls(Statement, Parameter)},
+ * {@link #wrapWithDeferScope(Statement)}) perform targeted AST mutations
+ * that transform high-level syntax into the lower-level runtime calls. The
+ * generated runtime calls now also inherit async-local context automatically,
+ * so compiler-generated code retains request/trace metadata across thread hops
+ * without special-case AST handling.
+ *
+ * @see AsyncASTTransformation
+ * @since 6.0.0
+ */
+public final class AsyncTransformHelper {
+
+ // ---- internal constants (all private) --------------------------------
+
+ private static final String ASYNC_SUPPORT_CLASS = "org.apache.groovy.runtime.async.AsyncSupport";
+ private static final ClassNode ASYNC_SUPPORT_TYPE = ClassHelper.make(ASYNC_SUPPORT_CLASS);
+ private static final String ASYNC_GEN_PARAM_NAME = "$__asyncGen__";
+ private static final String DEFER_SCOPE_VAR = "$__deferScope__";
+
+ private static final String AWAIT_METHOD = "await";
+ private static final String YIELD_RETURN_METHOD = "yieldReturn";
+ private static final String WRAP_ASYNC_METHOD = "wrapAsync";
+ private static final String WRAP_ASYNC_GENERATOR_METHOD = "wrapAsyncGenerator";
+ private static final String GENERATE_ASYNC_STREAM_METHOD = "generateAsyncStream";
+ private static final String EXECUTE_ASYNC_METHOD = "executeAsync";
+ private static final String EXECUTE_ASYNC_VOID_METHOD = "executeAsyncVoid";
+ private static final String TO_ASYNC_STREAM_METHOD = "toAsyncStream";
+ private static final String CLOSE_STREAM_METHOD = "closeStream";
+ private static final String CREATE_DEFER_SCOPE_METHOD = "createDeferScope";
+ private static final String DEFER_METHOD = "defer";
+ private static final String EXECUTE_DEFER_SCOPE_METHOD = "executeDeferScope";
+
+ private AsyncTransformHelper() { }
+
+ // ---- AST factory methods --------------------------------------------
+
+ /**
+ * Ensures the expression is an {@link ArgumentListExpression}.
+ * If it already is one, returns it as-is; otherwise wraps it.
+ */
+ private static Expression ensureArgs(Expression expr) {
+ return expr instanceof ArgumentListExpression ? expr : new ArgumentListExpression(expr);
+ }
+
+ /**
+ * Builds {@code AsyncSupport.await(arg)}.
+ * Accepts either a single expression or a pre-assembled
+ * {@link ArgumentListExpression}.
+ *
+ * @param arg the expression to await
+ * @return an AST node representing the static call
+ */
+ public static Expression buildAwaitCall(Expression arg) {
+ return callX(ASYNC_SUPPORT_TYPE, AWAIT_METHOD, ensureArgs(arg));
+ }
+
+ /**
+ * Returns {@code true} if the given method name is the {@code await}
+ * keyword — used by the {@code @Async} annotation's
+ * {@link AsyncASTTransformation.AwaitCallTransformer AwaitCallTransformer}
+ * to identify {@code await(expr)} method calls that need rewriting.
+ *
+ * @param name the method name to check
+ * @return {@code true} if the name is {@code "await"}
+ */
+ public static boolean isAwaitMethodName(String name) {
+ return AWAIT_METHOD.equals(name);
+ }
+
+ /**
+ * Builds {@code AsyncSupport.yieldReturn(expr)}.
+ *
+ * @param arg the expression to yield
+ * @return an AST node representing the static call
+ */
+ public static Expression buildYieldReturnCall(Expression arg) {
+ return callX(ASYNC_SUPPORT_TYPE, YIELD_RETURN_METHOD, ensureArgs(arg));
+ }
+
+ /**
+ * Builds {@code AsyncSupport.defer($__deferScope__, action)}.
+ * The synthetic defer-scope variable is injected by
+ * {@link #wrapWithDeferScope(Statement)} during AST transformation.
+ * <p>
+ * The parser validates that {@code defer} only appears inside an async
+ * context (async method or async closure); using it elsewhere is a
+ * compile-time error.
+ *
+ * @param action the deferred action expression (typically a closure)
+ * @return an AST node representing the static call
+ */
+ public static Expression buildDeferCall(Expression action) {
+ return callX(ASYNC_SUPPORT_TYPE, DEFER_METHOD,
+ args(varX(DEFER_SCOPE_VAR), action));
+ }
+
+ /**
+ * Builds {@code AsyncSupport.toAsyncStream(source)}.
+ *
+ * @param source the expression to convert to an async stream
+ * @return an AST node representing the static call
+ */
+ public static Expression buildToAsyncStreamCall(Expression source) {
+ return callX(ASYNC_SUPPORT_TYPE, TO_ASYNC_STREAM_METHOD, ensureArgs(source));
+ }
+
+ /**
+ * Builds {@code AsyncSupport.closeStream(stream)}.
+ *
+ * @param stream the stream expression to close
+ * @return an AST node representing the static call
+ */
+ public static Expression buildCloseStreamCall(Expression stream) {
+ return callX(ASYNC_SUPPORT_TYPE, CLOSE_STREAM_METHOD, ensureArgs(stream));
+ }
+
+ /**
+ * Builds {@code AsyncSupport.generateAsyncStream(closure)}.
+ *
+ * @param closure the generator closure expression
+ * @return an AST node representing the static call
+ */
+ public static Expression buildGenerateAsyncStreamCall(Expression closure) {
+ return callX(ASYNC_SUPPORT_TYPE, GENERATE_ASYNC_STREAM_METHOD, ensureArgs(closure));
+ }
+
+ /**
+ * Builds {@code AsyncSupport.wrapAsync(closure)}.
+ *
+ * @param closure the async closure expression
+ * @return an AST node representing the static call
+ */
+ public static Expression buildWrapAsyncCall(Expression closure) {
+ return callX(ASYNC_SUPPORT_TYPE, WRAP_ASYNC_METHOD, ensureArgs(closure));
+ }
+
+ /**
+ * Builds {@code AsyncSupport.wrapAsyncGenerator(closure)}.
+ *
+ * @param closure the generator closure expression
+ * @return an AST node representing the static call
+ */
+ public static Expression buildWrapAsyncGeneratorCall(Expression closure) {
+ return callX(ASYNC_SUPPORT_TYPE, WRAP_ASYNC_GENERATOR_METHOD, ensureArgs(closure));
+ }
+
+ /**
+ * Builds {@code AsyncSupport.executeAsync(closure, executor)}.
+ *
+ * @param closure the async closure expression
+ * @param executor the executor expression
+ * @return an AST node representing the static call
+ */
+ public static Expression buildExecuteAsyncCall(Expression closure, Expression executor) {
+ return callX(ASYNC_SUPPORT_TYPE, EXECUTE_ASYNC_METHOD, args(closure, executor));
+ }
+
+ /**
+ * Builds {@code AsyncSupport.executeAsyncVoid(closure, executor)}.
+ *
+ * @param closure the async void closure expression
+ * @param executor the executor expression
+ * @return an AST node representing the static call
+ */
+ public static Expression buildExecuteAsyncVoidCall(Expression closure, Expression executor) {
+ return callX(ASYNC_SUPPORT_TYPE, EXECUTE_ASYNC_VOID_METHOD, args(closure, executor));
+ }
+
+ /**
+ * Builds {@code AsyncSupport.getExecutor()}.
+ *
+ * @return an AST node representing the static call
+ */
+ public static Expression buildGetExecutorCall() {
+ return callX(ASYNC_SUPPORT_TYPE, "getExecutor", ArgumentListExpression.EMPTY_ARGUMENTS);
+ }
+
+ // ---- statement scanners ---------------------------------------------
+
+ /**
+ * Checks whether the given statement tree contains any
+ * {@code AsyncSupport.yieldReturn()} calls, indicating that the enclosing
+ * body is an async generator (which should produce an
+ * {@link groovy.concurrent.AsyncStream AsyncStream} rather than an
+ * {@link groovy.concurrent.Awaitable Awaitable}).
+ * <p>
+ * The scan does <em>not</em> descend into nested {@link ClosureExpression}s
+ * because each nested {@code async { ... }} closure handles its own
+ * generator wrapping independently.
+ *
+ * @param code the statement tree to scan
+ * @return {@code true} if at least one {@code yieldReturn} call is found
+ */
+ public static boolean containsYieldReturn(Statement code) {
+ return containsStaticCall(code, YIELD_RETURN_METHOD);
+ }
+
+ // ---- rewriting utilities --------------------------------------------
+
+ /**
+ * Rewrites {@code AsyncSupport.yieldReturn(expr)} calls to
+ * {@code AsyncSupport.yieldReturn($__asyncGen__, expr)}, injecting the
+ * synthetic generator parameter as the first argument.
+ * <p>
+ * This enables ThreadLocal-free yield semantics: the
+ * {@link org.apache.groovy.runtime.async.AsyncStreamGenerator} instance is
+ * passed explicitly through the closure parameter rather than being
+ * stored in a thread-local variable.
+ * <p>
+ * Does <em>not</em> descend into nested {@link ClosureExpression}s.
+ *
+ * @param code the statement tree to transform
+ * @param genParam the synthetic {@code $__asyncGen__} parameter whose
+ * variable reference will be injected
+ */
+ public static void injectGenParamIntoYieldReturnCalls(Statement code, Parameter genParam) {
+ code.visit(new CodeVisitorSupport() {
+ @Override
+ public void visitExpressionStatement(ExpressionStatement stmt) {
+ Expression expr = stmt.getExpression();
+ if (expr instanceof StaticMethodCallExpression smce
+ && YIELD_RETURN_METHOD.equals(smce.getMethod())
+ && ASYNC_SUPPORT_CLASS.equals(smce.getOwnerType().getName())) {
+ VariableExpression genRef = varX(ASYNC_GEN_PARAM_NAME);
+ genRef.setAccessedVariable(genParam);
+ ArgumentListExpression newArgs = new ArgumentListExpression();
+ newArgs.addExpression(genRef);
+ if (smce.getArguments() instanceof ArgumentListExpression argList) {
+ for (Expression arg : argList.getExpressions()) {
+ newArgs.addExpression(arg);
+ }
+ }
+ StaticMethodCallExpression replacement =
+ new StaticMethodCallExpression(smce.getOwnerType(), YIELD_RETURN_METHOD, newArgs);
+ replacement.setSourcePosition(smce);
+ stmt.setExpression(replacement);
+ }
+ super.visitExpressionStatement(stmt);
+ }
+
+ @Override
+ public void visitClosureExpression(ClosureExpression expression) {
+ // Do not descend into nested closures
+ }
+ });
+ }
+
+ /**
+ * Creates the synthetic {@code $__asyncGen__} parameter used by async
+ * generator closures. The parameter is dynamically typed.
+ *
+ * @return a new {@link Parameter} instance
+ */
+ public static Parameter createGenParam() {
+ return new Parameter(ClassHelper.OBJECT_TYPE, ASYNC_GEN_PARAM_NAME);
+ }
+
+ /**
+ * Checks whether the given statement tree contains any
+ * {@code AsyncSupport.defer()} calls, indicating that the enclosing
+ * method uses the {@code defer} statement and needs a try-finally
+ * wrapper to execute deferred actions on exit.
+ * <p>
+ * Does <em>not</em> descend into nested {@link ClosureExpression}s.
+ *
+ * @param code the statement tree to scan
+ * @return {@code true} if at least one {@code defer} call is found
+ */
+ public static boolean containsDefer(Statement code) {
+ return containsStaticCall(code, DEFER_METHOD);
+ }
+
+ /**
+ * Scans the given statement tree for a static method call to
+ * {@code AsyncSupport.<methodName>()}. Does <em>not</em> descend
+ * into nested {@link ClosureExpression}s, since each nested closure
+ * manages its own transformation independently.
+ *
+ * @param code the statement tree to scan
+ * @param methodName the method name to look for on {@code AsyncSupport}
+ * @return {@code true} if at least one matching call is found
+ */
+ private static boolean containsStaticCall(Statement code, String methodName) {
+ boolean[] found = {false};
+ code.visit(new CodeVisitorSupport() {
+ @Override
+ public void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
+ if (methodName.equals(call.getMethod())
+ && ASYNC_SUPPORT_CLASS.equals(call.getOwnerType().getName())) {
+ found[0] = true;
+ }
+ if (!found[0]) {
+ super.visitStaticMethodCallExpression(call);
+ }
+ }
+
+ @Override
+ public void visitClosureExpression(ClosureExpression expression) {
+ // Do not descend into nested closures
+ }
+ });
+ return found[0];
+ }
+
+ /**
+ * Wraps the given statement body with defer scope management.
+ * Generates the equivalent of:
+ * <pre>{@code
+ * def $__deferScope__ = AsyncSupport.createDeferScope()
+ * try {
+ * <originalBody>
+ * } finally {
+ * AsyncSupport.executeDeferScope($__deferScope__)
+ * }
+ * }</pre>
+ *
+ * @param body the original method or closure body
+ * @return a new block statement with defer scope initialization and try-finally
+ */
+ public static Statement wrapWithDeferScope(Statement body) {
+ // def $__deferScope__ = AsyncSupport.createDeferScope()
+ Expression createCall = callX(ASYNC_SUPPORT_TYPE,
+ CREATE_DEFER_SCOPE_METHOD, ArgumentListExpression.EMPTY_ARGUMENTS);
+ ExpressionStatement initStmt = new ExpressionStatement(
+ new DeclarationExpression(
+ varX(DEFER_SCOPE_VAR),
+ Token.newSymbol(Types.ASSIGN, -1, -1),
+ createCall));
+
+ // finally { AsyncSupport.executeDeferScope($__deferScope__) }
+ Expression executeCall = callX(ASYNC_SUPPORT_TYPE,
+ EXECUTE_DEFER_SCOPE_METHOD,
+ args(varX(DEFER_SCOPE_VAR)));
+ TryCatchStatement tryCatch = new TryCatchStatement(
+ body,
+ block(new ExpressionStatement(executeCall)));
+
+ return block(initStmt, tryCatch);
+ }
+}
diff --git a/src/resources/META-INF/services/groovy.concurrent.AwaitableAdapter b/src/resources/META-INF/services/groovy.concurrent.AwaitableAdapter
new file mode 100644
index 0000000000..a591ba6a55
--- /dev/null
+++ b/src/resources/META-INF/services/groovy.concurrent.AwaitableAdapter
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+org.apache.groovy.runtime.async.FlowPublisherAdapter
diff --git a/src/spec/doc/core-async-await.adoc b/src/spec/doc/core-async-await.adoc
new file mode 100644
index 0000000000..6540e61a9d
--- /dev/null
+++ b/src/spec/doc/core-async-await.adoc
@@ -0,0 +1,1624 @@
+//////////////////////////////////////////
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+//////////////////////////////////////////
+
+= Async/Await
+:jdk: https://docs.oracle.com/en/java/javase/21/docs/api
+
+[[async-await]]
+== Introduction
+
+Groovy provides native `async`/`await` support as a language-level feature, enabling developers to write
+asynchronous code in a sequential, readable style. Inspired by similar constructs in JavaScript, C#, Kotlin,
+Swift, and Go,
+Groovy's async/await integrates seamlessly with the JVM concurrency model while maintaining Groovy's hallmark
+conciseness and expressiveness.
+
+Key capabilities include:
+
+* **`async` methods** — declare methods that execute asynchronously and return an `Awaitable`
+* **`await` expressions** — suspend execution until an asynchronous result is available
+* **Async closures and lambdas** — create reusable asynchronous functions
+* **`for await`** — iterate over asynchronous data sources
+* **`yield return`** — produce asynchronous streams (generators)
+* **`defer`** — schedule cleanup actions that run when the method completes (LIFO order)
+* **`AsyncContext`** — propagate request, trace, and tenant metadata across async boundaries
+* **`Awaitable.go`, `Channel`, and `Awaitable.any`** — task spawning and channel-based communication primitives
+* **Framework integration** — built-in adapters for `CompletableFuture` and `Future`;
+`Flow.Publisher` support via an auto-discovered runtime adapter;
+extensible to RxJava, Reactor, and Spring via the adapter registry
+
+On JDK 21+, async methods automatically leverage
+{jdk}/java.base/java/lang/Thread.html#ofVirtual()[virtual threads] for optimal scalability.
+
+[[why-async-await]]
+== Why Groovy Needs Async/Await
+
+Modern applications are overwhelmingly concurrent. Web servers, microservices, data
+pipelines, and interactive UIs all spend most of their time waiting — waiting for
+network responses, database queries, file I/O, or downstream services. The question
+is not _whether_ to write asynchronous code, but _how pleasant the experience will
+be_.
+
+=== The Problem: Callback Complexity on the JVM
+
+Before async/await, JVM developers faced a fundamental tension: synchronous code is
+easy to read but wastes threads, while asynchronous code scales well but becomes
+hard to follow.
+
+Consider a typical three-step workflow — fetch a user ID, look up a name, then load a
+profile — expressed with `CompletableFuture`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_callback_hell,indent=0]
+----
+
+This code works, but the business logic is obscured by the plumbing.
+Each `.thenCompose()` introduces a nesting level, exception recovery is
+separated from the code that causes the exception, and the control flow
+reads inside-out rather than top-to-bottom.
+
+With `async`/`await`, exactly the same logic becomes:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_async_await_clean,indent=0]
+----
+
+The async version reads like a synchronous script. Variables are declared
+at the point of use, the return value is obvious, and there are no
+callbacks, lambdas, or chained combinators.
+
+=== Native Exception Handling
+
+One of the most painful aspects of callback-style async code is exception handling.
+With `CompletableFuture`, exceptions are wrapped in `CompletionException` or
+`ExecutionException`, requiring manual unwrapping. Groovy's `await` _automatically_
+unwraps these layers and re-throws the original exception, making standard
+`try`/`catch` blocks work exactly as they do in synchronous code:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_exception_handling,indent=0]
+----
+<1> The original `IOException` is caught directly — no `CompletionException` wrapper
+
+This _exception transparency_ guarantee means that switching a synchronous method
+to `async` does not change the exception types visible to callers.
+
+=== Async Streams and Back-Pressure
+
+Asynchronous programming isn't limited to single-value futures. Many real-world
+scenarios involve _streams_ of values — sensor readings, paginated API results,
+log events — where values arrive over time. Groovy's `yield return` and
+`for await` offer a first-class solution:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_stream_processing,indent=0]
+----
+
+The producer yields values on demand, and the consumer pulls them with
+`for await`. Internally, the runtime uses a handoff queue between the two, providing
+natural **back-pressure**: the producer blocks on each `yield return` until the
+consumer is ready, preventing unbounded memory growth — all without requiring
+the developer to manage queues, signals, or synchronization.
+
+=== Seamless JVM Ecosystem Integration
+
+Groovy does not force developers to abandon their existing async investments.
+The `await` keyword natively understands `CompletableFuture`, `CompletionStage`,
+`Future`, and `Flow.Publisher` (via an auto-discovered runtime adapter):
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_interop,indent=0]
+----
+
+For third-party frameworks like RxJava, Reactor, or Spring, the
+`AwaitableAdapterRegistry` SPI allows registering custom adapters so that
+`await` works transparently with _any_ asynchronous type — a single
+`await` keyword is all the application code needs, regardless of the
+underlying async library.
+
+=== Virtual Threads: Free Scalability
+
+On JDK 21+, every `async` method runs on a
+{jdk}/java.base/java/lang/Thread.html#ofVirtual()[virtual thread].
+When the thread blocks on `await`, the JVM automatically parks the virtual thread
+and releases the underlying carrier (OS) thread. This means millions of concurrent
+`async` tasks can coexist on a handful of OS threads — achieving the same practical
+scalability as compiler-generated state machines in C# or Kotlin, but with
+_dramatically simpler_ compiler implementation and full debugging support
+(complete stack traces, breakpoints, step-through debugging all work unmodified).
+
+On JDK 17–20, a bounded cached thread pool with caller-runs back-pressure
+provides stable performance until virtual threads become available.
+
+=== Why a Language Feature, Not a Library?
+
+GPars, RxJava, and Reactor already provide powerful async toolkits for the JVM. So
+why build async/await into the _language_?
+
+[cols="1,3"]
+|===
+| Reason | Explanation
+
+| **Readability**
+| `await` makes asynchronous code indistinguishable from synchronous code.
+Libraries can provide combinators, but they cannot change the way `for`, `try`,
+`if`, and assignment statements compose.
+
+| **Exception transparency**
+| Language-level `await` can intercept and unwrap JVM exception wrappers before they
+reach the programmer. A library-level `await()` method cannot prevent `CompletionException`
+from leaking into user code without bytecode manipulation.
+
+| **Control-flow integration**
+| `for await` integrates with the parser and AST. There is no way for a library to add
+new loop syntax. Similarly, `yield return` requires the compiler to rewrite the method
+into a generator — a transformation that no library can perform.
+
+| **`defer` semantics**
+| `defer` requires compiler-generated `try`/`finally` wrapping with LIFO
+execution ordering. This cannot be emulated with a simple method call.
+
+| **Tooling**
+| Syntax highlighting, code completion, and static analysis can recognize `async`,
+`await`, `for await`, and `yield return` as first-class constructs. Library-level
+equivalents are just method calls to the IDE.
+|===
+
+In short, async/await elevates asynchronous programming from a _library concern_
+to a _language concern_, giving Groovy the same expressive power that developers
+have come to expect in JavaScript, C#, Kotlin, and Swift.
+
+[[async-methods]]
+== Async Methods
+
+An `async` method executes its body asynchronously and returns a
+`groovy.concurrent.Awaitable` — a promise-like abstraction representing a computation that
+may not have completed yet.
+
+=== Basic Declaration
+
+Add the `async` modifier before the method's return type (or `def`):
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_method_basic,indent=0]
+----
+
+The caller receives an `Awaitable` immediately; the method body runs on a separate thread.
+Calling `get()` blocks until the result is available.
+
+=== Typed Return Values
+
+Async methods support explicit return types. The compiler wraps the declared type inside `Awaitable` automatically:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_method_typed,indent=0]
+----
+
+=== Void Methods
+
+Even `async void` methods return an `Awaitable<Void>`, allowing callers to wait for completion:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_method_void,indent=0]
+----
+
+=== Static Methods
+
+The `async` modifier works with static methods:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_method_static,indent=0]
+----
+
+=== Script-Level Methods
+
+Async methods can be defined directly in Groovy scripts:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_script,indent=0]
+----
+
+[[await-expression]]
+== The `await` Expression
+
+The `await` keyword suspends execution of the enclosing async method until the given asynchronous
+operation completes, then returns its result. It unwraps the value from an `Awaitable`,
+`CompletableFuture`, `CompletionStage`, `Future`, or any type registered with the adapter system.
+
+=== Basic Usage
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_basic,indent=0]
+----
+
+=== Awaiting CompletableFuture
+
+Groovy's `await` works directly with `java.util.concurrent.CompletableFuture`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_completable_future,indent=0]
+----
+
+=== Combining Multiple Awaits
+
+Multiple `await` expressions can appear in a single method:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_arithmetic,indent=0]
+----
+
+=== Parenthesized Form
+
+Both `await expr` and `await(expr)` are valid syntax:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_parenthesized,indent=0]
+----
+
+[NOTE]
+====
+The parenthesized form `await(expr)` is recommended when `await` is used in complex expressions
+(e.g., `await(f1) + await(f2)`) to avoid potential ambiguities.
+====
+
+[[async-closures-lambdas]]
+== Async Closures and Lambdas
+
+The `async` keyword can precede a closure or lambda expression to create a reusable
+asynchronous function. The result is a `Closure` that, when invoked, returns an `Awaitable`.
+
+[IMPORTANT]
+====
+`async { ... }` creates a _closure_, not an immediately executing task. You must explicitly
+invoke the closure (e.g., `asyncTask()`) to start asynchronous execution.
+This is analogous to defining an `async function` in JavaScript without calling it.
+====
+
+=== No-Argument Closure
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_closure_basic,indent=0]
+----
+
+=== Parameterized Closure
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_closure_params,indent=0]
+----
+
+=== Multiple Parameters
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_closure_multi_params,indent=0]
+----
+
+=== Closures in Collections
+
+Async closures are first-class values and can be stored in data structures:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_closure_collection,indent=0]
+----
+
+=== Lambda Syntax
+
+The `async` keyword also works with Groovy's lambda syntax:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_lambda,indent=0]
+----
+
+[[for-await]]
+[[for_await_section]]
+== `for await` — Async Iteration
+
+The `for await` loop iterates over asynchronous data sources (`AsyncStream`, `Flow.Publisher`, `Channel`, `Iterable`).
+Each element is resolved asynchronously before the loop body executes. This is analogous to JavaScript's
+`for await...of` and C#'s `await foreach`.
+
+The compiler generates a `try`/`finally` block around the loop that calls `close()` on the
+stream when iteration ends — whether by exhaustion, `break`, `return`, or exception.
+This guarantees that upstream resources (producer threads, `Flow.Publisher` subscriptions)
+are released promptly.
+
+=== Basic Iteration
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_basic,indent=0]
+----
+
+=== Transformation
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_transform,indent=0]
+----
+
+=== Early Termination
+
+Standard flow control (`return`, `break`) works inside `for await`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_break,indent=0]
+----
+
+=== Colon Notation
+
+`for await` supports both `in` and `:` notation:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_colon,indent=0]
+----
+
+[[yield-return]]
+== `yield return` — Async Generators
+
+The `yield return` statement enables async methods to produce a stream of values lazily.
+A method containing `yield return` returns an `AsyncStream<T>` instead of a regular `Awaitable<T>`.
+This is analogous to C#'s `yield return` within `IAsyncEnumerable<T>` and JavaScript's `async function*`
+generators.
+
+The producer (generator) and consumer (`for await` loop) coordinate via a
+handoff queue, providing natural back-pressure: the producer blocks on each `yield return`
+until the consumer is ready for the next value. This design means:
+
+* The generator body runs on its own thread, executing sequentially like any normal method
+* No values are buffered — the producer suspends until the consumer pulls the next item
+* If the consumer stops early (`break`, `return`, exception), the producer thread is
+interrupted, preventing resource leaks
+* The generator can mix `yield return` with `await`, performing I/O between yields
+
+NOTE: `yield return` can only appear inside an `async` method or `async` closure/lambda.
+Using it elsewhere produces a compile-time error:
+_"`yield return` can only be used inside an async method or async closure/lambda"_.
+
+=== Basic Generator
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=yield_return_basic,indent=0]
+----
+
+=== Generator with Loop
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=yield_return_loop,indent=0]
+----
+
+=== Filtered Generator
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=yield_return_filter,indent=0]
+----
+
+=== Combining `yield return` with `await`
+
+Async generators can perform asynchronous operations between yielding values:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=yield_return_with_await,indent=0]
+----
+
+[[defer]]
+== `defer` — Deferred Cleanup
+
+The `defer` keyword schedules a block of code to execute when the enclosing async method
+completes, regardless of whether it succeeds or throws an exception. Multiple deferred blocks
+execute in _last-in, first-out_ (LIFO) order, exactly as in
+https://go.dev/blog/defer-panic-and-recover[Go's `defer` statement].
+
+NOTE: `defer` can only appear inside an `async` method or `async` closure. Using it
+elsewhere produces a compile-time error: _"`defer` can only be used inside an async method or
+async closure"_.
+
+=== Basic Usage
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=defer_basic,indent=0]
+----
+
+=== Guaranteed Execution on Exception
+
+Deferred blocks always execute, even when the method throws an exception:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=defer_exception,indent=0]
+----
+
+=== Resource Cleanup Pattern
+
+`defer` is ideal for resource management — acquire a resource, immediately defer its release:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=defer_resource_cleanup,indent=0]
+----
+
+=== Async Cleanup in Deferred Blocks
+
+Deferred blocks may also return an `Awaitable`, `CompletionStage`, or `Future`.
+Groovy waits for that asynchronous cleanup to finish before the enclosing async
+method completes, which makes `defer` practical for non-blocking shutdown paths
+such as flushing telemetry, closing async streams, or returning buffered work
+to a channel:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=defer_async_cleanup,indent=0]
+----
+
+[TIP]
+====
+When an exception occurs in a deferred block and the enclosing method has also thrown,
+the deferred exception is _suppressed_ (attached via `addSuppressed()`) so that the original
+exception remains the primary failure.
+====
+
+[[exception-handling]]
+== Exception Handling
+
+Groovy's async/await provides transparent exception propagation. Exceptions thrown inside
+an async method are captured by the `Awaitable` and re-thrown when the caller invokes `await`.
+The original exception type, message, and stack trace are preserved — wrapper types such as
+`CompletionException` and `ExecutionException` are automatically unwrapped.
+
+=== Exception Transparency
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=exception_transparency,indent=0]
+----
+
+Standard `try`/`catch` blocks work naturally with `await`, catching the original exception type
+(not a wrapper). This is the same transparency guarantee provided by C#'s `await` and
+JavaScript's `await`.
+
+=== Handling Failures from Multiple Tasks
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=exception_multiple_tasks,indent=0]
+----
+
+[[utility-methods]]
+== Combinators
+
+The `groovy.concurrent.Awaitable` interface provides static combinator methods for common async
+coordination patterns, analogous to JavaScript's `Promise.all()` / `Promise.any()` /
+`Promise.allSettled()`, C#'s `Task.WhenAll()` / `Task.WhenAny()`, and Kotlin's coroutine
+coordination helpers.
+
+=== `all` — Parallel Execution
+
+Wait for all tasks to complete and collect their results. Fails fast on the first error:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_all,indent=0]
+----
+
+=== `any` — Race Pattern
+
+Return the result of the first task to complete:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_any,indent=0]
+----
+
+=== `allSettled` — Inspect All Outcomes
+
+Wait for all tasks to complete, collecting both successes and failures. Never throws —
+each result is wrapped in an `AwaitResult` with `isSuccess()`, `isFailure()`, `value`, and `error` properties:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_all_settled,indent=0]
+----
+
+[[await-result]]
+=== `AwaitResult` — Outcome Wrapper
+
+Each element returned by `allSettled` is an `AwaitResult<T>`. This immutable value type
+carries either a success value or a failure throwable, and provides safe accessors:
+
+* `isSuccess()` / `isFailure()` — check outcome type
+* `value` — the result (throws `IllegalStateException` on failure)
+* `error` — the exception (throws `IllegalStateException` on success)
+* `getOrElse(Function)` — recover from failures with a fallback function
+* `toString()` — returns `AwaitResult.Success[value]` or `AwaitResult.Failure[error]`
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_result_api,indent=0]
+----
+
+=== Continuation Helpers
+
+Use `thenAccept`, `whenComplete`, and `handle` when you want continuation-style
+composition rather than a direct `await`. This is especially useful at API boundaries
+where you want to attach logging, recovery, or side effects without immediately
+blocking for the result.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=continuation_helpers,indent=0]
+----
+
+=== `delay` — Non-Blocking Pause
+
+Create an awaitable timer that completes after the specified duration:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=delay_example,indent=0]
+----
+
+=== `orTimeout` / `completeOnTimeout` — Non-Blocking Deadlines
+
+Apply a deadline without blocking the calling thread. `Awaitable.orTimeoutMillis(...)`
+(or `Awaitable.orTimeout(source, duration, unit)` for explicit units)
+fails with `TimeoutException`, while `Awaitable.completeOnTimeoutMillis(...)`
+(or `Awaitable.completeOnTimeout(source, fallback, duration, unit)`) completes with a
+fallback value. The instance forms `orTimeoutMillis(...)` and `completeOnTimeoutMillis(...)`
+provide the same capability on an existing awaitable.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=timeout_combinators,indent=0]
+----
+
+==== Instance Forms: `orTimeoutMillis` and `completeOnTimeoutMillis`
+
+The instance methods `orTimeoutMillis(millis)` and `completeOnTimeoutMillis(fallback, millis)` provide
+a fluent alternative to the static `Awaitable.orTimeoutMillis()` and `Awaitable.completeOnTimeoutMillis()` forms:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=instance_timeout_methods,indent=0]
+----
+
+[[flow-publisher]]
+== `Flow.Publisher` Integration
+
+Groovy's await system has built-in support for
+{jdk}/java.base/java/util/concurrent/Flow.Publisher.html[`java.util.concurrent.Flow.Publisher`],
+the reactive streams interface standardized in JDK 9. This enables seamless consumption of
+reactive streams from any library that implements the `Flow.Publisher` contract (Project Reactor,
+RxJava 3 via adapters, Vert.x, etc.).
+
+The internal adapter uses a small bounded buffer to bridge the push-based `Publisher`
+model to Groovy's pull-based `AsyncStream` model, preventing unbounded memory growth with
+fast publishers while maintaining throughput. Back-pressure is enforced by requesting exactly
+one item at a time; demand for the next element is signalled _before_ the consumer's
+`moveNext()` awaitable completes, so the publisher can begin producing the next value while
+the consumer processes the current one.
+
+The examples below use deterministic subscriber-handshake waiting with a bounded timeout
+before submitting values, avoiding timing races and unbounded waits in CI environments.
+
+=== Awaiting a Single Value
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=flow_publisher_await,indent=0]
+----
+
+=== Iterating with `for await`
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=flow_publisher_for_await,indent=0]
+----
+
+[NOTE]
+====
+`for await` closes the adapted stream from a compiler-generated `finally` block.
+For `Flow.Publisher` sources, this means an early `break`, `return`, or exception
+cancels the upstream subscription rather than leaving it running in the background.
+This is conceptually similar to JavaScript async iterators' `return()`, C#'s
+`await foreach` disposal, and Swift's `AsyncSequence` early-exit cleanup.
+====
+
+[[adapter-registry]]
+== Adapter Registry
+
+=== Converting External Types with `from()`
+
+The `Awaitable.from(source)` and `AsyncStream.from(source)` static methods are the recommended
+entry points for converting external async types to Groovy's native abstractions. They delegate
+to the `AwaitableAdapterRegistry` internally but provide a more discoverable, user-friendly API:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=from_conversion,indent=0]
+----
+
+`Awaitable.from()` accepts any type supported by a registered adapter: `CompletableFuture`,
+`CompletionStage`, `Future`, `Flow.Publisher`, or any third-party type with a custom adapter.
+If the source is already an `Awaitable`, it is returned as-is. `AsyncStream.from()` works
+similarly for multi-value sources such as `Iterable`, `Iterator`, `Channel`, or `Flow.Publisher`.
+
+=== Built-in Adapters
+
+The `groovy.concurrent.AwaitableAdapterRegistry` allows extending `await` to support additional
+asynchronous types from third-party frameworks. The built-in adapter handles:
+
+* `groovy.concurrent.Awaitable` (native Groovy promise — passthrough)
+* `java.util.concurrent.CompletableFuture` and `CompletionStage`
+* `java.util.concurrent.Future` (blocking, delegated to a configurable executor)
+* `groovy.concurrent.Channel` (automatically converted to `AsyncStream` for `for await`)
+
+Additionally, `java.util.concurrent.Flow.Publisher` support (single-value `await` and
+multi-value `for await`) is provided by an internal runtime adapter
+that is auto-discovered via `ServiceLoader`.
+This decoupled design allows applications to override the default
+reactive-streams bridge with a framework-specific adapter (e.g., one backed by Reactor's
+native scheduler) by registering a higher-priority adapter.
+
+=== Registering a Custom Adapter
+
+Implement the `AwaitableAdapter` interface and register it with the registry. The `register()`
+method returns an `AutoCloseable` handle for deregistration, making it easy to use in tests
+or scoped contexts:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=adapter_registration,indent=0]
+----
+
+=== Framework Integration
+
+With appropriate adapters registered, `await` works transparently with any async type.
+Adapter implementations for frameworks such as RxJava 3, Project Reactor, and Spring
+should delegate to the framework's native blocking or conversion methods:
+
+* **RxJava 3**: `Single.blockingGet()`, `Observable.blockingIterable()` (for `AsyncStream`)
+* **Reactor**: `Mono.block()`, `Flux.toIterable()` (for `AsyncStream`)
+* **Spring**: Return `Awaitable` from `@Async` service methods; adapt `ListenableFuture` via `toCompletableFuture()`
+
+=== Automatic Discovery
+
+Adapters can also be discovered automatically via `java.util.ServiceLoader` by placing a
+provider configuration file at `META-INF/services/groovy.concurrent.AwaitableAdapter`.
+SPI-discovered adapters are loaded at class-initialization time and take priority over
+the built-in adapters. Runtime-registered adapters (via `register()`) take highest
+priority, allowing application code to override any default.
+
+[[async-annotation]]
+== `@Async` Annotation
+
+As an alternative to the `async` keyword modifier, the `@Async` annotation provides the same
+functionality in a more traditional Java-style declaration:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_annotation,indent=0]
+----
+
+The `async` keyword modifier is the preferred form. The `@Async` annotation is useful when
+interoperating with tools or frameworks that rely on annotation processing. Both forms
+produce identical compiled bytecode.
+
+=== Custom Executor via `@Async`
+
+By default, `@Async` methods use the global async executor (virtual threads on JDK 21+, or
+the configurable thread pool on earlier JDKs). You can route specific methods to a dedicated
+executor by naming a field in the declaring class:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_annotation_executor,indent=0]
+----
+
+The `executor` parameter must refer to a field of type `java.util.concurrent.Executor`
+(or its subtype). This is particularly useful when different methods have different
+threading requirements — for example, CPU-bound work on a fixed pool and I/O-bound work
+on virtual threads.
+
+[[executor-configuration]]
+== Executor Configuration
+
+=== Default Behavior
+
+On **JDK 21+**, async methods run on
+{jdk}/java.base/java/lang/Thread.html#ofVirtual()[virtual threads]
+for optimal scalability.
+On **JDK 17–20**, a cached thread pool is used (up to 256 threads by default, configurable
+via the `groovy.async.parallelism` system property). When all threads are busy, the submitting
+thread executes the task directly (caller-runs policy), providing natural back-pressure instead
+of rejecting the task.
+
+=== Custom Executor
+
+Override the default executor via `Awaitable.setExecutor()`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=custom_executor,indent=0]
+----
+
+Pass `null` to restore the default executor.
+
+[[patterns]]
+== Common Patterns
+
+=== Parallel Web Requests
+
+Launch multiple I/O tasks concurrently and collect all results:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=parallel_pattern,indent=0]
+----
+
+=== Retry with Exponential Backoff
+
+Automatically retry a transient failure with increasing delays:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=retry_pattern,indent=0]
+----
+
+=== Timeout
+
+Use `Awaitable.orTimeoutMillis()` (or the instance form `orTimeoutMillis()`) to apply a
+deadline without dropping back to manual race logic:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=timeout_pattern,indent=0]
+----
+
+[[cancellation]]
+=== Cancellation
+
+An `Awaitable` can be cancelled via the `cancel()` method. Once cancelled, any subsequent
+`await` on the task throws a `java.util.concurrent.CancellationException`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=cancellation,indent=0]
+----
+
+[NOTE]
+====
+Cancellation sets the `Awaitable` state to cancelled but does _not_ reliably interrupt the
+underlying thread (a limitation of `CompletableFuture`). For cooperative cancellation in
+long-running async bodies, check `Thread.currentThread().isInterrupted()` periodically.
+====
+
+[[best-practices]]
+== Best Practices
+
+[[async-all-the-way]]
+=== Asynchrony Is Viral — `await` All the Way
+
+Once a method becomes `async`, its callers should also be `async`. Partially async call chains
+— where an intermediate layer blocks with `.get()` — waste threads and risk pool starvation:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=sync_over_async,indent=0]
+----
+
+[[prefer-awaitable-of]]
+=== Prefer `Awaitable.of()` for Pre-Computed Data
+
+When the result is already known, use `Awaitable.of(value)` to avoid spawning a thread.
+Use `Awaitable.failed(error)` for pre-computed error results:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=awaitable_of,indent=0]
+----
+
+[[prefer-await-over-then]]
+=== Prefer `await` over `.then()` / `.thenCompose()`
+
+Sequential `await` calls read like synchronous code and provide better stack traces
+than chained `.then()` / `.thenCompose()`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=prefer_await_over_then,indent=0]
+----
+
+[[async-caching]]
+=== Async Lazy Caching
+
+Store `Awaitable` values in a `ConcurrentHashMap` so that only the first request triggers the
+async operation, and subsequent requests await the same cached result:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_caching,indent=0]
+----
+
+[[static-factory-pattern]]
+=== Static Factory for Async Construction
+
+When a class requires asynchronous initialization, use a static `async` factory
+method instead of blocking in the constructor:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=static_factory,indent=0]
+----
+
+[[fire-and-forget]]
+=== Fire-and-Forget Safely
+
+Always capture the `Awaitable` returned by async methods. Uncaptured awaitables may
+lose exceptions silently:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=fire_and_forget,indent=0]
+----
+
+[[inspection]]
+== Inspecting Awaitable State
+
+An `Awaitable` provides non-blocking inspection methods for checking its state without
+blocking the calling thread:
+
+* `isDone()` — `true` when the awaitable has completed (successfully, exceptionally, or via cancellation)
+* `isCancelled()` — `true` when the awaitable was cancelled via `cancel()`
+* `isCompletedExceptionally()` — `true` when the awaitable completed with an exception (including cancellation)
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=inspection_methods,indent=0]
+----
+
+[[async-modifier-restrictions]]
+== `async` Modifier Restrictions
+
+The `async` keyword is valid only on **method declarations** (including script-level methods)
+and **closure/lambda expressions**. Applying `async` to other declarations is a compile-time error:
+
+* **Class/interface declarations**: `async class Foo {}` → parser error
+* **Field/variable declarations**: `async def x = 1` → compilation error ("async not allowed for variable declarations")
+* **Constructor declarations**: `async Foo() {}` → compilation error ("async not allowed for constructors")
+
+[[implementation-details]]
+== Implementation Notes
+
+This section provides architectural context for Groovy language maintainers and advanced
+users who want to understand how async/await works under the hood. The descriptions
+focus on _design principles and behavioural guarantees_ rather than internal class names,
+so they remain valid even as the runtime implementation evolves.
+
+[[implementation-strategy]]
+=== Thread-Based vs State Machine
+
+Languages like C#, Kotlin, and Swift transform async workflows into compiler-managed
+state machines or coroutine frames. Each suspension point becomes resumable state,
+allowing the runtime to suspend the logical task without blocking an OS thread.
+JavaScript takes a different route again, relying on an event loop plus promise
+microtasks.
+
+Groovy takes a different approach: each `async` method body runs on its **own thread**
+(a virtual thread on JDK 21+, or a pooled platform thread on JDK 17–20). The `await`
+keyword blocks the running thread until the awaited result is available.
+
+On JDK 21+, virtual threads make this blocking essentially free — the JVM automatically
+parks the virtual thread and releases the underlying carrier thread, achieving the same
+practical efficiency as a state machine. The thread-based approach has several advantages:
+
+* **Simpler debugging** — stack traces show the complete call chain rather than
+ fragmented state machine frames
+* **Full language support** — all control-flow constructs (`try`/`catch`, loops,
+ `switch`, closures) work naturally inside async methods without special compiler handling
+* **Minimal compiler changes** — no need to rewrite control flow into a switch-based
+ state machine, keeping the implementation maintainable
+* **Natural blocking semantics** — existing blocking APIs (I/O, JDBC, etc.) work
+ transparently inside `async` methods without explicit wrapping
+
+On JDK 17–20, the thread pool is bounded (default 256 threads) with a caller-runs policy
+for back-pressure, ensuring stable performance even without virtual threads.
+
+[[architecture]]
+=== Architecture Overview
+
+The async/await feature is implemented across three layers:
+
+**Grammar layer**: Defines `async`, `await`, `defer` as contextual keywords, along with
+parser rules for `for await`, `yield return`, and `defer` statements.
+
+**AST transformation layer**: Converts parse trees into AST nodes, rewriting `async`
+method bodies into calls to runtime support methods. The `@Async` annotation is processed
+by the same transformation pipeline, ensuring consistent code generation for both the
+keyword and annotation forms.
+
+**Runtime layer**: Provides the static methods invoked by compiler-generated code —
+`await`, `async`, `defer`, `yield return`, timeout composition, stream cleanup, and
+combinators. Key design decisions include:
+
+* A dedicated promise wrapper decouples the public `Awaitable` API from `CompletableFuture`,
+ so the implementation can evolve independently of the user-facing contract.
+* Queue-based `AsyncStream` implementations share a common template base class (Template
+ Method pattern) that centralises signal dispatch, lifecycle management, and interrupt
+ handling. Subclasses only override a small number of hook methods — e.g. one subclass
+ powers async generators (`yield return`), another bridges `Flow.Publisher` sources.
+* The adapter registry (`AwaitableAdapterRegistry`) provides an SPI extension point for
+ third-party async types, with `ServiceLoader`-based auto-discovery and runtime
+ registration.
+
+==== API Decoupling
+
+The public API that Groovy developers interact with is defined entirely in the
+`groovy.concurrent` package (`Awaitable`, `AsyncStream`, `AwaitResult`,
+`AwaitableAdapter`, `AwaitableAdapterRegistry`). All implementation classes live in an
+internal package that is not part of the public contract.
+
+This separation means:
+
+* User code depends on `Awaitable`, _not_ on `CompletableFuture`. If the JDK async
+ infrastructure evolves (e.g., structured concurrency in future JDK releases), only the
+ internal implementation needs to change — the public `Awaitable` contract remains stable.
+* The promise wrapper is the sole bridge between `Awaitable` and `CompletableFuture`.
+ Replacing it (for example, with a structured-concurrency-based implementation) would be
+ transparent to all application code.
+* `toCompletableFuture()` is the one explicit escape hatch for interoperating with Java
+ APIs that require a `CompletableFuture` directly.
+
+[[thread-safety]]
+=== Thread Safety and Robustness
+
+A key design goal is that **thread safety is the framework's responsibility, not the
+user's**. All concurrency control is encapsulated inside the runtime, so application code
+never needs explicit locks, atomics, or volatile annotations.
+
+==== Lock-Free Synchronization
+
+All runtime components employ lock-free or minimal-contention synchronization. Shared
+mutable state uses `volatile` fields for safe publication, `AtomicBoolean` for lifecycle
+flags, `AtomicReference` for thread and subscription tracking, and copy-on-write
+collections for read-heavy registries (such as the adapter list). No component acquires a
+monitor lock during normal operation.
+
+==== Async Generator Safety
+
+The async generator (powering `yield return` / `for await`) uses a handoff queue between
+the producer and consumer threads, with several concurrency safeguards handled
+transparently by the runtime:
+
+[cols="2,3"]
+|===
+| Concern | Guarantee
+
+| **Back-pressure**
+| The producer blocks on each `yield return` until the consumer calls `moveNext()`.
+No unbounded buffering can occur.
+
+| **Cooperative cancellation**
+| When the consumer calls `close()` (explicitly or via the compiler-generated `finally`
+block), the producer thread is interrupted, allowing it to clean up promptly even if
+blocked in I/O or a long computation.
+
+| **Race-free lifecycle**
+| The `closed` flag is checked _after_ registering the consumer thread in `moveNext()`,
+closing a race window where `close()` could execute between the initial check and the
+thread registration, which would leave the consumer blocked with no one to interrupt it.
+
+| **Idempotent close**
+| `close()` can be called multiple times from any thread without side effects.
+
+| **Signal delivery under interrupt**
+| If a terminal signal (`complete` or `error`) cannot be delivered because the thread is
+interrupted and no consumer is waiting, the generator force-closes itself to prevent the
+consumer from blocking indefinitely on a subsequent `moveNext()`.
+|===
+
+==== Flow.Publisher Adaptation Safety
+
+The `Flow.Publisher` bridge converts the push-based `Publisher` protocol into the
+pull-based `AsyncStream` model, with the following guarantees:
+
+[cols="2,3"]
+|===
+| Concern | Guarantee
+
+| **Back-pressure**
+| A small bounded buffer bridges push and pull. Demand is capped at one item per
+`moveNext()` call. Demand is signalled _before_ `moveNext()` returns, preventing
+livelock when producer and consumer share a thread pool.
+
+| **Interrupt-safe signal delivery**
+| Subscriber callbacks use blocking delivery with a non-blocking fallback when the
+publisher thread is interrupted. If the fallback also fails, the upstream subscription
+is cancelled and an error signal is injected to unblock the consumer.
+
+| **Allocation-free hot path**
+| `moveNext()` returns shared cached `Awaitable<Boolean>` constants for the value and
+end-of-stream cases, eliminating per-call object allocation on the hot path.
+
+| **Idempotent close**
+| `close()` can be called multiple times from any thread without side effects.
+|===
+
+These mechanisms ensure that `yield return` / `for await` code remains as simple as writing
+a synchronous loop, while the runtime handles all cross-thread coordination.
+
+[[cross-language-comparison]]
+== Cross-Language Comparison
+
+The following table compares Groovy's async/await with the corresponding features in
+JavaScript, C#, Kotlin, and Swift, for developers familiar with those languages.
+
+[cols="2,3,3,3,3,3"]
+|===
+| Feature | Groovy | JavaScript | C# | Kotlin | Swift
+
+| **Async declaration**
+| `async foo() { }` / `async returnType foo() { }`
+| `async function foo() { }`
+| `async Task<T> Foo() { }`
+| `suspend fun foo(): T`
+| `func foo() async throws -> T`
+
+| **Await expression**
+| `await expr` / `await(expr)`
+| `await expr`
+| `await expr`
+| `deferred.await()` / suspend call
+| `try await expr`
+
+| **Return type**
+| `Awaitable<T>`
+| `Promise<T>`
+| `Task<T>` / `ValueTask<T>`
+| `Deferred<T>` / suspended `T`
+| `Task<T, Error>` / `async` function returns `T`
+
+| **Async closure/lambda**
+| `async { \-> body }` / `async () \-> { body }`
+| `async () \=> body`
+| `async () \=> body`
+| `scope.async { body }`
+| `Task { body }`
+
+| **Async iteration**
+| `for await (x in src) { }` +
+_(supports `AsyncStream`, `Flow.Publisher`, `Channel`, `Iterable`)_
+| `for await (x of src) { }`
+| `await foreach (x in src) { }`
+| _(manual via `Flow`, `Channel`, or `Flow.collect`)_
+| `for try await x in seq`
+
+| **Channels**
+| `Awaitable.channel(n)` +
+`await ch.send(x)` / `await ch.receive()` +
+`for await (x in ch) { }`
+| _(none; use streams or message ports)_
+| `System.Threading.Channels` +
+`Channel<T>` / `ChannelReader.ReadAllAsync()`
+| `Channel<T>` / `channel.send()` +
+`for (x in channel) { }`
+| `AsyncStream` / `AsyncChannel` (3rd party)
+
+| **Async generator**
+| `yield return expr`
+| `yield expr` (in `async function*`)
+| `yield return expr` (in `IAsyncEnumerable<T>`)
+| `flow { emit(x) }` / channel producers
+| `AsyncStream { continuation.yield(x) }`
+
+| **Deferred cleanup**
+| `defer { cleanup }`
+| _(none; use `try`/`finally`)_
+| `await using` / `try`/`finally`
+| `try`/`finally` / `use`
+| `defer`
+
+| **Wait all**
+| `Awaitable.all(a, b, c)`
+| `Promise.all([a, b, c])`
+| `Task.WhenAll(a, b, c)`
+| `awaitAll(a, b, c)`
+| `async let` / `withTaskGroup`
+
+| **Wait any**
+| `Awaitable.any(a, b, c)`
+| `Promise.any([a, b, c])`
+| `Task.WhenAny(a, b, c)`
+| `select {}` / manual race
+| `TaskGroup.next()`
+
+| **Wait all settled**
+| `Awaitable.allSettled(a, b, c)`
+| `Promise.allSettled([a, b, c])`
+| _(manual; no built-in equivalent)_
+| _(manual; typically `runCatching`)_
+| _(manual; typically `Result`)_
+
+| **Pre-computed result**
+| `Awaitable.of(value)`
+| `Promise.resolve(value)`
+| `Task.FromResult(value)`
+| `CompletableDeferred(value)` / `async { value }`
+| `Task { value }`
+
+| **Pre-computed error**
+| `Awaitable.failed(error)`
+| `Promise.reject(error)`
+| `Task.FromException(ex)`
+| `CompletableDeferred<T>().completeExceptionally(ex)`
+| `Task { throw error }`
+
+| **Delay**
+| `Awaitable.delay(ms)`
+| `new Promise(r => setTimeout(r, ms))`
+| `Task.Delay(ms)`
+| `delay(ms)`
+| `Task.sleep(for:)`
+
+| **Timeout**
+| `Awaitable.orTimeoutMillis(task, ms)` / `task.orTimeoutMillis(ms)`
+| `Promise.race([...])` / `AbortSignal.timeout()`
+| `task.WaitAsync(timeout)` / `CancelAfter`
+| `withTimeout(ms) { ... }`
+| _(manual race with `TaskGroup` / sleep)_
+
+| **Cancellation**
+| `awaitable.cancel()`
+| `AbortController.abort()`
+| `CancellationToken`
+| `Job.cancel()`
+| `Task.cancel()`
+
+| **Executor/scheduler**
+| `Awaitable.setExecutor(exec)`
+| _(event loop; not configurable)_
+| `TaskScheduler`
+| `CoroutineDispatcher`
+| task priority / executor chosen by runtime
+
+| **Third-party support**
+| `AwaitableAdapterRegistry` SPI +
+`Awaitable.from()` / `AsyncStream.from()`
+| _(native `thenable` protocol)_
+| Custom awaiters via `GetAwaiter()`
+| coroutine adapters / bridges
+| `AsyncSequence` / `Task` interop patterns
+
+| **Annotation form**
+| `@Async`
+| _(none)_
+| _(none; attribute-driven async not supported)_
+| _(none)_
+| _(none)_
+
+| **Implementation**
+| Thread-per-task (virtual threads on JDK 21+)
+| Event loop + microtask queue
+| State machine (compiled IL transformation)
+| Coroutine state machine
+| Compiler-generated async state machine
+|===
+
+[[structured-concurrency]]
+== Structured Concurrency with `AsyncScope`
+
+=== The Problem: Uncontrolled Task Lifetimes
+
+When you launch async tasks with `async { ... }`, each task runs independently — it can
+outlive the method that created it, leak resources if it is never awaited, or silently
+swallow exceptions if nobody observes the result. This "fire-and-forget" model is
+convenient for simple cases but becomes a liability in production systems where every task
+must be accounted for.
+
+=== The Solution: Scoped Task Ownership
+
+`AsyncScope` binds the lifetime of child tasks to a well-defined scope. Inspired by
+Kotlin's `coroutineScope`, Swift's `TaskGroup`, and
+{jdk}/java.base/java/util/concurrent/StructuredTaskScope.html[JEP 453 (Structured Concurrency)],
+it enforces a simple invariant: **when the scope exits, all child tasks have completed or
+been cancelled**.
+
+`AsyncScope` integrates naturally with `async` methods and `await` expressions. You
+can call `async` methods inside `scope.async { ... }` blocks and `await` their results —
+just as you would in any other async context. The `withScope` convenience method creates
+a scope, executes a closure within it, and automatically closes the scope when the closure
+returns:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_scope_basic,indent=0]
+----
+
+In the example above, `fetchUser` and `fetchOrders` are ordinary `async` methods.
+`scope.async` launches each call as a child task whose lifetime is bound to the scope.
+The `await` expressions inside child tasks suspend until their respective async operations
+complete, and `await(userTask)` / `await(ordersTask)` suspend the parent until the scoped
+tasks deliver their results. When `withScope` returns, both tasks are guaranteed to have
+completed — no orphan tasks can leak.
+
+=== Fail-Fast Semantics
+
+By default, `AsyncScope` uses a _fail-fast_ policy: when any child task throws an
+exception, all sibling tasks are immediately cancelled. The first failure becomes the
+primary exception; subsequent failures are attached as
+{jdk}/java.base/java/lang/Throwable.html#addSuppressed(java.lang.Throwable)[suppressed exceptions].
+
+This prevents healthy tasks from continuing to consume resources after a sibling has
+already doomed the overall operation. Combined with `async` / `await`, failures in any
+child propagate cleanly through the scope:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_scope_fail_fast,indent=0]
+----
+
+When `failingTask` throws, the scope cancels `slowTask` immediately — there is no need
+to wait 5 seconds for it to finish or to add manual cancellation logic.
+
+=== Fan-Out / Fan-In
+
+A common concurrency pattern is _fan-out / fan-in_: launch a dynamic number of tasks
+in parallel and collect all results. `AsyncScope` makes this safe — every task is bound
+to the scope, so even if one fails, all siblings are cancelled before the scope exits:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_scope_fanout,indent=0]
+----
+
+The `collect` call inside `withScope` creates one scoped child task per URL. All pages
+are fetched concurrently, and if any single fetch fails, the remaining fetches are
+cancelled automatically.
+
+=== Manual Scope Management
+
+For advanced use cases — such as scopes that span multiple method calls or integration
+with resource managers — you can create and manage scopes directly. `AsyncScope`
+implements `AutoCloseable`, so it works with Groovy's resource-management idioms:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_scope_manual,indent=0]
+----
+
+[IMPORTANT]
+====
+Always close a scope. An unclosed scope leaves child tasks in an indeterminate state
+and may leak threads.
+====
+
+[[async-context]]
+== Async Execution Context with `AsyncContext`
+
+Asynchronous code frequently needs more than just a return value: it must also
+carry request IDs, tenant IDs, security information, locale overrides, or
+trace metadata through thread hops. `AsyncContext` provides a small, explicit,
+async-local map for exactly that purpose.
+
+Groovy captures the current `AsyncContext` when a task or continuation is
+scheduled and restores that snapshot when it runs. This gives you the most
+useful production invariant:
+
+* child tasks inherit the parent context as it existed at spawn time;
+* child-side mutations do not leak back into the parent;
+* continuations (`.then()`, `.exceptionally()`, `.handle()`, etc.) see the
+ context that was active when they were registered.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_context_basic,indent=0]
+----
+
+=== Snapshots and Manual Restoration
+
+`AsyncContext.capture()` returns an immutable `Snapshot` of the current context.
+You can restore that snapshot later—even from a different thread—using
+`AsyncContext.withSnapshot(snapshot) { ... }`. Snapshots are lightweight: they
+are simply frozen copies of the context map at capture time.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_context_snapshot,indent=0]
+----
+
+[TIP]
+====
+Use `AsyncContext` for small pieces of execution metadata — correlation IDs,
+tenant IDs, principals, feature flags. Keep large mutable business objects in
+ordinary method parameters or return values.
+====
+
+[NOTE]
+====
+`AsyncContext` is backed by a `ThreadLocal`. In pooled-thread environments the
+context is automatically captured and restored by the async runtime, but if you
+create your own threads you must capture a snapshot beforehand and restore it
+manually via `AsyncContext.withSnapshot(snapshot) { ... }`.
+====
+
+[[go-tasks-and-channels]]
+== Go-Style Tasks and Channels
+
+Groovy's core async model is based on `Awaitable`, but it also provides
+Go-inspired coordination primitives for workflows that benefit from explicit
+message passing.
+
+=== Spawning Tasks with `go`
+
+`Awaitable.go { ... }` starts a lightweight task. Outside an `AsyncScope` it
+behaves like a detached async job. Inside an `AsyncScope`, it automatically
+joins the current scope so the task is awaited and cancelled with its siblings.
+
+=== Channels
+
+`Awaitable.channel()` creates an unbuffered rendezvous channel; `Awaitable.channel(n)`
+creates a buffered channel with capacity `n`. Both `send()` and `receive()`
+return `Awaitable` values, which means they compose naturally with `await` and
+with `Awaitable.any(...)` for first-completion-wins logic.
+
+Key channel properties:
+
+* **Unbuffered (capacity 0)**: `send()` suspends until a matching `receive()`
+ arrives, providing direct handoff rendezvous semantics.
+* **Buffered (capacity > 0)**: `send()` completes immediately if buffer space is
+ available; otherwise it suspends until a consumer drains an element.
+* **Close semantics**: closing a channel is idempotent. Buffered values remain
+ receivable after close. Once drained, further receives fail with
+ `ChannelClosedException`. Pending senders fail immediately on close.
+* **Null rejection**: channels reject `null` payloads to maintain a clean
+ distinction between "no value yet" and actual data.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=go_channel_select,indent=0]
+----
+
+=== Racing Channel Operations
+
+Use `Awaitable.any(...)` to race channel receives against timeouts or other
+channels — the first completed source wins:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=channel_racing,indent=0]
+----
+
+Because `Awaitable.any` accepts any awaitable source, it works seamlessly with
+channels, timers, and arbitrary async tasks — serving a similar role to Go's
+`select` statement.
+
+=== Iterating Channels with `for await`
+
+Because `Channel` integrates with the <<for_await_section,`for await`>> syntax,
+you can iterate received values in a natural loop — the Groovy equivalent of Go's
+`for range ch`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_channel,indent=0]
+----
+
+Internally, `for await` obtains a read-only `AsyncStream` view from the channel
+(via `Channel.asStream()`). Each iteration calls `receive()`; when the channel is
+closed and all buffered values are drained, the stream signals completion and the
+loop exits.
+
+[IMPORTANT]
+====
+The stream view does **not** own the channel. Exiting the loop — whether
+normally, via `break`, or on exception — does _not_ close the channel. The
+producer is responsible for closing the channel when it has finished sending.
+This matches Go's convention that only the _sender_ closes a channel.
+====
+
+=== Fan-In Pipelines
+
+Channels are particularly effective for fan-in and fan-out pipelines where
+multiple producers feed a shared channel and one or more consumers drain it:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=channel_fan_in,indent=0]
+----
+
+This pattern is useful for work distribution, aggregating results from
+parallel workers, and building actor-like coordination without explicit locks.
+
+=== Channels vs `Flow.Publisher`
+
+Channels and `Flow.Publisher` both carry asynchronous data, and both work with
+<<for_await_section,`for await`>>. However, they serve **different purposes** and
+should _not_ be confused or substituted for each other:
+
+[cols="1,2,2"]
+|===
+| Aspect | Channel | `Flow.Publisher`
+
+| **Model**
+| Bidirectional send/receive (CSP)
+| Unidirectional push (Reactive Streams)
+
+| **Backpressure**
+| Implicit — sender waits when the buffer is full
+| Explicit — subscriber signals demand via `request(n)`
+
+| **Direction**
+| Symmetric — both sides actively write and read
+| Asymmetric — publisher pushes, subscriber pulls demand
+
+| **Close semantics**
+| Idempotent; buffered values survive close and drain first
+| Terminal `onComplete`/`onError` signals after all items
+
+| **Multi-consumer**
+| Competing — each `receive()` is exclusive (only one consumer gets the value)
+| Copied — each subscriber gets its own stream of items
+
+| **Best for**
+| Task-to-task coordination, fan-in/fan-out pipelines
+| Consuming reactive streams, HTTP bodies, database cursors
+|===
+
+Despite these differences, **both are consumed through the same `for await` syntax**
+via the unified `AsyncStream` abstraction:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=channel_vs_flow_for_await,indent=0]
+----
+
+TIP: Use **channels** when you control both the producer and consumer and need
+explicit coordination (e.g., work queues, pipelines, actor-style messaging).
+Use **`Flow.Publisher`** when you receive a push-based data stream from an
+external source (e.g., a reactive HTTP client, a message broker, or a database
+cursor).
+
+[[performance-notes]]
+== Performance Characteristics
+
+=== Adapter Registry: Amortized O(1) Lookups
+
+Every `await` expression resolves the awaited object's type to a matching
+`AwaitableAdapter` via the `AwaitableAdapterRegistry`. The registry uses
+{jdk}/java.base/java/lang/ClassValue.html[`ClassValue`]-based per-class memoization,
+reducing adapter resolution from O(n) (linear scan of registered adapters) to amortized
+O(1) after the first lookup for each class. The cache is automatically invalidated when
+adapters are registered or unregistered at runtime.
+
+=== Synchronous Completion Fast-Path
+
+When an `Awaitable` wraps a `CompletableFuture` that has already completed by the time
+`get()` is called, the runtime extracts the result via `CompletableFuture.join()` instead
+of the full interruptible `get()` path. This bypasses the park/unpark machinery and
+eliminates unnecessary thread state transitions — a measurable win when async operations
+complete before being awaited, which is common for cached or pre-computed results.
+
+This optimisation is analogous to C#'s
+https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1[`ValueTask<T>`]
+synchronous completion path, where already-completed tasks skip the state machine
+continuation entirely.
+
+=== Virtual Threads (JDK 21+)
+
+On JDK 21+, Groovy automatically uses
+{jdk}/java.base/java/lang/Thread.html#ofVirtual()[virtual threads] for async execution.
+Virtual threads are extremely lightweight — a few hundred bytes of stack compared to
+~1 MB for platform threads — and are scheduled by the JVM's M:N scheduler onto a small
+pool of carrier threads. The `await` keyword blocks the virtual thread (not the carrier),
+so the JVM can multiplex hundreds of thousands of concurrent async operations onto a
+handful of OS threads.
+
+On JDK 17–20, the runtime falls back to a bounded cached thread pool (default
+256 threads, configurable via the `groovy.async.parallelism` system property) with a
+caller-runs back-pressure policy.
+
+[[summary]]
+== Summary
+
+[cols="2,5"]
+|===
+| Feature | Syntax
+
+| Async method
+| `async returnType methodName(params) { ... }`
+
+| Async closure
+| `def fn = async { params -> body }; await(fn(args))`
+
+| Async lambda
+| `def fn = async (params) -> { body }; await(fn(args))`
+
+| Await expression
+| `await expr` or `await(expr)`
+
+| For await
+| `for await (item in source) { ... }` or `for await (item : source) { ... }`
+
+| Yield return
+| `yield return expr` (inside async methods)
+
+| Defer
+| `defer { cleanup code }`
+
+| Parallel wait
+| `await Awaitable.all(a, b, c)`
+
+| Race
+| `await Awaitable.any(a, b)`
+
+| All settled
+| `await Awaitable.allSettled(a, b, c)`
+
+| Delay
+| `Awaitable.delay(millis)`
+
+| Pre-computed result
+| `Awaitable.of(value)` / `Awaitable.failed(error)`
+
+| Type conversion
+| `Awaitable.from(source)` / `AsyncStream.from(source)`
+
+| Annotation form
+| `@Async def methodName() { ... }`
+
+| Annotation with executor
+| `@Async(executor = "myPool") def methodName() { ... }`
+
+| Inspection
+| `awaitable.isDone()` / `isCancelled()` / `isCompletedExceptionally()`
+
+| Timeout (instance)
+| `awaitable.orTimeoutMillis(ms)` / `awaitable.completeOnTimeoutMillis(fallback, ms)`
+
+| AwaitResult
+| `result.isSuccess()` / `result.isFailure()` / `result.getOrElse { fallback }`
+
+| Structured concurrency
+| `AsyncScope.withScope { scope -> scope.async { await task() } }`
+|===
diff --git a/src/spec/test/AsyncAwaitSpecTest.groovy b/src/spec/test/AsyncAwaitSpecTest.groovy
new file mode 100644
index 0000000000..0e17056efd
--- /dev/null
+++ b/src/spec/test/AsyncAwaitSpecTest.groovy
@@ -0,0 +1,1828 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * Spec tests for async/await documentation.
+ * <p>
+ * Tagged code snippets in this file are referenced by {@code core-async-await.adoc}
+ * via AsciiDoc {@code include::} directives.
+ */
+class AsyncAwaitSpecTest {
+
+ // =========================================================================
+ // 1. Basic async methods
+ // =========================================================================
+
+ @Test
+ void testAsyncMethodBasic() {
+ assertScript '''
+// tag::async_method_basic[]
+import groovy.concurrent.Awaitable
+
+class GreetingService {
+ async greet(String name) {
+ return "Hello, ${name}!"
+ }
+}
+
+def service = new GreetingService()
+def awaitable = service.greet("World")
+assert awaitable instanceof Awaitable
+assert awaitable.get() == "Hello, World!"
+// end::async_method_basic[]
+ '''
+ }
+
+ @Test
+ void testAsyncMethodTyped() {
+ assertScript '''
+// tag::async_method_typed[]
+class MathService {
+ async int square(int n) { n * n }
+ async String upper(String s) { s.toUpperCase() }
+}
+
+def svc = new MathService()
+assert svc.square(7).get() == 49
+assert svc.upper("groovy").get() == "GROOVY"
+// end::async_method_typed[]
+ '''
+ }
+
+ @Test
+ void testAsyncMethodVoid() {
+ assertScript '''
+// tag::async_method_void[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.atomic.AtomicReference
+
+class Logger {
+ AtomicReference<String> lastMessage = new AtomicReference<>()
+
+ async void log(String msg) {
+ lastMessage.set(msg)
+ }
+}
+
+def logger = new Logger()
+def a = logger.log("event occurred")
+assert a instanceof Awaitable
+a.get()
+assert logger.lastMessage.get() == "event occurred"
+// end::async_method_void[]
+ '''
+ }
+
+ @Test
+ void testAsyncMethodStatic() {
+ assertScript '''
+// tag::async_method_static[]
+class Util {
+ async static int add(int a, int b) { a + b }
+}
+
+assert Util.add(3, 4).get() == 7
+// end::async_method_static[]
+ '''
+ }
+
+ @Test
+ void testAsyncScript() {
+ assertScript '''
+// tag::async_script[]
+async multiply(int a, int b) { a * b }
+
+def result = multiply(6, 7)
+assert result.get() == 42
+// end::async_script[]
+ '''
+ }
+
+ // =========================================================================
+ // 2. await expression
+ // =========================================================================
+
+ @Test
+ void testAwaitBasic() {
+ assertScript '''
+// tag::await_basic[]
+class DataService {
+ async fetchData() { "database result" }
+
+ async process() {
+ def data = await fetchData()
+ return "Processed: ${data}"
+ }
+}
+
+def result = await(new DataService().process())
+assert result == "Processed: database result"
+// end::await_basic[]
+ '''
+ }
+
+ @Test
+ void testAwaitCompletableFuture() {
+ assertScript '''
+// tag::await_completable_future[]
+import java.util.concurrent.CompletableFuture
+
+class Service {
+ async compute() {
+ def value = await CompletableFuture.supplyAsync { 42 }
+ return value * 2
+ }
+}
+
+assert await(new Service().compute()) == 84
+// end::await_completable_future[]
+ '''
+ }
+
+ @Test
+ void testAwaitArithmetic() {
+ assertScript '''
+// tag::await_arithmetic[]
+import java.util.concurrent.CompletableFuture
+
+class Calculator {
+ async sum() {
+ def a = await(CompletableFuture.supplyAsync { 10 })
+ def b = await(CompletableFuture.supplyAsync { 20 })
+ return a + b
+ }
+}
+
+assert await(new Calculator().sum()) == 30
+// end::await_arithmetic[]
+ '''
+ }
+
+ @Test
+ void testAwaitParenthesized() {
+ assertScript '''
+// tag::await_parenthesized[]
+import java.util.concurrent.CompletableFuture
+
+async compute() {
+ // Both forms are equivalent:
+ def a = await CompletableFuture.supplyAsync { 10 }
+ def b = await(CompletableFuture.supplyAsync { 20 })
+ return a + b
+}
+
+assert await(compute()) == 30
+// end::await_parenthesized[]
+ '''
+ }
+
+ // =========================================================================
+ // 3. Async closures and lambdas
+ // =========================================================================
+
+ @Test
+ void testAsyncClosureBasic() {
+ assertScript '''
+// tag::async_closure_basic[]
+import groovy.concurrent.Awaitable
+
+// async { ... } creates a Closure that returns an Awaitable
+def asyncTask = async { 1 + 2 }
+assert asyncTask instanceof Closure
+
+// Explicit invocation is required to start execution
+def awaitable = asyncTask()
+assert awaitable instanceof Awaitable
+assert await(awaitable) == 3
+// end::async_closure_basic[]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithParams() {
+ assertScript '''
+// tag::async_closure_params[]
+def asyncSquare = async { int n -> n * n }
+
+// Call with argument, then await the result
+assert await(asyncSquare(5)) == 25
+assert await(asyncSquare(7)) == 49
+// end::async_closure_params[]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureMultipleParams() {
+ assertScript '''
+// tag::async_closure_multi_params[]
+def asyncConcat = async { a, b, sep -> "${a}${sep}${b}" }
+assert await(asyncConcat("hello", "world", " ")) == "hello world"
+// end::async_closure_multi_params[]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureInCollection() {
+ assertScript '''
+// tag::async_closure_collection[]
+def ops = [
+ add: async { a, b -> a + b },
+ mul: async { a, b -> a * b },
+ sub: async { a, b -> a - b }
+]
+
+assert await(ops.add(3, 4)) == 7
+assert await(ops.mul(3, 4)) == 12
+assert await(ops.sub(10, 4)) == 6
+// end::async_closure_collection[]
+ '''
+ }
+
+ @Test
+ void testAsyncLambda() {
+ assertScript '''
+// tag::async_lambda[]
+def asyncDouble = async (n) -> { n * 2 }
+assert await(asyncDouble(21)) == 42
+
+def asyncGreet = async (name) -> { "Hello, ${name}!" }
+assert await(asyncGreet("Groovy")) == "Hello, Groovy!"
+// end::async_lambda[]
+ '''
+ }
+
+ // =========================================================================
+ // 4. for await — async iteration
+ // =========================================================================
+
+ @Test
+ void testForAwaitBasic() {
+ assertScript '''
+// tag::for_await_basic[]
+class Collector {
+ async collectItems() {
+ def results = []
+ for await (item in [10, 20, 30]) {
+ results << item
+ }
+ return results
+ }
+}
+
+assert await(new Collector().collectItems()) == [10, 20, 30]
+// end::for_await_basic[]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithTransform() {
+ assertScript '''
+// tag::for_await_transform[]
+class Transformer {
+ async doubleAll() {
+ def results = []
+ for await (item in [1, 2, 3, 4]) {
+ results << item * 2
+ }
+ return results
+ }
+}
+
+assert await(new Transformer().doubleAll()) == [2, 4, 6, 8]
+// end::for_await_transform[]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithBreak() {
+ assertScript '''
+// tag::for_await_break[]
+class Finder {
+ async findFirstOver(int threshold) {
+ for await (item in [5, 10, 15, 20, 25]) {
+ if (item > threshold) return item
+ }
+ return -1
+ }
+}
+
+assert await(new Finder().findFirstOver(12)) == 15
+// end::for_await_break[]
+ '''
+ }
+
+ // =========================================================================
+ // 5. yield return — async generators
+ // =========================================================================
+
+ @Test
+ void testYieldReturnBasic() {
+ assertScript '''
+// tag::yield_return_basic[]
+import groovy.concurrent.AsyncStream
+
+class NumberGenerator {
+ async numbers() {
+ yield return 1
+ yield return 2
+ yield return 3
+ }
+}
+
+def stream = new NumberGenerator().numbers()
+assert stream instanceof AsyncStream
+
+def results = []
+for await (n in stream) {
+ results << n
+}
+assert results == [1, 2, 3]
+// end::yield_return_basic[]
+ '''
+ }
+
+ @Test
+ void testYieldReturnLoop() {
+ assertScript '''
+// tag::yield_return_loop[]
+class RangeGenerator {
+ async range(int start, int end) {
+ for (int i = start; i <= end; i++) {
+ yield return i
+ }
+ }
+}
+
+def results = []
+for await (n in new RangeGenerator().range(1, 5)) {
+ results << n
+}
+assert results == [1, 2, 3, 4, 5]
+// end::yield_return_loop[]
+ '''
+ }
+
+ @Test
+ void testYieldReturnFilter() {
+ assertScript '''
+// tag::yield_return_filter[]
+class FilteredGenerator {
+ async evenNumbers(int max) {
+ for (int i = 1; i <= max; i++) {
+ if (i % 2 == 0) {
+ yield return i
+ }
+ }
+ }
+}
+
+def results = []
+for await (n in new FilteredGenerator().evenNumbers(10)) {
+ results << n
+}
+assert results == [2, 4, 6, 8, 10]
+// end::yield_return_filter[]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithAwait() {
+ assertScript '''
+// tag::yield_return_with_await[]
+import java.util.concurrent.CompletableFuture
+
+class AsyncDataGenerator {
+ async fetchSequence() {
+ for (int i = 1; i <= 3; i++) {
+ def value = await CompletableFuture.supplyAsync { i * 10 }
+ yield return value
+ }
+ }
+}
+
+def results = []
+for await (v in new AsyncDataGenerator().fetchSequence()) {
+ results << v
+}
+assert results == [10, 20, 30]
+// end::yield_return_with_await[]
+ '''
+ }
+
+ // =========================================================================
+ // 6. defer — Go-style cleanup
+ // =========================================================================
+
+ @Test
+ void testDeferBasic() {
+ assertScript '''
+// tag::defer_basic[]
+class DeferExample {
+ async runWithDefer() {
+ def log = []
+ defer { log << "first deferred" }
+ defer { log << "second deferred" }
+ defer { log << "third deferred" }
+ log << "body executed"
+ return log
+ }
+}
+
+def result = await(new DeferExample().runWithDefer())
+// Body runs first; deferred blocks execute in LIFO order
+assert result == ["body executed", "third deferred", "second deferred", "first deferred"]
+// end::defer_basic[]
+ '''
+ }
+
+ @Test
+ void testDeferOnException() {
+ assertScript '''
+// tag::defer_exception[]
+class ResourceHandler {
+ static log = []
+
+ async processWithCleanup() {
+ defer { log << "cleanup done" }
+ throw new RuntimeException("something went wrong")
+ }
+}
+
+try {
+ await(new ResourceHandler().processWithCleanup())
+} catch (RuntimeException e) {
+ assert e.message == "something went wrong"
+}
+// Deferred blocks always execute, even on exception
+assert ResourceHandler.log == ["cleanup done"]
+// end::defer_exception[]
+ '''
+ }
+
+ @Test
+ void testDeferResourceCleanup() {
+ assertScript '''
+// tag::defer_resource_cleanup[]
+class ResourceManager {
+ static resources = []
+ static cleanupLog = []
+
+ async processResources() {
+ def r1 = "database-conn"
+ resources << r1
+ defer { resources.remove(r1); cleanupLog << "closed ${r1}" }
+
+ def r2 = "file-handle"
+ resources << r2
+ defer { resources.remove(r2); cleanupLog << "closed ${r2}" }
+
+ assert resources.size() == 2
+ return "done"
+ }
+}
+
+assert await(new ResourceManager().processResources()) == "done"
+// LIFO order: r2 closed first, then r1
+assert ResourceManager.cleanupLog == ["closed file-handle", "closed database-conn"]
+assert ResourceManager.resources.isEmpty()
+// end::defer_resource_cleanup[]
+ '''
+ }
+
+ // =========================================================================
+ // 7. Exception handling
+ // =========================================================================
+
+ @Test
+ void testExceptionTransparency() {
+ assertScript '''
+// tag::exception_transparency[]
+async fetchData() {
+ throw new java.io.IOException("disk failure")
+}
+
+async caller() {
+ try {
+ await fetchData()
+ assert false : "should not reach here"
+ } catch (java.io.IOException e) {
+ return "Recovered: ${e.message}"
+ }
+}
+
+assert await(caller()) == "Recovered: disk failure"
+// end::exception_transparency[]
+ '''
+ }
+
+ @Test
+ void testExceptionMultipleTasks() {
+ assertScript '''
+// tag::exception_multiple_tasks[]
+async riskyTask(boolean shouldFail) {
+ if (shouldFail) throw new IllegalStateException("task failed")
+ return "success"
+}
+
+async coordinator() {
+ def results = []
+ for (flag in [false, true, false]) {
+ try {
+ results << await(riskyTask(flag))
+ } catch (IllegalStateException e) {
+ results << "error: ${e.message}"
+ }
+ }
+ return results
+}
+
+assert await(coordinator()) == ["success", "error: task failed", "success"]
+// end::exception_multiple_tasks[]
+ '''
+ }
+
+ // =========================================================================
+ // 8. Utility methods — awaitAll, awaitAny, awaitAllSettled, delay
+ // =========================================================================
+
+ @Test
+ void testAwaitAll() {
+ assertScript '''
+// tag::await_all[]
+import groovy.concurrent.Awaitable
+
+async fetchUser() { "Alice" }
+async fetchOrder() { "Order#42" }
+async fetchBalance() { 100.0 }
+
+def results = await(Awaitable.all(fetchUser(), fetchOrder(), fetchBalance()))
+assert results == ["Alice", "Order#42", 100.0]
+// end::await_all[]
+ '''
+ }
+
+ @Test
+ void testAwaitAny() {
+ assertScript '''
+// tag::await_any[]
+import groovy.concurrent.Awaitable
+
+def fast = async { "fast result" }
+def slow = async {
+ await(Awaitable.delay(2000))
+ return "slow result"
+}
+
+def winner = await(Awaitable.any(fast(), slow()))
+assert winner == "fast result"
+// end::await_any[]
+ '''
+ }
+
+ @Test
+ void testAwaitAllSettled() {
+ assertScript '''
+// tag::await_all_settled[]
+import groovy.concurrent.Awaitable
+
+async caller() {
+ def a = Awaitable.of(1)
+ def b = Awaitable.failed(new IOException("network error"))
+ def c = Awaitable.of(3)
+ return await(Awaitable.allSettled(a, b, c))
+}
+
+def results = await(caller())
+assert results.size() == 3
+
+assert results[0].isSuccess() && results[0].value == 1
+assert results[1].isFailure() && results[1].error.message == "network error"
+assert results[2].isSuccess() && results[2].value == 3
+// end::await_all_settled[]
+ '''
+ }
+
+ @Test
+ void testDelay() {
+ assertScript '''
+// tag::delay_example[]
+import groovy.concurrent.Awaitable
+
+async delayedGreeting() {
+ await(Awaitable.delay(100)) // pause for 100 milliseconds
+ return "Hello after delay"
+}
+
+assert await(delayedGreeting()) == "Hello after delay"
+// end::delay_example[]
+ '''
+ }
+
+ @Test
+ void testContinuationHelpers() {
+ assertScript '''
+// tag::continuation_helpers[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.atomic.AtomicReference
+
+def observed = new AtomicReference()
+await(Awaitable.of("groovy").thenAccept { observed.set(it.toUpperCase()) })
+assert observed.get() == "GROOVY"
+
+def recovered = await(
+ Awaitable.failed(new IOException("boom"))
+ .handle { value, error -> "recovered from ${error.message}" }
+)
+assert recovered == "recovered from boom"
+
+def completion = new AtomicReference()
+assert await(Awaitable.of(7).whenComplete { value, error ->
+ completion.set("${value}:${error}")
+}) == 7
+assert completion.get() == "7:null"
+// end::continuation_helpers[]
+ '''
+ }
+
+ @Test
+ void testTimeoutCombinators() {
+ assertScript '''
+// tag::timeout_combinators[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.TimeoutException
+
+async slowCall() {
+ await(Awaitable.delay(5_000))
+ return "done"
+}
+
+try {
+ await(Awaitable.orTimeoutMillis(slowCall(), 50))
+ assert false : "should have timed out"
+} catch (TimeoutException e) {
+ assert e.message.contains("Timed out after 50")
+}
+
+assert await(Awaitable.completeOnTimeoutMillis(slowCall(), "cached", 50)) == "cached"
+// end::timeout_combinators[]
+ '''
+ }
+
+ // =========================================================================
+ // 9. Flow.Publisher integration
+ // =========================================================================
+
+ @Test
+ void testFlowPublisherAwait() {
+ assertScript '''
+// tag::flow_publisher_await[]
+import java.util.concurrent.SubmissionPublisher
+
+def publisher = new SubmissionPublisher<String>()
+def waitForSubscriber = { SubmissionPublisher pub, long timeoutMillis = 5_000L ->
+ long deadline = System.nanoTime() + timeoutMillis * 1_000_000L
+ while (pub.numberOfSubscribers == 0 && System.nanoTime() < deadline) {
+ Thread.sleep(1)
+ }
+ assert pub.numberOfSubscribers > 0 : "Timed out waiting for publisher subscription"
+}
+Thread.start {
+ waitForSubscriber(publisher)
+ publisher.submit("hello from publisher")
+ publisher.close()
+}
+
+def result = await(publisher)
+assert result == "hello from publisher"
+// end::flow_publisher_await[]
+ '''
+ }
+
+ @Test
+ void testFlowPublisherForAwait() {
+ assertScript '''
+// tag::flow_publisher_for_await[]
+import java.util.concurrent.SubmissionPublisher
+
+class StreamConsumer {
+ async consumeAll(SubmissionPublisher<Integer> pub) {
+ def results = []
+ for await (item in pub) {
+ results << item
+ }
+ return results
+ }
+}
+
+def publisher = new SubmissionPublisher<Integer>()
+def future = new StreamConsumer().consumeAll(publisher)
+def waitForSubscriber = { SubmissionPublisher pub, long timeoutMillis = 5_000L ->
+ long deadline = System.nanoTime() + timeoutMillis * 1_000_000L
+ while (pub.numberOfSubscribers == 0 && System.nanoTime() < deadline) {
+ Thread.sleep(1)
+ }
+ assert pub.numberOfSubscribers > 0 : "Timed out waiting for publisher subscription"
+}
+Thread.start {
+ waitForSubscriber(publisher)
+ (1..5).each { publisher.submit(it) }
+ publisher.close()
+}
+
+assert await(future) == [1, 2, 3, 4, 5]
+// end::flow_publisher_for_await[]
+ '''
+ }
+
+ // =========================================================================
+ // 10. @Async annotation
+ // =========================================================================
+
+ @Test
+ void testAsyncAnnotation() {
+ assertScript '''
+// tag::async_annotation[]
+import groovy.transform.Async
+import groovy.concurrent.Awaitable
+
+class Service {
+ @Async
+ def fetchData() {
+ return "data loaded"
+ }
+}
+
+def svc = new Service()
+def awaitable = svc.fetchData()
+assert awaitable instanceof Awaitable
+assert awaitable.get() == "data loaded"
+// end::async_annotation[]
+ '''
+ }
+
+ @Test
+ void testAsyncAnnotationWithExecutor() {
+ assertScript '''
+// tag::async_annotation_executor[]
+import groovy.transform.Async
+import groovy.concurrent.Awaitable
+import java.util.concurrent.Executors
+
+class DataService {
+ private final myPool = Executors.newFixedThreadPool(4)
+
+ @Async(executor = "myPool")
+ def fetchData() {
+ return Thread.currentThread().name
+ }
+}
+
+def svc = new DataService()
+def name = svc.fetchData().get()
+assert name.startsWith('pool-')
+// end::async_annotation_executor[]
+ '''
+ }
+
+ @Test
+ void testChannelRacingPattern() {
+ assertScript '''
+// tag::channel_racing[]
+import groovy.concurrent.Awaitable
+
+async def racingExample() {
+ def ch = Awaitable.channel(1)
+
+ Awaitable.go {
+ await Awaitable.delay(50)
+ await ch.send('data arrived')
+ }
+
+ def winner = await Awaitable.any(
+ ch.receive(), // channel receive
+ Awaitable.delay(1000).then { 'timeout' } // timeout fallback
+ )
+
+ assert winner == 'data arrived'
+ winner
+}
+
+assert racingExample().get() == 'data arrived'
+// end::channel_racing[]
+ '''
+ }
+
+ // =========================================================================
+ // 11. Custom executor
+ // =========================================================================
+
+ @Test
+ void testCustomExecutor() {
+ assertScript '''
+// tag::custom_executor[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.Executors
+
+class WorkService {
+ async work() { Thread.currentThread().name }
+}
+
+def customPool = Executors.newFixedThreadPool(2)
+try {
+ Awaitable.setExecutor(customPool)
+
+ def threadName = await(new WorkService().work())
+ assert threadName.contains("pool")
+} finally {
+ Awaitable.setExecutor(null) // restore default
+ customPool.shutdown()
+}
+// end::custom_executor[]
+ '''
+ }
+
+ // =========================================================================
+ // 12. Comprehensive pattern — parallel web scraping
+ // =========================================================================
+
+ @Test
+ void testParallelPattern() {
+ assertScript '''
+// tag::parallel_pattern[]
+import groovy.concurrent.Awaitable
+
+async fetchPage(String url) {
+ await(Awaitable.delay(10)) // simulate network I/O
+ return "Content of ${url}"
+}
+
+// Launch three tasks in parallel
+def pages = await(Awaitable.all(
+ fetchPage("https://example.com/page1"),
+ fetchPage("https://example.com/page2"),
+ fetchPage("https://example.com/page3")
+))
+
+assert pages.size() == 3
+assert pages.every { it.startsWith("Content of") }
+// end::parallel_pattern[]
+ '''
+ }
+
+ // =========================================================================
+ // 13. Common patterns — retry, timeout, cancellation
+ // =========================================================================
+
+ @Test
+ void testRetryPattern() {
+ assertScript '''
+// tag::retry_pattern[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.atomic.AtomicInteger
+
+class RetryDemo {
+ final AtomicInteger attempts = new AtomicInteger(0)
+
+ async fetchData() {
+ if (attempts.incrementAndGet() < 3) throw new IOException("transient error")
+ return "data"
+ }
+
+ async fetchWithRetry(int maxRetries) {
+ for (int attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ return await fetchData()
+ } catch (Exception e) {
+ if (attempt == maxRetries) throw e
+ await(Awaitable.delay(10 * attempt)) // exponential backoff
+ }
+ }
+ }
+}
+
+def demo = new RetryDemo()
+assert await(demo.fetchWithRetry(5)) == "data"
+assert demo.attempts.get() == 3
+// end::retry_pattern[]
+ '''
+ }
+
+ @Test
+ void testTimeoutPattern() {
+ assertScript '''
+// tag::timeout_pattern[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.TimeoutException
+
+async longRunningTask() {
+ await(Awaitable.delay(5000))
+ return "completed"
+}
+
+try {
+ await Awaitable.orTimeoutMillis(longRunningTask(), 50)
+ assert false : "should have timed out"
+} catch (TimeoutException e) {
+ assert e.message.contains("Timed out after 50")
+}
+// end::timeout_pattern[]
+ '''
+ }
+
+ @Test
+ void testCancellation() {
+ assertScript '''
+// tag::cancellation[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.CancellationException
+
+async longRunningTask() {
+ await(Awaitable.delay(10_000))
+ return "done"
+}
+
+def task = longRunningTask()
+task.cancel()
+
+try {
+ await(task)
+ assert false : "should have thrown"
+} catch (CancellationException e) {
+ assert task.isCancelled()
+}
+// end::cancellation[]
+ '''
+ }
+
+ // =========================================================================
+ // 14. Best practices — Awaitable.of, async caching, static factory
+ // =========================================================================
+
+ @Test
+ void testAwaitableOf() {
+ assertScript '''
+// tag::awaitable_of[]
+import groovy.concurrent.Awaitable
+
+// No thread spawned — result is immediately available
+Awaitable<Integer> addAsync(int a, int b) {
+ return Awaitable.of(a + b)
+}
+
+assert await(addAsync(2, 3)) == 5
+
+// Pre-computed error result
+Awaitable<String> failFast() {
+ return Awaitable.failed(new IllegalStateException("not ready"))
+}
+
+try {
+ await failFast()
+ assert false
+} catch (IllegalStateException e) {
+ assert e.message == "not ready"
+}
+// end::awaitable_of[]
+ '''
+ }
+
+ @Test
+ void testAsyncCaching() {
+ assertScript '''
+// tag::async_caching[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicInteger
+
+class CacheDemo {
+ final AtomicInteger fetchCount = new AtomicInteger(0)
+ final cache = new ConcurrentHashMap<Integer, Awaitable>()
+
+ async fetchPersonAsync(int id) {
+ fetchCount.incrementAndGet()
+ await(Awaitable.delay(10))
+ return "Person-${id}"
+ }
+
+ def getOrFetch(int id) {
+ cache.computeIfAbsent(id, { key -> fetchPersonAsync(key) })
+ }
+}
+
+def demo = new CacheDemo()
+
+// First call triggers actual fetch
+def p1 = await(demo.getOrFetch(42))
+// Second call returns cached Awaitable (no new fetch)
+def p2 = await(demo.getOrFetch(42))
+
+assert p1 == "Person-42"
+assert p2 == "Person-42"
+assert demo.fetchCount.get() == 1 // fetched only once
+// end::async_caching[]
+ '''
+ }
+
+ @Test
+ void testStaticFactoryPattern() {
+ assertScript '''
+// tag::static_factory[]
+import groovy.concurrent.Awaitable
+
+class RemoteService {
+ final String connectionId
+ private RemoteService(String connectionId) { this.connectionId = connectionId }
+
+ static async create() {
+ def connId = await connectToRemote()
+ return new RemoteService(connId)
+ }
+
+ private static async connectToRemote() {
+ await(Awaitable.delay(10)) // simulate I/O
+ return "conn-" + System.nanoTime()
+ }
+}
+
+def service = await(RemoteService.create())
+assert service.connectionId.startsWith("conn-")
+// end::static_factory[]
+ '''
+ }
+
+ // =========================================================================
+ // 15. Best practices — sync-over-async, prefer await, fire-and-forget
+ // =========================================================================
+
+ @Test
+ void testSyncOverAsync() {
+ assertScript '''
+// tag::sync_over_async[]
+import groovy.concurrent.Awaitable
+
+class ConsumerService {
+ // BAD: blocking sync-over-async
+ // def result = computeAsync(5).get()
+
+ // GOOD: propagate await through the call chain
+ async computeAsync(int n) {
+ await(Awaitable.delay(10))
+ return n * 2
+ }
+
+ async doSomething() {
+ def result = await computeAsync(5)
+ return result + 1
+ }
+}
+
+def svc = new ConsumerService()
+assert await(svc.doSomething()) == 11
+// end::sync_over_async[]
+ '''
+ }
+
+ @Test
+ void testPreferAwaitOverThen() {
+ assertScript '''
+// tag::prefer_await_over_then[]
+import groovy.concurrent.Awaitable
+
+class PipelineDemo {
+ async step1() { await(Awaitable.delay(5)); return 10 }
+ async step2(int n) { await(Awaitable.delay(5)); return n * 2 }
+ async step3(int n) { await(Awaitable.delay(5)); return n + 3 }
+
+ // More readable: sequential awaits
+ async pipeline() {
+ def a = await step1()
+ def b = await step2(a)
+ return await step3(b)
+ }
+}
+
+def demo = new PipelineDemo()
+assert await(demo.pipeline()) == 23
+// end::prefer_await_over_then[]
+ '''
+ }
+
+ @Test
+ void testFireAndForget() {
+ assertScript '''
+// tag::fire_and_forget[]
+import groovy.concurrent.Awaitable
+
+def errors = Collections.synchronizedList([])
+
+async backgroundOperation() {
+ await(Awaitable.delay(10))
+ throw new RuntimeException("background error")
+}
+
+// GOOD: capture for tracking
+def task = backgroundOperation()
+Thread.sleep(100) // wait for completion
+assert task.isCompletedExceptionally()
+// end::fire_and_forget[]
+ '''
+ }
+
+ @Test
+ void testForAwaitColonNotation() {
+ assertScript '''
+// tag::for_await_colon[]
+class ColonNotationDemo {
+ async generateNumbers() {
+ for (int i = 1; i <= 3; i++) {
+ yield return i
+ }
+ }
+}
+
+def demo = new ColonNotationDemo()
+def results = []
+// for await supports both `in` and `:` notation
+for await (n : demo.generateNumbers()) {
+ results << n
+}
+assert results == [1, 2, 3]
+// end::for_await_colon[]
+ '''
+ }
+
+ // =========================================================================
+ // 16. AwaitResult API
+ // =========================================================================
+
+ @Test
+ void testAwaitResultApi() {
+ assertScript '''
+// tag::await_result_api[]
+import groovy.concurrent.Awaitable
+import groovy.concurrent.AwaitResult
+
+async caller() {
+ def a = Awaitable.of(42)
+ def b = Awaitable.failed(new IOException("disk full"))
+ return await(Awaitable.allSettled(a, b))
+}
+
+def results = await(caller())
+
+// Inspect each outcome
+AwaitResult success = results[0]
+assert success.isSuccess()
+assert success.value == 42
+assert !success.isFailure()
+
+AwaitResult failure = results[1]
+assert failure.isFailure()
+assert failure.error.message == "disk full"
+assert !failure.isSuccess()
+
+// getOrElse: recover from failures with a fallback function
+assert success.getOrElse { -1 } == 42
+assert failure.getOrElse { err -> "recovered: ${err.message}" } == "recovered: disk full"
+
+// toString for debugging
+assert success.toString() == "AwaitResult.Success[42]"
+assert failure.toString().startsWith("AwaitResult.Failure[")
+// end::await_result_api[]
+ '''
+ }
+
+ // =========================================================================
+ // 17. Instance timeout methods
+ // =========================================================================
+
+ @Test
+ void testInstanceTimeoutMethods() {
+ assertScript '''
+// tag::instance_timeout_methods[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.TimeoutException
+
+async slowTask() {
+ await(Awaitable.delay(5_000))
+ return "done"
+}
+
+// orTimeout: fails with TimeoutException if not completed in time
+try {
+ await(slowTask().orTimeoutMillis(50))
+ assert false : "should have timed out"
+} catch (TimeoutException e) {
+ assert e.message.contains("50")
+}
+
+// completeOnTimeout: completes with a fallback value instead of failing
+assert await(slowTask().completeOnTimeoutMillis("fallback", 50)) == "fallback"
+// end::instance_timeout_methods[]
+ '''
+ }
+
+ // =========================================================================
+ // 18. Inspection methods
+ // =========================================================================
+
+ @Test
+ void testInspectionMethods() {
+ assertScript '''
+// tag::inspection_methods[]
+import groovy.concurrent.Awaitable
+
+// Check state without blocking
+def completed = Awaitable.of(42)
+assert completed.isDone()
+assert !completed.isCancelled()
+
+def failed = Awaitable.failed(new RuntimeException("oops"))
+assert failed.isDone()
+assert failed.isCompletedExceptionally()
+
+// Cancel an awaitable
+def task = Awaitable.delay(10_000)
+task.cancel()
+assert task.isCancelled()
+assert task.isDone()
+// end::inspection_methods[]
+ '''
+ }
+
+ // =========================================================================
+ // 19. Type conversion with from()
+ // =========================================================================
+
+ @Test
+ void testFromConversion() {
+ assertScript '''
+// tag::from_conversion[]
+import groovy.concurrent.Awaitable
+import groovy.concurrent.AsyncStream
+import java.util.concurrent.CompletableFuture
+
+// Convert a CompletableFuture to Awaitable
+def cf = CompletableFuture.completedFuture("hello")
+Awaitable<String> awaitable = Awaitable.from(cf)
+assert awaitable.get() == "hello"
+
+// If the source is already an Awaitable, it is returned as-is
+def original = Awaitable.of(42)
+assert Awaitable.from(original).is(original)
+
+// Convert an Iterable to AsyncStream
+AsyncStream<String> stream = AsyncStream.from(["a", "b", "c"])
+def items = []
+assert stream.moveNext().get() == true
+items << stream.current
+assert stream.moveNext().get() == true
+items << stream.current
+assert stream.moveNext().get() == true
+items << stream.current
+assert stream.moveNext().get() == false
+assert items == ["a", "b", "c"]
+// end::from_conversion[]
+ '''
+ }
+
+ // =========================================================================
+ // 20. Custom adapter registration
+ // =========================================================================
+
+ @Test
+ void testAdapterRegistration() {
+ assertScript '''
+// tag::adapter_registration[]
+import groovy.concurrent.AwaitableAdapterRegistry
+import groovy.concurrent.AwaitableAdapter
+import groovy.concurrent.Awaitable
+
+// A simple custom async type (simulating a third-party framework)
+class CustomFuture<T> {
+ private final T value
+ CustomFuture(T value) { this.value = value }
+ T blockingGet() { return value }
+}
+
+// Register adapter for the custom type
+def handle = AwaitableAdapterRegistry.register(new AwaitableAdapter() {
+ boolean supportsAwaitable(Class<?> type) { CustomFuture.isAssignableFrom(type) }
+ Awaitable toAwaitable(Object source) { Awaitable.of(((CustomFuture) source).blockingGet()) }
+})
+
+try {
+ def future = new CustomFuture<String>("hello from adapter")
+ assert await(future) == "hello from adapter"
+} finally {
+ handle.close() // deregister when done
+}
+// end::adapter_registration[]
+ '''
+ }
+
+ // =========================================================================
+ // Motivation: Why Groovy Needs Async/Await
+ // =========================================================================
+
+ @Test
+ void testMotivationCallbackHell() {
+ assertScript '''
+// tag::motivation_callback_hell[]
+import java.util.concurrent.CompletableFuture
+
+// ------ Before: chained CompletableFuture (callback style) ------
+def result = CompletableFuture.supplyAsync { 42 }
+ .thenCompose { userId ->
+ CompletableFuture.supplyAsync { "User-${userId}" }
+ }
+ .thenCompose { userName ->
+ CompletableFuture.supplyAsync { "${userName}: premium" }
+ }
+ .thenApply { profile ->
+ profile.toUpperCase()
+ }
+ .exceptionally { ex ->
+ "ERROR: ${ex.message}"
+ }
+ .get()
+
+assert result == "USER-42: PREMIUM"
+// end::motivation_callback_hell[]
+ '''
+ }
+
+ @Test
+ void testMotivationAsyncAwaitClean() {
+ assertScript '''
+// tag::motivation_async_await_clean[]
+import groovy.concurrent.Awaitable
+
+// ------ After: sequential async/await (same logic, flat and readable) ------
+async def fetchProfile(int userId) {
+ def name = await Awaitable.of("User-${userId}")
+ def profile = await Awaitable.of("${name}: premium")
+ return profile.toUpperCase()
+}
+
+assert await(fetchProfile(42)) == "USER-42: PREMIUM"
+// end::motivation_async_await_clean[]
+ '''
+ }
+
+ @Test
+ void testMotivationExceptionHandling() {
+ assertScript '''
+// tag::motivation_exception_handling[]
+import groovy.concurrent.Awaitable
+
+async def riskyOperation() {
+ throw new IOException("network timeout")
+}
+
+// Exception handling works exactly like synchronous code
+try {
+ await riskyOperation()
+ assert false : "should not reach here"
+} catch (IOException e) {
+ assert e.message == "network timeout" // <1>
+}
+// end::motivation_exception_handling[]
+ '''
+ }
+
+ @Test
+ void testMotivationStreamProcessing() {
+ assertScript '''
+// tag::motivation_stream_processing[]
+import groovy.concurrent.Awaitable
+
+// Async generator produces values on-demand with back-pressure
+async def fibonacci(int count) {
+ long a = 0, b = 1
+ for (int i = 0; i < count; i++) {
+ yield return a
+ (a, b) = [b, a + b]
+ }
+}
+
+def results = []
+for await (num in fibonacci(8)) {
+ results << num
+}
+assert results == [0, 1, 1, 2, 3, 5, 8, 13]
+// end::motivation_stream_processing[]
+ '''
+ }
+
+ @Test
+ void testMotivationInteropCF() {
+ assertScript '''
+// tag::motivation_interop[]
+import java.util.concurrent.CompletableFuture
+import groovy.concurrent.Awaitable
+
+// Groovy's await understands CompletableFuture, CompletionStage,
+// Future, and Flow.Publisher out of the box — no conversion needed
+async def mixedSources() {
+ def fromCF = await CompletableFuture.supplyAsync { "cf" }
+ def fromAwaitable = await Awaitable.of("awaitable")
+ return "${fromCF}+${fromAwaitable}"
+}
+
+assert await(mixedSources()) == "cf+awaitable"
+// end::motivation_interop[]
+ '''
+ }
+
+ // =========================================================================
+ // Structured concurrency — AsyncScope
+ // =========================================================================
+
+ @Test
+ void testAsyncScopeBasic() {
+ assertScript '''
+// tag::async_scope_basic[]
+import groovy.concurrent.AsyncScope
+import groovy.concurrent.Awaitable
+
+class UserService {
+ async fetchUser(int id) {
+ await(Awaitable.delay(10))
+ return [id: id, name: "User-${id}"]
+ }
+
+ async fetchOrders(int userId) {
+ await(Awaitable.delay(10))
+ return [[item: "Book", userId: userId], [item: "Pen", userId: userId]]
+ }
+
+ async loadDashboard(int userId) {
+ AsyncScope.withScope { scope ->
+ def userTask = scope.async { await fetchUser(userId) }
+ def ordersTask = scope.async { await fetchOrders(userId) }
+ [user: await(userTask), orders: await(ordersTask)]
+ }
+ }
+}
+
+def svc = new UserService()
+def dashboard = await(svc.loadDashboard(42))
+assert dashboard.user.name == "User-42"
+assert dashboard.orders.size() == 2
+// Both tasks are guaranteed complete when loadDashboard returns
+// end::async_scope_basic[]
+ '''
+ }
+
+ @Test
+ void testAsyncScopeFailFast() {
+ assertScript '''
+// tag::async_scope_fail_fast[]
+import groovy.concurrent.AsyncScope
+import groovy.concurrent.Awaitable
+
+async slowTask() {
+ await(Awaitable.delay(5000))
+ return "done"
+}
+
+async failingTask() {
+ await(Awaitable.delay(10))
+ throw new IllegalStateException("service unavailable")
+}
+
+def error = null
+try {
+ AsyncScope.withScope { scope ->
+ scope.async { await slowTask() }
+ scope.async { await failingTask() }
+ }
+} catch (IllegalStateException e) {
+ error = e
+}
+
+assert error != null
+assert error.message == "service unavailable"
+// The slow task was automatically cancelled when failingTask threw
+// end::async_scope_fail_fast[]
+ '''
+ }
+
+ @Test
+ void testAsyncScopeFanOut() {
+ assertScript '''
+// tag::async_scope_fanout[]
+import groovy.concurrent.AsyncScope
+import groovy.concurrent.Awaitable
+
+async fetchPage(String url) {
+ await(Awaitable.delay(10))
+ return "Content of ${url}"
+}
+
+async crawlAll(List<String> urls) {
+ AsyncScope.withScope { scope ->
+ def tasks = urls.collect { url ->
+ scope.async { await fetchPage(url) }
+ }
+ tasks.collect { task -> await(task) }
+ }
+}
+
+def urls = ["https://example.com/1", "https://example.com/2", "https://example.com/3"]
+def pages = await(crawlAll(urls))
+assert pages.size() == 3
+assert pages.every { it.startsWith("Content of") }
+// All fetches ran concurrently; all guaranteed complete when crawlAll returns
+// end::async_scope_fanout[]
+ '''
+ }
+
+ @Test
+ void testAsyncScopeManual() {
+ assertScript '''
+// tag::async_scope_manual[]
+import groovy.concurrent.AsyncScope
+import groovy.concurrent.Awaitable
+
+async computePrice(String item) {
+ await(Awaitable.delay(10))
+ return item == "Book" ? 29.99 : 9.99
+}
+
+def scope = new AsyncScope()
+def price1 = scope.async { await computePrice("Book") }
+def price2 = scope.async { await computePrice("Pen") }
+
+assert await(price1) == 29.99
+assert await(price2) == 9.99
+scope.close() // waits for all children, idempotent
+// end::async_scope_manual[]
+ '''
+ }
+
+ @Test
+ void testAsyncContextPropagation() {
+ assertScript '''
+// tag::async_context_basic[]
+import groovy.concurrent.AsyncContext
+import groovy.concurrent.Awaitable
+
+AsyncContext.with([traceId: 'req-7', tenant: 'acme']) {
+ def task = Awaitable.go {
+ assert AsyncContext.current()['traceId'] == 'req-7'
+ AsyncContext.current()['traceId'] = 'child-trace'
+ [trace: AsyncContext.current()['traceId'], tenant: AsyncContext.current()['tenant']]
+ }
+
+ def child = await task
+ assert child == [trace: 'child-trace', tenant: 'acme']
+ assert AsyncContext.current()['traceId'] == 'req-7'
+}
+// end::async_context_basic[]
+ '''
+ }
+
+ @Test
+ void testGoChannelSelect() {
+ assertScript '''
+// tag::go_channel_select[]
+import groovy.concurrent.AsyncScope
+import groovy.concurrent.Awaitable
+
+def result = AsyncScope.withScope { scope ->
+ def ch = Awaitable.channel(1)
+
+ def producer = Awaitable.go {
+ await ch.send('payload')
+ 'sent'
+ }
+
+ def consumer = scope.async {
+ await ch.receive()
+ }
+
+ def value = await Awaitable.any(
+ consumer,
+ Awaitable.delay(250).then { 'timeout' }
+ )
+
+ [value: value, producer: await(producer), childCount: scope.childCount]
+}
+
+assert result == [value: 'payload', producer: 'sent', childCount: 2]
+// end::go_channel_select[]
+ '''
+ }
+
+ @Test
+ void testDeferAsyncCleanup() {
+ assertScript '''
+// tag::defer_async_cleanup[]
+import groovy.concurrent.Awaitable
+
+class ResourceHolder {
+ final List<String> log = []
+
+ Awaitable<Void> closeAsync(String name) {
+ Awaitable.go {
+ await Awaitable.delay(10)
+ log << "close:${name}"
+ null
+ }
+ }
+
+ async useResource() {
+ defer { closeAsync('outer') }
+ defer { closeAsync('inner') }
+ log << 'body'
+ return log
+ }
+}
+
+def holder = new ResourceHolder()
+assert await(holder.useResource()) == ['body', 'close:inner', 'close:outer']
+// end::defer_async_cleanup[]
+ '''
+ }
+
+ @Test
+ void testChannelFanInPipeline() {
+ assertScript '''
+// tag::channel_fan_in[]
+import groovy.concurrent.AsyncScope
+import groovy.concurrent.Awaitable
+
+def results = AsyncScope.withScope { scope ->
+ def ch = Awaitable.channel(8)
+
+ // Fan-out: multiple producers write to a shared channel
+ 3.times { producerId ->
+ scope.async {
+ for (int i = 0; i < 4; i++) {
+ await ch.send("p${producerId}-item${i}")
+ }
+ null
+ }
+ }
+
+ // Fan-in: single consumer drains the channel
+ def consumer = scope.async {
+ def items = []
+ 12.times { items << await(ch.receive()) }
+ items.sort()
+ }
+
+ await consumer
+}
+
+assert results.size() == 12
+assert results.count { it.startsWith('p0-') } == 4
+assert results.count { it.startsWith('p1-') } == 4
+assert results.count { it.startsWith('p2-') } == 4
+// end::channel_fan_in[]
+ '''
+ }
+
+ @Test
+ void testAsyncContextSnapshot() {
+ assertScript '''
+// tag::async_context_snapshot[]
+import groovy.concurrent.AsyncContext
+import groovy.concurrent.Awaitable
+
+// Capture a snapshot to hand off to a different async scope
+AsyncContext.current()['requestId'] = 'req-99'
+def snapshot = AsyncContext.capture()
+
+// Mutate the parent context — the snapshot is unaffected
+AsyncContext.current()['requestId'] = 'req-100'
+
+def childResult = await Awaitable.go {
+ // Child sees the parent's captured state, not the earlier snapshot
+ AsyncContext.current()['requestId']
+}
+
+assert childResult == 'req-100' // go captures at call time
+
+// Manually restore snapshot in a different scope
+def restored = AsyncContext.withSnapshot(snapshot, { ->
+ AsyncContext.current()['requestId']
+} as java.util.function.Supplier)
+
+assert restored == 'req-99'
+// end::async_context_snapshot[]
+ '''
+ }
+
+ @Test
+ void testForAwaitOverChannel() {
+ assertScript '''
+// tag::for_await_channel[]
+import groovy.concurrent.AsyncScope
+import groovy.concurrent.Awaitable
+
+def items = []
+def ch = Awaitable.channel(4)
+
+AsyncScope.withScope { scope ->
+ // Producer sends values and closes the channel when done
+ scope.async {
+ for (word in ['hello', 'async', 'world']) {
+ await ch.send(word)
+ }
+ ch.close()
+ null
+ }
+
+ // Consumer iterates the channel with for-await
+ for await (item in ch) {
+ items << item
+ }
+}
+
+assert items == ['hello', 'async', 'world']
+// end::for_await_channel[]
+ '''
+ }
+
+ @Test
+ void testChannelVsFlowPublisherForAwait() {
+ assertScript '''
+// tag::channel_vs_flow_for_await[]
+import groovy.concurrent.AsyncScope
+import groovy.concurrent.Awaitable
+import java.util.concurrent.Flow
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+// --- Channel: Go-style coordination primitive ---
+// Producers and consumers communicate through explicit send/receive.
+// Best for task-to-task coordination and fan-in/fan-out pipelines.
+def channelItems = []
+def ch = Awaitable.channel(3)
+
+AsyncScope.withScope { scope ->
+ scope.async {
+ for (n in [10, 20, 30]) { await ch.send(n) }
+ ch.close()
+ null
+ }
+ for await (item in ch) {
+ channelItems << item
+ }
+}
+assert channelItems == [10, 20, 30]
+
+// --- Flow.Publisher: reactive data stream ---
+// A push-based source adapted automatically for for-await consumption.
+// Best for consuming reactive streams, HTTP responses, and database cursors.
+def publisherItems = []
+
+def publisher = new Flow.Publisher<Integer>() {
+ void subscribe(Flow.Subscriber<? super Integer> subscriber) {
+ subscriber.onSubscribe(new Flow.Subscription() {
+ int count = 0
+ void request(long n) {
+ for (int i = 0; i < n && count < 3; i++) {
+ count++
+ subscriber.onNext(count * 100)
+ }
+ if (count >= 3) subscriber.onComplete()
+ }
+ void cancel() {}
+ })
+ }
+}
+for await (item in publisher) {
+ publisherItems << item
+}
+assert publisherItems == [100, 200, 300]
+
+// Both Channel and Flow.Publisher work with 'for await' through
+// the unified AsyncStream abstraction — same syntax, different use cases.
+// end::channel_vs_flow_for_await[]
+ '''
+ }
+}
diff --git a/src/test/groovy/org/apache/groovy/runtime/async/AsyncConcurrencyTest.groovy b/src/test/groovy/org/apache/groovy/runtime/async/AsyncConcurrencyTest.groovy
new file mode 100644
index 0000000000..abc1e6c501
--- /dev/null
+++ b/src/test/groovy/org/apache/groovy/runtime/async/AsyncConcurrencyTest.groovy
@@ -0,0 +1,540 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.groovy.runtime.async
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.Timeout
+import org.junit.jupiter.api.DisplayName
+
+import java.io.IOException
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.CyclicBarrier
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Thread-safety verification tests for AsyncStreamGenerator.
+ * These tests verify that the generator correctly handles concurrent access patterns
+ * and detects unsafe operations before they cause deadlocks or data corruption.
+ *
+ * @since 6.0.0
+ */
+@DisplayName("AsyncStreamGenerator Concurrency Tests")
+class AsyncConcurrencyTest {
+
+ /**
+ * Two threads calling moveNext() concurrently on the same generator.
+ *
+ * Expected behavior: Should fail fast (IllegalStateException or similar),
+ * NOT deadlock one thread or overwrite consumer reference.
+ */
+ @Test
+ @Timeout(5)
+ @DisplayName("Concurrent moveNext() calls should fail fast, not deadlock")
+ void testConcurrentMoveNext_ShouldFailFast() {
+ AsyncStreamGenerator<Integer> gen = new AsyncStreamGenerator<>()
+
+ // Set up producer to emit values
+ Thread producer = Thread.start {
+ gen.attachProducer(Thread.currentThread())
+ try {
+ gen.yield(1)
+ gen.yield(2)
+ gen.yield(3)
+ gen.complete()
+ } finally {
+ gen.detachProducer(Thread.currentThread())
+ }
+ }
+
+ CyclicBarrier barrier = new CyclicBarrier(2)
+ AtomicReference<Exception> error1 = new AtomicReference<>(null)
+ AtomicReference<Exception> error2 = new AtomicReference<>(null)
+
+ Thread consumer1 = Thread.start {
+ try {
+ barrier.await(5, TimeUnit.SECONDS) // Synchronize both consumers
+ gen.moveNext()
+ } catch (Exception e) {
+ error1.set(e)
+ }
+ }
+
+ Thread consumer2 = Thread.start {
+ try {
+ barrier.await(5, TimeUnit.SECONDS) // Synchronize both consumers
+ gen.moveNext()
+ } catch (Exception e) {
+ error2.set(e)
+ }
+ }
+
+ // Wait for both consumers to complete (or error)
+ consumer1.join(3000)
+ consumer2.join(3000)
+ producer.join(3000)
+
+ // Verify that both consumers should not be alive (no deadlock) - CRITICAL
+ assert !consumer1.isAlive() : "Consumer 1 should not deadlock"
+ assert !consumer2.isAlive() : "Consumer 2 should not deadlock"
+
+ // Verify we're testing the right thing: if one got an error, it should be IllegalStateException
+ if (error1.get() != null) {
+ assert error1.get() instanceof IllegalStateException : \
+ "Consumer 1 error should be IllegalStateException, got: ${error1.get()?.class?.simpleName}"
+ }
+ if (error2.get() != null) {
+ assert error2.get() instanceof IllegalStateException : \
+ "Consumer 2 error should be IllegalStateException, got: ${error2.get()?.class?.simpleName}"
+ }
+ }
+
+ /**
+ * Interrupt the producer thread while blocked in yield().
+ *
+ * Expected behavior: Consumer should receive signal or complete,
+ * NOT hang.
+ */
+ @Test
+ @Timeout(5)
+ @DisplayName("Interrupted producer thread should signal consumer, not hang")
+ void testInterruptedProducer_ShouldSignalConsumer() {
+ AsyncStreamGenerator<Integer> gen = new AsyncStreamGenerator<>()
+
+ CountDownLatch producerBlocked = new CountDownLatch(1)
+ CountDownLatch consumerStarted = new CountDownLatch(1)
+ AtomicReference<Exception> consumerError = new AtomicReference<>(null)
+
+ // Producer yields one value, then blocks on the second yield
+ Thread producer = Thread.start {
+ gen.attachProducer(Thread.currentThread())
+ try {
+ gen.yield(1)
+ producerBlocked.countDown() // Signal that we're about to block
+ Thread.sleep(100) // Small delay to ensure consumer reads the first value
+ gen.yield(2) // This will block indefinitely without a consumer
+ gen.complete()
+ } catch (Exception e) {
+ // Producer may receive InterruptedException wrapped in CancellationException
+ consumerError.set(e)
+ } finally {
+ gen.detachProducer(Thread.currentThread())
+ }
+ }
+
+ // Consumer reads values until stream ends
+ Thread consumer = Thread.start {
+ try {
+ consumerStarted.countDown()
+ // Try to read all values from the stream
+ while (gen.moveNext().get()) {
+ gen.getCurrent()
+ }
+ } catch (Exception e) {
+ // Consumer may get an exception
+ consumerError.set(e)
+ }
+ }
+
+ // Wait for producer to be blocked in yield()
+ producerBlocked.await(2, TimeUnit.SECONDS)
+ consumerStarted.await(2, TimeUnit.SECONDS)
+
+ Thread.sleep(200) // Ensure producer is blocked
+
+ // Interrupt producer to simulate external interruption
+ producer.interrupt()
+
+ // Give threads time to react
+ producer.join(3000)
+ consumer.join(3000)
+
+ // Verify consumer didn't hang - this is the main assertion
+ assert !consumer.isAlive() : "Consumer should not hang after producer interruption"
+
+ // Verify producer also terminated
+ assert !producer.isAlive() : "Producer should be interrupted and cleanup"
+ }
+
+ /**
+ * Early exit from consumer (simulating break in for-await loop).
+ *
+ * Expected behavior: Producer should be unblocked via interrupt and cleanup.
+ */
+ @Test
+ @Timeout(5)
+ @DisplayName("Early consumer exit should cleanup producer thread")
+ void testEarlyConsumerExit_ShouldCleanupProducer() {
+ AsyncStreamGenerator<Integer> gen = new AsyncStreamGenerator<>()
+
+ CountDownLatch producerStarted = new CountDownLatch(1)
+ CountDownLatch producerBlocked = new CountDownLatch(1)
+
+ Thread producer = Thread.start {
+ gen.attachProducer(Thread.currentThread())
+ producerStarted.countDown()
+ try {
+ for (int i = 1; i <= 10; i++) {
+ if (i == 5) {
+ producerBlocked.countDown() // Signal we're about to block on item 5
+ }
+ gen.yield(i)
+ }
+ gen.complete()
+ } catch (Exception e) {
+ // Expected: InterruptedException wrapped in CancellationException
+ // Producer received expected exception
+ } finally {
+ gen.detachProducer(Thread.currentThread())
+ }
+ }
+
+ // Consumer exits early after reading first 2 values
+ Thread consumer = Thread.start {
+ try {
+ gen.moveNext() // Read 1
+ gen.getCurrent()
+ gen.moveNext() // Read 2
+ gen.getCurrent()
+ // Exit early - should trigger cleanup of producer
+ gen.close()
+ } catch (Exception e) {
+ // Consumer error during close
+ }
+ }
+
+ consumer.join(3000)
+ producerStarted.await(2, TimeUnit.SECONDS)
+
+ // If producer reaches blocked state, give it a moment
+ producerBlocked.await(3, TimeUnit.SECONDS)
+ Thread.sleep(200)
+
+ producer.join(3000)
+
+ // Verify producer is not still running
+ assert !producer.isAlive() : "Producer should be interrupted and cleanup when consumer closes"
+ }
+
+ /**
+ * Multiple close() calls should be idempotent and safe.
+ */
+ @Test
+ @Timeout(5)
+ @DisplayName("Multiple close() calls should be safe and idempotent")
+ void testMultipleClose_ShouldBeIdempotent() {
+ AsyncStreamGenerator<Integer> gen = new AsyncStreamGenerator<>()
+
+ Thread producer = Thread.start {
+ gen.attachProducer(Thread.currentThread())
+ try {
+ gen.yield(1)
+ gen.complete()
+ } finally {
+ gen.detachProducer(Thread.currentThread())
+ }
+ }
+
+ // Multiple threads closing simultaneously
+ List<Thread> closers = (1..5).collect { idx ->
+ Thread.start {
+ try {
+ gen.close()
+ } catch (Exception e) {
+ // Close error
+ }
+ }
+ }
+
+ producer.join(3000)
+ closers.each { it.join(3000) }
+
+ closers.each { assert !it.isAlive() : "Close threads should not hang" }
+ }
+
+ /**
+ * Producer completing vs consumer exiting - race condition test.
+ *
+ * Expected behavior: Both should complete cleanly without deadlock.
+ */
+ @Test
+ @Timeout(5)
+ @DisplayName("Producer completion and consumer exit should not deadlock")
+ void testProducerCompletionVsConsumerExit_NoDeadlock() {
+ for (int iteration = 0; iteration < 10; iteration++) {
+ AsyncStreamGenerator<Integer> gen = new AsyncStreamGenerator<>()
+
+ Thread producer = Thread.start {
+ gen.attachProducer(Thread.currentThread())
+ try {
+ gen.yield(1)
+ gen.yield(2)
+ gen.complete()
+ } finally {
+ gen.detachProducer(Thread.currentThread())
+ }
+ }
+
+ Thread consumer = Thread.start {
+ try {
+ gen.moveNext()
+ gen.getCurrent()
+ gen.moveNext()
+ gen.getCurrent()
+ gen.moveNext()
+ } catch (Exception e) {
+ // Consumer exception
+ }
+ }
... 19251 lines suppressed ...