This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch GROOVY-9381_2
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY-9381_2 by this push:
new a32ed209ab Add async/await docs
a32ed209ab is described below
commit a32ed209ab910aa9346088641eb8c6ac615fa248
Author: Daniel Sun <[email protected]>
AuthorDate: Mon Mar 2 02:31:30 2026 +0900
Add async/await docs
---
src/spec/doc/core-async-await.adoc | 549 ++++++++++++++++++++++
src/spec/test/AsyncAwaitSpecTest.groovy | 776 ++++++++++++++++++++++++++++++++
2 files changed, 1325 insertions(+)
diff --git a/src/spec/doc/core-async-await.adoc
b/src/spec/doc/core-async-await.adoc
new file mode 100644
index 0000000000..1fb316eccd
--- /dev/null
+++ b/src/spec/doc/core-async-await.adoc
@@ -0,0 +1,549 @@
+//////////////////////////////////////////
+
+ 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# 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 Go-style cleanup actions that run when the method
completes
+* **Framework integration** — built-in adapters for `CompletableFuture`,
`Future`, and `Flow.Publisher`;
+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.
+
+[[async-methods]]
+== Async Methods
+
+An `async` method executes its body asynchronously and returns a
+`groovy.concurrent.Awaitable` — a promise-like abstraction that represents a
pending result.
+
+=== 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`, 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`, `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.
+====
+
+=== 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` — Async Iteration
+
+The `for await` loop iterates over asynchronous data sources. Each element may
be an
+asynchronous value that is resolved before the loop body executes.
+
+=== 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]
+----
+
+`for await` supports both `in` and `:` notation:
+
+[source,groovy]
+----
+for await (item in source) { ... }
+for await (item : source) { ... }
+----
+
+[[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` instead of a
regular `Awaitable`.
+
+=== 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` — Go-Style 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, similar to Go's `defer`
statement.
+
+=== 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]
+----
+
+[[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.
+
+=== 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).
+
+=== Handling Failures from Multiple Tasks
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=exception_multiple_tasks,indent=0]
+----
+
+[[utility-methods]]
+== Utility Methods
+
+The `groovy.concurrent.AsyncUtils` class provides utility methods for common
async
+coordination patterns.
+
+=== `awaitAll` — Parallel Execution
+
+Wait for all tasks to complete and collect their results:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_all,indent=0]
+----
+
+=== `awaitAny` — Race Pattern
+
+Return the result of the first task to complete:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_any,indent=0]
+----
+
+=== `awaitAllSettled` — Inspect All Outcomes
+
+Wait for all tasks to complete, collecting both successes and failures:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_all_settled,indent=0]
+----
+
+Each `AwaitResult` has `isSuccess()`, `isFailure()`, `value`, and `error`
properties.
+
+=== `delay` — Non-Blocking Pause
+
+Create an awaitable timer:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=delay_example,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`],
+enabling seamless consumption of reactive streams.
+
+=== 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]
+----
+
+[[adapter-registry]]
+== Adapter Registry
+
+The `groovy.concurrent.AwaitableAdapterRegistry` allows extending `await` to
support additional
+asynchronous types. Built-in adapters handle:
+
+* `groovy.concurrent.Awaitable` (native Groovy promise)
+* `java.util.concurrent.CompletableFuture`
+* `java.util.concurrent.Future`
+* `java.util.concurrent.Flow.Publisher`
+
+To support frameworks like RxJava or Reactor, register custom adapters:
+
+[source,groovy]
+----
+import groovy.concurrent.AwaitableAdapterRegistry
+import groovy.concurrent.AwaitableAdapter
+
+AwaitableAdapterRegistry.register(new AwaitableAdapter() {
+ boolean supports(Object obj) { obj instanceof
io.reactivex.rxjava3.core.Single }
+ Object await(Object obj) {
+ ((io.reactivex.rxjava3.core.Single) obj).blockingGet()
+ }
+})
+----
+
+=== Framework Integration Examples
+
+With adapters registered, `await` works transparently:
+
+[source,groovy]
+----
+// RxJava 3
+def single = Single.just("RxJava value")
+def result = await(single)
+
+// Project Reactor
+def mono = Mono.just("Reactor value")
+def result = await(mono)
+----
+
+[[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.
+
+[[executor-configuration]]
+== Executor Configuration
+
+=== Default Behavior
+
+On **JDK 21+**, async methods run on 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).
+
+=== Custom Executor
+
+Override the default executor via `AsyncUtils.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
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=parallel_pattern,indent=0]
+----
+
+=== Retry Pattern
+
+[source,groovy]
+----
+async fetchWithRetry(int maxRetries) {
+ for (int attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ return await fetchData()
+ } catch (Exception e) {
+ if (attempt == maxRetries) throw e
+ await(AsyncUtils.delay(100 * attempt)) // exponential backoff
+ }
+ }
+}
+----
+
+=== Timeout Pattern
+
+[source,groovy]
+----
+import groovy.concurrent.AsyncUtils
+
+def result = AsyncUtils.awaitAny(
+ longRunningTask(),
+ AsyncUtils.delay(5000).then { throw new TimeoutException("timed out") }
+)
+----
+
+[[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) { ... }`
+
+| Yield return
+| `yield return expr` (inside async methods)
+
+| Defer
+| `defer { cleanup code }`
+
+| Parallel wait
+| `AsyncUtils.awaitAll(a, b, c)`
+
+| Race
+| `AsyncUtils.awaitAny(a, b)`
+
+| All settled
+| `AsyncUtils.awaitAllSettled(a, b, c)`
+
+| Delay
+| `AsyncUtils.delay(millis)`
+
+| Annotation form
+| `@Async def methodName() { ... }`
+|===
diff --git a/src/spec/test/AsyncAwaitSpecTest.groovy
b/src/spec/test/AsyncAwaitSpecTest.groovy
new file mode 100644
index 0000000000..365f982e57
--- /dev/null
+++ b/src/spec/test/AsyncAwaitSpecTest.groovy
@@ -0,0 +1,776 @@
+/*
+ * 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.AsyncUtils
+
+async fetchUser() { "Alice" }
+async fetchOrder() { "Order#42" }
+async fetchBalance() { 100.0 }
+
+def results = AsyncUtils.awaitAll(fetchUser(), fetchOrder(), fetchBalance())
+assert results == ["Alice", "Order#42", 100.0]
+// end::await_all[]
+ '''
+ }
+
+ @Test
+ void testAwaitAny() {
+ assertScript '''
+// tag::await_any[]
+import groovy.concurrent.AsyncUtils
+
+def fast = async { "fast result" }
+def slow = async {
+ await(AsyncUtils.delay(2000))
+ return "slow result"
+}
+
+def winner = AsyncUtils.awaitAny(fast(), slow())
+assert winner == "fast result"
+// end::await_any[]
+ '''
+ }
+
+ @Test
+ void testAwaitAllSettled() {
+ assertScript '''
+// tag::await_all_settled[]
+import groovy.concurrent.AsyncUtils
+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 AsyncUtils.awaitAllSettled(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.AsyncUtils
+
+async delayedGreeting() {
+ await(AsyncUtils.delay(100)) // pause for 100 milliseconds
+ return "Hello after delay"
+}
+
+assert await(delayedGreeting()) == "Hello after delay"
+// end::delay_example[]
+ '''
+ }
+
+ //
=========================================================================
+ // 9. Flow.Publisher integration
+ //
=========================================================================
+
+ @Test
+ void testFlowPublisherAwait() {
+ assertScript '''
+// tag::flow_publisher_await[]
+import java.util.concurrent.SubmissionPublisher
+
+def publisher = new SubmissionPublisher<String>()
+Thread.start {
+ Thread.sleep(50)
+ 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)
+Thread.start {
+ Thread.sleep(50)
+ (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[]
+ '''
+ }
+
+ //
=========================================================================
+ // 11. Custom executor
+ //
=========================================================================
+
+ @Test
+ void testCustomExecutor() {
+ assertScript '''
+// tag::custom_executor[]
+import groovy.concurrent.AsyncUtils
+import java.util.concurrent.Executors
+
+class WorkService {
+ async work() { Thread.currentThread().name }
+}
+
+def customPool = Executors.newFixedThreadPool(2)
+try {
+ AsyncUtils.setExecutor(customPool)
+
+ def threadName = await(new WorkService().work())
+ assert threadName.contains("pool")
+} finally {
+ AsyncUtils.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.AsyncUtils
+
+async fetchPage(String url) {
+ await(AsyncUtils.delay(10)) // simulate network I/O
+ return "Content of ${url}"
+}
+
+// Launch three tasks in parallel
+def pages = AsyncUtils.awaitAll(
+ 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[]
+ '''
+ }
+}