This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/groovy-website.git


The following commit(s) were added to refs/heads/asf-site by this push:
     new 2c4b454  update async for simplified proposal
2c4b454 is described below

commit 2c4b4544f846cf1e5b1c26c3eced66b5bd202a51
Author: Paul King <[email protected]>
AuthorDate: Mon Apr 6 17:20:44 2026 +1000

    update async for simplified proposal
---
 site/src/site/blog/groovy-async-await.adoc   | 587 ++++++++++++---------------
 site/src/site/blog/groovy-async-await_5.adoc | 443 --------------------
 2 files changed, 251 insertions(+), 779 deletions(-)

diff --git a/site/src/site/blog/groovy-async-await.adoc 
b/site/src/site/blog/groovy-async-await.adoc
index 9f6349a..79eb9aa 100644
--- a/site/src/site/blog/groovy-async-await.adoc
+++ b/site/src/site/blog/groovy-async-await.adoc
@@ -1,87 +1,32 @@
 = Async/await for Groovy&trade;
 Paul King <paulk-asert|PMC_Member>
 :revdate: 2026-03-27T16:30:00+00:00
+:updated: 2026-04-06T23:30:00+00:00
 :keywords: async, await, concurrency, virtual-threads
-:description: This post looks at a proposed extension to Groovy which provides 
comprehensive async/await support.
+:description: Groovy's async/await: write concurrent code that reads like 
synchronous code, with virtual thread support, generators, channels, & 
structured concurrency.
 
 == Introduction
 
-A proposed enhancement, targeted for Groovy 6,
-adds native `async`/`await` as a language-level feature
-(https://issues.apache.org/jira/browse/GROOVY-9381[GROOVY-9381],
-https://github.com/apache/groovy/pull/2387[PR \#2387]).
-Inspired by similar constructs in JavaScript, C#, Kotlin, and Swift,
-the proposal would let you write asynchronous code in a sequential, readable
-style — with first-class support for async streams, deferred cleanup,
-structured concurrency, Go-style channels, and framework adapters
-for Reactor and RxJava.
-
-On JDK 21+, async methods automatically leverage
-https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#ofVirtual()[virtual
 threads]
-for optimal scalability.
-
-This is a comprehensive feature. Rather than cover every detail,
-this post walks through a handful of bite-sized examples that show
-what the day-to-day experience would feel like — and how it compares
-to what you'd write in plain Java today.
-
-== Choosing the right tool
-
-The proposal provides several complementary features.
-Before diving in, here's a quick guide to when you'd reach for each:
-
-[cols="2,3,3", options="header"]
-|===
-| Feature | Use when… | Complements
-
-| `async`/`await`
-| You have sequential steps that involve I/O or other blocking work and want 
code that reads top-to-bottom.
-| Foundation for everything else — the other features build on top of async 
methods and closures.
-
-| `Awaitable.all` / `any`
-| You need to launch several independent tasks and collect (all) or race 
(first) their results.
-| Pairs with `async` closures to create the tasks that `all`/`any` coordinate.
-
-| `yield return` / `for await`
-| You're producing or consuming a _stream_ of values over time — paginated 
APIs, sensor data, log tailing.
-| Producer uses `async` + `yield return`; consumer uses `for await`. 
Back-pressure is automatic.
-
-| `defer`
-| You acquire resources at different points and want guaranteed cleanup 
without nested `try`/`finally`.
-| Works inside async methods _and_ async closures. LIFO order mirrors Go's 
`defer`.
-
-| Channels (`AsyncChannel`)
-| Two or more tasks need to communicate — producer/consumer, fan-out/fan-in, 
or rendezvous hand-off.
-| Created with `AsyncChannel.create()`; consumed with `for await`; launched 
with `Awaitable.go`.
-
-| `AsyncScope`
-| You want structured concurrency — child task lifetimes tied to a scope with 
automatic cancellation.
-| Groovy's take on the same goal as JDK `StructuredTaskScope`, with 
`async`/`await` integration.
-
-| `AsyncContext`
-| You need contextual data (e.g. player session, logging trace ID) to follow 
your async calls across thread hops.
-| Automatically propagated through `async`/`await`, `AsyncScope`, and 
`Awaitable.go`.
+Groovy 6 adds native `async`/`await` as a language-level feature
+(https://issues.apache.org/jira/browse/GROOVY-9381[GROOVY-9381]).
+Write asynchronous code in a sequential, readable style —
+with support for generators, deferred cleanup, Go-style channels,
+structured concurrency, and framework adapters for Reactor and RxJava.
 
-| Framework adapters
-| You're already using Reactor or RxJava and want `await` to work 
transparently with their types.
-| Auto-discovered via `ServiceLoader` — just add the dependency.
-|===
+On JDK 21+, async tasks automatically leverage
+https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#ofVirtual()[virtual
 threads]
+for optimal scalability. On JDK 17–20, a cached thread pool provides
+correct behavior as a fallback.
 
-In practice, you'll mix and match. A typical service handler might
-use `async`/`await` for its main flow, `Awaitable.all` to fan out
-parallel calls, `defer` for cleanup, and `AsyncScope` to ensure
-nothing leaks.
+To make the features concrete, the examples follow a running theme:
+building the backend for _Groovy Quest_, a fictitious online game
+where heroes battle villains across dungeons.
 
-To make the features concrete, the examples below follow a running
-theme: building the backend for _Groovy Quest_, a fictitious online game
-where heroes battle villains across dungeons. Each example tackles
-a different part of the game — loading heroes, spawning enemies,
-streaming dungeon waves, managing resources, and coordinating
-raid parties.
+== Getting started
 
-== The problem: callback complexity
+=== The problem: callback complexity
 
-Imagine a player logs in and we need to load their quest: look up
+A player logs in and we need to load their quest: look up
 their hero ID, fetch the hero's class, then load their active quest.
 With `CompletableFuture` the logic gets buried under plumbing:
 
@@ -96,34 +41,43 @@ CompletableFuture<Quest> quest =
 ----
 
 Each `.thenCompose()` adds a nesting level, exception recovery is
-separated from the code that causes the exception, and the control
-flow reads inside-out. For this example, the simple chaining
-is manageable, but the complexity grows non-linearly with
-branching and error handling.
+separated from the code that causes it, and the control flow reads
+inside-out.
 
-== Example 1: loading a hero — reads like synchronous code
+=== Loading a hero — reads like synchronous code
 
-With the proposed `async`/`await`, the same logic becomes:
+With `async`/`await`, the same logic becomes:
 
 [source,groovy]
 ----
-async Quest loadHeroQuest(String loginToken) {
+Quest loadHeroQuest(String loginToken) {
     var heroId    = await lookupHeroId(loginToken)
     var heroClass = await fetchHeroClass(heroId)
-    var quest     = await loadActiveQuest(heroClass)
-    return quest
+    return await loadActiveQuest(heroClass)
 }
 ----
 
-Variables are declared at the point of use. The return value is
-obvious. No callbacks, no lambdas, no chained combinators.
+Variables are declared at the point of use. The return value is obvious.
+No callbacks, no lambdas, no chained combinators. The method is a
+regular method — the caller decides whether to run it asynchronously:
+
+[source,groovy]
+----
+// Run asynchronously:
+def quest = await async { loadHeroQuest(token) }
+
+// Or call directly (blocking — fine on virtual threads):
+def quest = loadHeroQuest(token)
+----
+
+=== Exception handling — just `try`/`catch`
 
 What about the `.exceptionally(e -> Quest.DEFAULT)` fallback from
-the Java version? With `async`/`await`, it's just a `try`/`catch`:
+the Java version?
 
 [source,groovy]
 ----
-async Quest loadHeroQuest(String loginToken) {
+Quest loadHeroQuest(String loginToken) {
     try {
         var heroId    = await lookupHeroId(loginToken)
         var heroClass = await fetchHeroClass(heroId)
@@ -134,44 +88,39 @@ async Quest loadHeroQuest(String loginToken) {
 }
 ----
 
-`await` automatically unwraps `CompletionException`, so you catch
-the _original_ exception type — `NoActiveQuestException` here, not
-a `CompletionException` wrapper. Error handling reads exactly like
-synchronous code — no separate `.exceptionally()` callback bolted
-on at the end of a chain.
+`await` unwraps `CompletionException` automatically, so you catch
+the _original_ exception type. Error handling reads exactly like
+synchronous code.
+
+== Running tasks in parallel
 
-== Example 2: preparing for battle — fetch once, await together
+=== Preparing for battle — `Awaitable.all`
 
-Before a battle, the game needs to load several things in parallel:
-the hero's stats, their inventory, and the villain they're about
-to face. Launching concurrent work and collecting the results is a
-common pattern. Here's how it looks with `Awaitable.all`:
+Before a battle, the game loads the hero's stats, inventory, and
+the villain — all in parallel:
 
 [source,groovy]
 ----
-async prepareBattle(heroId, visibleVillainId) {
+def prepareBattle(heroId, visibleVillainId) {
     var stats     = async { fetchHeroStats(heroId) }
     var inventory = async { fetchInventory(heroId) }
     var villain   = async { fetchVillain(visibleVillainId) }
 
-    var (s, inv, v) = await stats(), inventory(), villain()
+    var (s, inv, v) = await Awaitable.all(stats, inventory, villain)
     return new BattleScreen(s, inv, v)
 }
 ----
 
-Here, `async { … }` creates an async closure — a reusable
-block that doesn't run until you call it.
-Invoking `stats()`, `inventory()`, and `villain()` each launches its 
respective block concurrently and returns an `Awaitable`.
+Each `async { ... }` starts immediately on a background thread.
+The `await stats, inventory, villain` expression waits for all three
+to complete — it's shorthand for `await Awaitable.all(stats, inventory, 
villain)`.
+Parentheses also work: `await(stats, inventory, villain)`.
 
-The `await stats(), inventory(), villain()` statement is a shorthand for
-`await Awaitable.all(stats(), inventory(), villain())`.
-The `all` combinator produces another `Awaitable` that completes when every 
task has finished. If any task fails, the remaining tasks still run to 
completion, and the first exception is thrown unwrapped. (For fail-fast 
semantics — cancelling siblings as soon as one fails — see `AsyncScope` in 
Example 6.)
-
-=== How this compares to Java's `StructuredTaskScope`
+==== How this compares to Java's `StructuredTaskScope`
 
 Java's structured concurrency preview
-(https://openjdk.org/jeps/525[JEP 525], previewing since JDK 21)
-provides a similar capability through `StructuredTaskScope`:
+(https://openjdk.org/jeps/525[JEP 525]) provides a similar
+capability:
 
 [source,java]
 ----
@@ -186,146 +135,117 @@ try (var scope = StructuredTaskScope.open()) {
 }
 ----
 
-The goals are aligned — both approaches bind task lifetimes to a
-scope and cancel siblings on failure. The Groovy version adds
-syntactic sugar (`await`, `all`) and integrates with the same
-`async`/`await` model used everywhere else, whereas Java's API
-is deliberately lower-level and imperative. We'll see more on
-how Groovy's `AsyncScope` complements JDK structured concurrency
-in <<_example_6_the_raid_party, Example 6>>.
-
-Note that this isn't an exact equivalent of our Groovy example.
-The async factory-like closures are reusable. If you don't need that
-flexibility, you can also use `Awaitable.go` to launch a one-off task.
-This more closely mirrors the Java version:
-
-[source,groovy]
-----
-async prepareBattle(heroId, visibleVillainId) {
-    var stats     = Awaitable.go { fetchHeroStats(heroId) }
-    var inventory = Awaitable.go { fetchInventory(heroId) }
-    var villain   = Awaitable.go { fetchVillain(visibleVillainId) }
-
-    await stats, inventory, villain
-    return new BattleScreen(stats.get(), inventory.get(), villain.get())
-}
-----
+Both approaches bind task lifetimes to a scope. Groovy adds syntactic
+sugar (`await`, `all`) and integrates with the same model used
+everywhere else, whereas Java's API is deliberately lower-level.
+Groovy's `AsyncScope` (covered later) brings the full structured
+concurrency model.
 
-=== The flip side: `Awaitable.any` — first one wins
+=== Capture the flag — `Awaitable.any`
 
-Where `all` waits for _every_ task, `any` returns as soon as the
-_first_ one completes — a race. Imagine a capture-the-flag battle
-where the hero and villain both dash for the flag:
+Where `all` waits for _every_ task, `any` returns the _first_ to
+complete — a race:
 
 [source,groovy]
 ----
-async captureTheFlag(hero, villain, flag) {
+def captureTheFlag(hero, villain, flag) {
     var heroGrab    = async { hero.grab(flag) }
     var villainGrab = async { villain.grab(flag) }
 
-    var winner = await Awaitable.any(heroGrab(), villainGrab())
+    var winner = await Awaitable.any(heroGrab, villainGrab)
     println "$winner.name captured the flag!"
 }
 ----
 
 The loser's task still runs to completion in the background
-(use `AsyncScope` if you want the loser cancelled immediately).
-This is the same "race" pattern as JavaScript's `Promise.race`
-or Go's `select`.
+(use `AsyncScope` for fail-fast cancellation).
+
+=== Other combinators
 
-== Example 3: dungeon waves — async streams with `yield return` and `for await`
+* **`Awaitable.first(a, b, c)`** — returns the first _successful_
+result, ignoring individual failures. Like JavaScript's
+`Promise.any()`. Useful for hedged requests and graceful degradation.
+* **`Awaitable.allSettled(a, b)`** — waits for all tasks to settle
+(succeed or fail) without throwing. Returns an `AwaitResult` list
+with `success`, `value`, and `error` fields.
 
-A dungeon sends waves of enemies at the hero. Each wave is fetched
-from the server (maybe procedurally generated), and the hero fights
-them as they arrive. This is a natural fit for _async streams_:
-`yield return` produces values lazily, and `for await` consumes them.
+== Generators and streaming
+
+=== Dungeon waves — `yield return` and `for await`
+
+A dungeon sends waves of enemies. Each wave is generated on demand
+and the hero fights them as they arrive:
 
 [source,groovy]
 ----
-async generateWaves(String dungeonId) {
-    var depth = 1
-    while (depth <= await dungeonDepth(dungeonId)) {
-        var wave = await spawnEnemies(dungeonId, depth)
-        yield return wave
-        depth++
+def generateWaves(String dungeonId) {
+    async {
+        var depth = 1
+        while (depth <= dungeonDepth(dungeonId)) {
+            yield return spawnEnemies(dungeonId, depth)
+            depth++
+        }
     }
 }
 
-async runDungeon(hero, dungeonId) {
+def runDungeon(hero, dungeonId) {
     for await (wave in generateWaves(dungeonId)) {
         wave.each { villain -> hero.fight(villain) }
     }
 }
 ----
 
-The producer yields each wave on demand. The consumer pulls them
-with `for await`. The runtime provides natural *back-pressure* —
-the producer blocks on each `yield return` until the hero is ready
-for the next wave, preventing unbounded enemy spawning. No explicit
-queues, signals, or synchronization required.
+The producer yields each wave on demand. The consumer pulls with
+`for await`. Natural *back-pressure* — the producer blocks on each
+`yield return` until the consumer is ready. No queues, signals, or
+synchronization.
 
-There's no language-level equivalent in plain Java today.
-You'd typically reach for Reactor's `Flux` or RxJava's `Flowable`, each of 
which
-brings its own operator vocabulary and mental model. With `for await`,
-async iteration feels as natural as a regular `for` loop.
+Since generators return a standard `Iterable`, regular `for` loops
+and Groovy collection methods (`collect`, `findAll`, `take`) also
+work — `for await` is optional for generators but required for
+reactive types (Flux, Observable).
 
-== Example 4: entering a dungeon — `defer` for guaranteed cleanup
+== Deferred cleanup — `defer`
 
-Before entering a dungeon, our hero summons a familiar (_spirit pet_) and 
opens a
-magic portal. Both must be cleaned up when the quest ends, whether
-the hero triumphs or falls. The `defer` keyword schedules cleanup
-to run when the enclosing async method completes — multiple deferred
-blocks execute in LIFO order, exactly like
+Before entering a dungeon, the hero summons a familiar and opens a
+portal. Both must be cleaned up when the quest ends. `defer` schedules
+cleanup in LIFO order, like
 https://go.dev/blog/defer-panic-and-recover[Go's `defer`]:
 
 [source,groovy]
 ----
-async enterDungeon(hero, dungeonId) {
-    var familiar = hero.summonFamiliar()
-    defer familiar.dismiss()
+def enterDungeon(hero, dungeonId) {
+    def task = async {
+        var familiar = hero.summonFamiliar()
+        defer familiar.dismiss()
 
-    var portal = openPortal(dungeonId)
-    defer portal.close()
+        var portal = openPortal(dungeonId)
+        defer portal.close()
 
-    await hero.explore(portal, familiar)
+        hero.explore(portal, familiar)
+    }
+    await task
 }
 ----
 
-`defer` also works inside async closures — handy for one-off
-tasks like a hero briefly powering up. Notice how the deferred
-cleanup runs _after_ the body completes:
-
-[source,groovy]
-----
-def log = []
-def powerUp = async {
-    defer { log << 'shield down' }
-    log << 'shield up'
-    'charged'
-}
-def result = await powerUp()
-assert result == 'charged'
-assert log == ['shield up', 'shield down']
-----
+Deferred actions always run — even when an exception occurs.
+This is cleaner than nested `try`/`finally` blocks when multiple
+resources are acquired at different points.
 
-This is cleaner than nested `try`/`finally` blocks, especially when
-multiple resources are acquired at different points in the method
-or closure.
+== Diving deeper
 
-== Example 5: the villain spawner — Go-style channels
+=== Channels — the villain spawner
 
-In a boss fight, a villain factory spawns enemies in the background
-while the hero fights them as they appear. The two sides need to
-communicate without tight coupling — a perfect fit for CSP-style
-channels inspired by Go:
+In a boss fight, a villain factory spawns enemies while the hero
+fights them. Channels provide Go-style decoupled communication:
 
 [source,groovy]
 ----
-async bossFight(hero, bossArena) {
+def bossFight(hero, bossArena) {
     var enemies = AsyncChannel.create(3)  // buffered channel
 
     // Villain spawner — runs concurrently
-    Awaitable.go {
+    async {
         for (type in bossArena.spawnOrder) {
             await enemies.send(new Villain(type))
         }
@@ -341,194 +261,189 @@ async bossFight(hero, bossArena) {
 }
 ----
 
-Channels support both unbuffered (rendezvous) and buffered modes.
-`for await` iterates received values until the channel is closed —
-the Groovy equivalent of Go's `for range ch`. You can also race
-channel operations with `Awaitable.any(...)`, serving a similar
-role to Go's `select` statement.
+Channels support unbuffered (rendezvous) and buffered modes.
+`for await` iterates until the channel is closed and drained.
+Channels implement `Iterable`, so regular `for` loops work too.
 
-[#_example_6_the_raid_party]
-== Example 6: the raid party — structured concurrency with `AsyncScope`
+=== Structured concurrency — the raid party
 
-A raid sends multiple heroes to scout different dungeon rooms
-simultaneously. If any hero falls, the whole raid retreats.
-`AsyncScope` binds child task lifetimes to a scope — inspired by
-Kotlin's `coroutineScope`, Swift's `TaskGroup`, and Java's
-`StructuredTaskScope`. When the scope exits, all child tasks have
-completed or been canceled:
+A raid sends heroes to scout different rooms. If anyone falls, the
+raid retreats. `AsyncScope` binds child task lifetimes to a scope:
 
 [source,groovy]
 ----
-async raidDungeon(List<Hero> party, List<Room> rooms) {
-    try(var scope = AsyncScope.create()) {
+def raidDungeon(List<Hero> party, List<Room> rooms) {
+    AsyncScope.withScope { scope ->
         var missions = unique(party, rooms).collect { hero, room ->
-            scope.async { await hero.scout(room) }
+            scope.async { hero.scout(room) }
         }
         missions.collect { await it }  // all loot gathered
     }
 }
 ----
 
-By default, `AsyncScope` uses fail-fast semantics: if any hero's
-scouting task throws (the hero falls), sibling tasks are cancelled
-immediately — the raid retreats.
+By default, `AsyncScope` uses **fail-fast** semantics: if any task
+fails, siblings are cancelled immediately. The scope guarantees all
+children have completed when `withScope` returns.
 
-=== Cancellation and timeouts
+==== Timeouts
 
-Cancellation is one of the trickiest parts of async programming —
-and one of `CompletableFuture`'s biggest pain points. The proposal
-makes it straightforward. For instance, a raid might have a time
-limit — if the party takes too long, all scouting missions are
-cancelled:
+A raid with a time limit:
 
 [source,groovy]
 ----
-async raidWithTimeLimit(List<Hero> party, List<Room> rooms) {
+def raidWithTimeLimit(List<Hero> party, List<Room> rooms) {
     try {
-        await Awaitable.orTimeout(raidDungeon(party, rooms), 30, SECONDS)
+        await Awaitable.orTimeoutMillis(
+            async { raidDungeon(party, rooms) }, 30_000)
     } catch (TimeoutException e) {
         party.each { it.retreat() }
-        return []  // no loot this time
+        return []
     }
 }
 ----
 
-When the timeout fires, the scope's child tasks are cancelled and
-a `TimeoutException` is thrown — which you handle with an ordinary
-`catch`, just like any other error.
+Or with a fallback value:
 
-In simple cases, you can also use `completeOnTimeout`:
+[source,groovy]
+----
+var loot = await Awaitable.completeOnTimeoutMillis(
+    async { raidDungeon(heroes, rooms) }, ['an old boot'], 30_000)
+----
 
+==== Complementing JDK structured concurrency
+
+`AsyncScope` shares the same design goals as Java's
+`StructuredTaskScope` but adds:
+
+* **`async`/`await` integration** — `scope.async { ... }` and
+`await` instead of `fork()` + `join()`.
+* **Works on JDK 17+** — uses `ThreadLocal` (virtual threads on 21+).
+* **Composes with other features** — `defer`, `for await`, channels,
+and combinators all work inside a scope.
+* **Groovy-idiomatic API** — `AsyncScope.withScope { scope -> … }`
+with a closure, no `try`-with-resources boilerplate.
+
+=== Framework adapters
+
+`await` natively understands `CompletableFuture`, `CompletionStage`,
+`Future`, and any type with a registered `AwaitableAdapter`.
+
+Drop-in adapter modules are provided:
+
+* **`groovy-reactor`** — `await` on `Mono`, `for await` over `Flux`
+* **`groovy-rxjava`** — `await` on `Single`/`Maybe`/`Completable`,
+`for await` over `Observable`/`Flowable`
+
+Without the adapter:
 [source,groovy]
 ----
-var boobyPrize = ['an old boot']
-var loot = await Awaitable.completeOnTimeout(raidDungeon(heroes, rooms), 
boobyPrize, 30, SECONDS)
-----
-
-=== Complementing JDK structured concurrency
-
-Java's `StructuredTaskScope`
-(https://openjdk.org/jeps/525[JEP 525], previewing since JDK 21)
-brings structured concurrency to the platform. `AsyncScope` shares
-the same design goals — child lifetimes bounded by a parent scope,
-automatic cancellation on failure — but layers additional value
-on top:
-
-* **`async`/`await` integration.** JDK scopes use `fork()` and
-  `join()` as separate steps; `AsyncScope` uses `scope.async { ... }`
-  and `await`, keeping scoped work consistent with the rest of
-  your async code.
-* **Works on JDK 17+.** `StructuredTaskScope` requires JDK 21+ and
-  is still a preview API. `AsyncScope` runs on JDK 17+ (using
-  `ThreadLocal` fallback) and uses `ScopedValue` when available on
-  JDK 25+.
-* **Composes with other async features.** Inside a scope you can
-  use `defer` for cleanup, `for await` to consume streams, channels
-  for inter-task communication, and `Awaitable.all`/`any` for
-  coordination — all within the same structured lifetime guarantee.
-* **Groovy-idiomatic API.** `AsyncScope.withScope { scope -> … }` uses
-  a closure, avoiding the `try`-with-resources boilerplate of Java's
-  `scope.open()` / `scope.close()`.
-
-Think of `AsyncScope` as Groovy's opinionated take on the same
-principle: structured concurrency is the safety net, and
-`async`/`await` is the ergonomic surface you interact with daily.
-
-== Example 7: game event streams — framework integration
-
-Many game backends already use reactive frameworks. The `await`
-keyword natively understands `CompletableFuture`,
-`CompletionStage`, `Future`, and `Flow.Publisher`. For third-party
-frameworks, drop-in adapter modules are auto-discovered via
-`ServiceLoader`.
-
-Here, heroes might asynchronously gain boosts in power (_buff_), and we might 
be able to stream villain alerts from a dungeon's alert feed. With the 
appropriate adapters on the classpath, we can `await` Reactor's `Mono` and 
`Flux` or RxJava's `Single` and `Observable` directly:
+def result = 
Single.just('hello').toCompletionStage().toCompletableFuture().join()
+----
 
+With `groovy-rxjava` on the classpath:
 [source,groovy]
 ----
-// With groovy-reactor on the classpath:
-async heroBoosts(heroId) {
-    var hero = await Mono.just(fetchHero(heroId))
-    for await (boost in Flux.from(hero.activeBoostStream())) {
-        hero.applyBoost(boost)
-    }
-}
+def result = await Awaitable.from(Single.just('hello'))
+----
 
-// With groovy-rxjava on the classpath:
-async villainAlerts(dungeonId) {
-    var dungeon = await Single.just(loadDungeon(dungeonId))
-    for await (alert in Observable.from(dungeon.alertFeed())) {
-        broadcastToParty(alert)
-    }
-}
+== Best practices
+
+=== Prefer returning values over shared mutation
+
+Async closures run on separate threads. Mutating shared variables
+is a race condition:
+
+[source,groovy]
+----
+// UNSAFE
+var count = 0
+def tasks = (1..100).collect { async { count++ } }
+tasks.each { await it }
+// count may not be 100!
 ----
 
-No manual adapter registration is needed — add the dependency and
-`await` works transparently with Reactor and RxJava types.
+Return values and collect results instead:
+
+[source,groovy]
+----
+// SAFE
+def tasks = (1..100).collect { n -> async { n } }
+def results = await Awaitable.all(*tasks)
+assert results.sum() == 5050
+----
+
+When shared mutable state is unavoidable, use the appropriate
+concurrency-aware type — `AtomicInteger` for a shared counter,
+or thread-safe types from `java.util.concurrent`.
+
+=== Choosing the right tool
+
+[cols="2,3", options="header"]
+|===
+| Feature | Use when...
+
+| `async`/`await`
+| Sequential steps with I/O or blocking work.
+
+| `Awaitable.all` / `any` / `first`
+| Launch independent tasks, collect all, race them, or take first success.
+
+| `yield return` / `for await`
+| Producing or consuming a stream of values.
+
+| `defer`
+| Guaranteed cleanup without nested `try`/`finally`.
+
+| `AsyncChannel`
+| Producer/consumer communication between tasks.
+
+| `AsyncScope`
+| Child task lifetimes tied to a scope with fail-fast cancellation.
+
+| Framework adapters
+| Transparent `await` / `for await` with Reactor or RxJava types.
+|===
 
 == How it relates to GPars and virtual threads
 
 Readers of the
 https://groovy.apache.org/blog/gpars-meets-virtual-threads[GPars meets virtual 
threads]
 blog post will recall that GPars provides parallel collections,
-actors, agents, and dataflow concurrency — and that it works well
-with virtual threads via custom executor services.
+actors, agents, and dataflow concurrency.
 
-The async/await proposal complements GPars rather than replacing
-it. GPars excels at data-parallel operations (`collectParallel`,
-`findAllParallel`) and actor-based designs. Async/await targets a
-different sweet spot: sequential-looking code that is actually
+Async/await complements GPars rather than replacing it. GPars
+excels at data-parallel operations and actor-based designs.
+Async/await targets sequential-looking code that is actually
 asynchronous, with language-level support for streams, cleanup,
-structured concurrency, and framework bridging. If you're calling
-microservices, paginating through APIs, or coordinating I/O-bound
-tasks, async/await gives you a concise way to express that without
-dropping into callback chains.
+structured concurrency, and framework bridging.
 
 Both approaches benefit from virtual threads on JDK 21+, and
 both can coexist in the same codebase.
 
-== The full picture
-
-The examples above are only a taste. The complete proposal also includes
-async closures and lambdas, the `@Async` annotation (for Java-style
-declarations), other `Awaitable` combinators (`any`, `allSettled`, `delay`),
-more details about `AsyncContext` for propagating trace and tenant metadata 
across
-thread hops, cancellation support, and a pluggable adapter registry for
-custom async types. The full spec is available in the
-https://github.com/apache/groovy/blob/GROOVY-9381_3/src/spec/doc/core-async-await.adoc[draft
 documentation].
-
-== We'd love your feedback
-
-The async/await feature is currently a proposal in
-https://github.com/apache/groovy/pull/2387[PR #2387]
-(tracking issue
-https://issues.apache.org/jira/browse/GROOVY-9381[GROOVY-9381]).
-This is a substantial addition to the language and we want to get
-it right.
-
-* *Comment* on the https://github.com/apache/groovy/pull/2387[PR] or
-  the https://issues.apache.org/jira/browse/GROOVY-9381[JIRA issue]
-  with your thoughts, use cases, or design suggestions.
-* *Vote* on the JIRA issue if you'd like to see this feature land.
-
-Your feedback helps us gauge interest and shape the final design.
-
 == Conclusion
 
-Through our _Groovy Quest_ examples we've seen how the proposed
-async/await feature lets you write async Groovy code that reads
-almost like synchronous code — from loading a hero's quest, to
-preparing a battle in parallel, streaming dungeon waves, cleaning
-up summoned familiars, coordinating a boss fight over channels,
-and rallying a raid party with structured concurrency. The syntax
-is concise, the mental model is straightforward, and virtual
-threads make it scale.
+Through our _Groovy Quest_ examples we've seen how async/await lets
+you write concurrent code that reads like synchronous code — from
+loading a hero's quest, to preparing a battle in parallel, streaming
+dungeon waves, cleaning up summoned familiars, coordinating a boss
+fight over channels, and rallying a raid party with structured
+concurrency.
+
+The design philosophy is simple: closures run on real threads (virtual
+when available), stack traces are preserved, exceptions propagate
+naturally, and there's no function coloring. The caller decides what's
+concurrent — not the method signature.
 
 == References
 
-* https://github.com/apache/groovy/pull/2387[PR #2387 — Async/await support]
 * https://issues.apache.org/jira/browse/GROOVY-9381[GROOVY-9381 — Tracking 
issue]
-* 
https://github.com/apache/groovy/blob/GROOVY-9381_3/src/spec/doc/core-async-await.adoc[Draft
 spec documentation]
-* https://openjdk.org/jeps/525[JEP 525 — Structured Concurrency (Sixth 
Preview)]
+* https://openjdk.org/jeps/525[JEP 525 — Structured Concurrency]
 * https://groovy.apache.org/blog/gpars-meets-virtual-threads[GPars meets 
Virtual Threads]
 * http://gpars.org/[GPars]
+
+.Update history
+****
+*27/Mar/2026*: Initial version. +
+*06/Apr/2026*: Revised version after feedback including numerous 
simplifications.
+****
diff --git a/site/src/site/blog/groovy-async-await_5.adoc 
b/site/src/site/blog/groovy-async-await_5.adoc
deleted file mode 100644
index a3b2dfa..0000000
--- a/site/src/site/blog/groovy-async-await_5.adoc
+++ /dev/null
@@ -1,443 +0,0 @@
-= Async/await for Groovy&trade;
-Paul King <paulk-asert|PMC_Member>
-:revdate: 2026-04-03T10:00:00+00:00
-:draft: true
-:keywords: async, await, concurrency, virtual-threads
-:description: This post introduces Groovy's simplified async/await feature — 
write concurrent code that reads like synchronous code, with virtual thread 
support, generators, channels, and structured concurrency.
-
-== Introduction
-
-Groovy 6 adds native `async`/`await` as a language-level feature
-(https://issues.apache.org/jira/browse/GROOVY-9381[GROOVY-9381]).
-Write asynchronous code in a sequential, readable style —
-with support for generators, deferred cleanup, Go-style channels,
-structured concurrency, and framework adapters for Reactor and RxJava.
-
-On JDK 21+, async tasks automatically leverage
-https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#ofVirtual()[virtual
 threads]
-for optimal scalability. On JDK 17–20, a cached thread pool provides
-correct behavior as a fallback.
-
-To make the features concrete, the examples follow a running theme:
-building the backend for _Groovy Quest_, a fictitious online game
-where heroes battle villains across dungeons.
-
-== Getting started
-
-=== The problem: callback complexity
-
-A player logs in and we need to load their quest: look up
-their hero ID, fetch the hero's class, then load their active quest.
-With `CompletableFuture` the logic gets buried under plumbing:
-
-[source,java]
-----
-// Java with CompletableFuture
-CompletableFuture<Quest> quest =
-    lookupHeroId(loginToken)
-        .thenCompose(id -> fetchHeroClass(id))
-        .thenCompose(heroClass -> loadActiveQuest(heroClass))
-        .exceptionally(e -> Quest.DEFAULT);
-----
-
-Each `.thenCompose()` adds a nesting level, exception recovery is
-separated from the code that causes it, and the control flow reads
-inside-out.
-
-=== Loading a hero — reads like synchronous code
-
-With `async`/`await`, the same logic becomes:
-
-[source,groovy]
-----
-Quest loadHeroQuest(String loginToken) {
-    var heroId    = await lookupHeroId(loginToken)
-    var heroClass = await fetchHeroClass(heroId)
-    return await loadActiveQuest(heroClass)
-}
-----
-
-Variables are declared at the point of use. The return value is obvious.
-No callbacks, no lambdas, no chained combinators. The method is a
-regular method — the caller decides whether to run it asynchronously:
-
-[source,groovy]
-----
-// Run asynchronously:
-def quest = await async { loadHeroQuest(token) }
-
-// Or call directly (blocking — fine on virtual threads):
-def quest = loadHeroQuest(token)
-----
-
-=== Exception handling — just `try`/`catch`
-
-What about the `.exceptionally(e -> Quest.DEFAULT)` fallback from
-the Java version?
-
-[source,groovy]
-----
-Quest loadHeroQuest(String loginToken) {
-    try {
-        var heroId    = await lookupHeroId(loginToken)
-        var heroClass = await fetchHeroClass(heroId)
-        return await loadActiveQuest(heroClass)
-    } catch (NoActiveQuestException e) {
-        return Quest.DEFAULT
-    }
-}
-----
-
-`await` unwraps `CompletionException` automatically, so you catch
-the _original_ exception type. Error handling reads exactly like
-synchronous code.
-
-== Running tasks in parallel
-
-=== Preparing for battle — `Awaitable.all`
-
-Before a battle, the game loads the hero's stats, inventory, and
-the villain — all in parallel:
-
-[source,groovy]
-----
-def prepareBattle(heroId, visibleVillainId) {
-    var stats     = async { fetchHeroStats(heroId) }
-    var inventory = async { fetchInventory(heroId) }
-    var villain   = async { fetchVillain(visibleVillainId) }
-
-    var (s, inv, v) = await Awaitable.all(stats, inventory, villain)
-    return new BattleScreen(s, inv, v)
-}
-----
-
-Each `async { ... }` starts immediately on a background thread.
-The `await stats, inventory, villain` expression waits for all three
-to complete — it's shorthand for `await Awaitable.all(stats, inventory, 
villain)`.
-Parentheses also work: `await(stats, inventory, villain)`.
-
-==== How this compares to Java's `StructuredTaskScope`
-
-Java's structured concurrency preview
-(https://openjdk.org/jeps/525[JEP 525]) provides a similar
-capability:
-
-[source,java]
-----
-// Java with StructuredTaskScope (JDK 25 preview API)
-try (var scope = StructuredTaskScope.open()) {
-    var statsTask     = scope.fork(() -> fetchHeroStats(heroId));
-    var inventoryTask = scope.fork(() -> fetchInventory(heroId));
-    var villainTask   = scope.fork(() -> fetchVillain(villainId));
-    scope.join();
-    return new BattleScreen(
-        statsTask.get(), inventoryTask.get(), villainTask.get());
-}
-----
-
-Both approaches bind task lifetimes to a scope. Groovy adds syntactic
-sugar (`await`, `all`) and integrates with the same model used
-everywhere else, whereas Java's API is deliberately lower-level.
-Groovy's `AsyncScope` (covered later) brings the full structured
-concurrency model.
-
-=== Capture the flag — `Awaitable.any`
-
-Where `all` waits for _every_ task, `any` returns the _first_ to
-complete — a race:
-
-[source,groovy]
-----
-def captureTheFlag(hero, villain, flag) {
-    var heroGrab    = async { hero.grab(flag) }
-    var villainGrab = async { villain.grab(flag) }
-
-    var winner = await Awaitable.any(heroGrab, villainGrab)
-    println "$winner.name captured the flag!"
-}
-----
-
-The loser's task still runs to completion in the background
-(use `AsyncScope` for fail-fast cancellation).
-
-=== Other combinators
-
-* **`Awaitable.first(a, b, c)`** — returns the first _successful_
-  result, ignoring individual failures. Like JavaScript's
-  `Promise.any()`. Useful for hedged requests and graceful degradation.
-* **`Awaitable.allSettled(a, b)`** — waits for all tasks to settle
-  (succeed or fail) without throwing. Returns an `AwaitResult` list
-  with `success`, `value`, and `error` fields.
-
-== Generators and streaming
-
-=== Dungeon waves — `yield return` and `for await`
-
-A dungeon sends waves of enemies. Each wave is generated on demand
-and the hero fights them as they arrive:
-
-[source,groovy]
-----
-def generateWaves(String dungeonId) {
-    async {
-        var depth = 1
-        while (depth <= dungeonDepth(dungeonId)) {
-            yield return spawnEnemies(dungeonId, depth)
-            depth++
-        }
-    }
-}
-
-def runDungeon(hero, dungeonId) {
-    for await (wave in generateWaves(dungeonId)) {
-        wave.each { villain -> hero.fight(villain) }
-    }
-}
-----
-
-The producer yields each wave on demand. The consumer pulls with
-`for await`. Natural *back-pressure* — the producer blocks on each
-`yield return` until the consumer is ready. No queues, signals, or
-synchronization.
-
-Since generators return a standard `Iterable`, regular `for` loops
-and Groovy collection methods (`collect`, `findAll`, `take`) also
-work — `for await` is optional for generators but required for
-reactive types (Flux, Observable).
-
-== Deferred cleanup — `defer`
-
-Before entering a dungeon, the hero summons a familiar and opens a
-portal. Both must be cleaned up when the quest ends. `defer` schedules
-cleanup in LIFO order, like
-https://go.dev/blog/defer-panic-and-recover[Go's `defer`]:
-
-[source,groovy]
-----
-def enterDungeon(hero, dungeonId) {
-    def task = async {
-        var familiar = hero.summonFamiliar()
-        defer familiar.dismiss()
-
-        var portal = openPortal(dungeonId)
-        defer portal.close()
-
-        hero.explore(portal, familiar)
-    }
-    await task
-}
-----
-
-Deferred actions always run — even when an exception occurs.
-This is cleaner than nested `try`/`finally` blocks when multiple
-resources are acquired at different points.
-
-== Diving deeper
-
-=== Channels — the villain spawner
-
-In a boss fight, a villain factory spawns enemies while the hero
-fights them. Channels provide Go-style decoupled communication:
-
-[source,groovy]
-----
-def bossFight(hero, bossArena) {
-    var enemies = AsyncChannel.create(3)  // buffered channel
-
-    // Villain spawner — runs concurrently
-    async {
-        for (type in bossArena.spawnOrder) {
-            await enemies.send(new Villain(type))
-        }
-        enemies.close()
-    }
-
-    // Hero fights each enemy as it arrives
-    var xp = 0
-    for await (villain in enemies) {
-        xp += hero.fight(villain)
-    }
-    return xp
-}
-----
-
-Channels support unbuffered (rendezvous) and buffered modes.
-`for await` iterates until the channel is closed and drained.
-Channels implement `Iterable`, so regular `for` loops work too.
-
-=== Structured concurrency — the raid party
-
-A raid sends heroes to scout different rooms. If anyone falls, the
-raid retreats. `AsyncScope` binds child task lifetimes to a scope:
-
-[source,groovy]
-----
-def raidDungeon(List<Hero> party, List<Room> rooms) {
-    AsyncScope.withScope { scope ->
-        var missions = unique(party, rooms).collect { hero, room ->
-            scope.async { hero.scout(room) }
-        }
-        missions.collect { await it }  // all loot gathered
-    }
-}
-----
-
-By default, `AsyncScope` uses **fail-fast** semantics: if any task
-fails, siblings are cancelled immediately. The scope guarantees all
-children have completed when `withScope` returns.
-
-==== Timeouts
-
-A raid with a time limit:
-
-[source,groovy]
-----
-def raidWithTimeLimit(List<Hero> party, List<Room> rooms) {
-    try {
-        await Awaitable.orTimeoutMillis(
-            async { raidDungeon(party, rooms) }, 30_000)
-    } catch (TimeoutException e) {
-        party.each { it.retreat() }
-        return []
-    }
-}
-----
-
-Or with a fallback value:
-
-[source,groovy]
-----
-var loot = await Awaitable.completeOnTimeoutMillis(
-    async { raidDungeon(heroes, rooms) }, ['an old boot'], 30_000)
-----
-
-==== Complementing JDK structured concurrency
-
-`AsyncScope` shares the same design goals as Java's
-`StructuredTaskScope` but adds:
-
-* **`async`/`await` integration** — `scope.async { ... }` and
-  `await` instead of `fork()` + `join()`.
-* **Works on JDK 17+** — uses `ThreadLocal` (virtual threads on 21+).
-* **Composes with other features** — `defer`, `for await`, channels,
-  and combinators all work inside a scope.
-* **Groovy-idiomatic API** — `AsyncScope.withScope { scope -> … }`
-  with a closure, no `try`-with-resources boilerplate.
-
-=== Framework adapters
-
-`await` natively understands `CompletableFuture`, `CompletionStage`,
-`Future`, and any type with a registered `AwaitableAdapter`.
-
-Drop-in adapter modules are provided:
-
-* **`groovy-reactor`** — `await` on `Mono`, `for await` over `Flux`
-* **`groovy-rxjava`** — `await` on `Single`/`Maybe`/`Completable`,
-  `for await` over `Observable`/`Flowable`
-
-Without the adapter:
-[source,groovy]
-----
-def result = 
Single.just('hello').toCompletionStage().toCompletableFuture().join()
-----
-
-With `groovy-rxjava` on the classpath:
-[source,groovy]
-----
-def result = await Awaitable.from(Single.just('hello'))
-----
-
-== Best practices
-
-=== Prefer returning values over shared mutation
-
-Async closures run on separate threads. Mutating shared variables
-is a race condition:
-
-[source,groovy]
-----
-// UNSAFE
-var count = 0
-def tasks = (1..100).collect { async { count++ } }
-tasks.each { await it }
-// count may not be 100!
-----
-
-Return values and collect results instead:
-
-[source,groovy]
-----
-// SAFE
-def tasks = (1..100).collect { n -> async { n } }
-def results = await Awaitable.all(*tasks)
-assert results.sum() == 5050
-----
-
-When shared mutable state is unavoidable, use the appropriate
-concurrency-aware type — `AtomicInteger` for a shared counter,
-or thread-safe types from `java.util.concurrent`.
-
-=== Choosing the right tool
-
-[cols="2,3", options="header"]
-|===
-| Feature | Use when...
-
-| `async`/`await`
-| Sequential steps with I/O or blocking work.
-
-| `Awaitable.all` / `any` / `first`
-| Launch independent tasks, collect all, race them, or take first success.
-
-| `yield return` / `for await`
-| Producing or consuming a stream of values.
-
-| `defer`
-| Guaranteed cleanup without nested `try`/`finally`.
-
-| `AsyncChannel`
-| Producer/consumer communication between tasks.
-
-| `AsyncScope`
-| Child task lifetimes tied to a scope with fail-fast cancellation.
-
-| Framework adapters
-| Transparent `await` / `for await` with Reactor or RxJava types.
-|===
-
-== How it relates to GPars and virtual threads
-
-Readers of the
-https://groovy.apache.org/blog/gpars-meets-virtual-threads[GPars meets virtual 
threads]
-blog post will recall that GPars provides parallel collections,
-actors, agents, and dataflow concurrency.
-
-Async/await complements GPars rather than replacing it. GPars
-excels at data-parallel operations and actor-based designs.
-Async/await targets sequential-looking code that is actually
-asynchronous, with language-level support for streams, cleanup,
-structured concurrency, and framework bridging.
-
-Both approaches benefit from virtual threads on JDK 21+, and
-both can coexist in the same codebase.
-
-== Conclusion
-
-Through our _Groovy Quest_ examples we've seen how async/await lets
-you write concurrent code that reads like synchronous code — from
-loading a hero's quest, to preparing a battle in parallel, streaming
-dungeon waves, cleaning up summoned familiars, coordinating a boss
-fight over channels, and rallying a raid party with structured
-concurrency.
-
-The design philosophy is simple: closures run on real threads (virtual
-when available), stack traces are preserved, exceptions propagate
-naturally, and there's no function coloring. The caller decides what's
-concurrent — not the method signature.
-
-== References
-
-* https://issues.apache.org/jira/browse/GROOVY-9381[GROOVY-9381 — Tracking 
issue]
-* https://openjdk.org/jeps/525[JEP 525 — Structured Concurrency]
-* https://groovy.apache.org/blog/gpars-meets-virtual-threads[GPars meets 
Virtual Threads]
-* http://gpars.org/[GPars]


Reply via email to