[
https://issues.apache.org/jira/browse/GROOVY-9381?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-9381:
------------------------------
Description:
h2. Summary
Introduce first-class {{async}}/{{await}} language support to Groovy,
enabling developers to write asynchronous code in a sequential, readable style
— on par with the async/await facilities in JavaScript (ES2017), C# (5.0),
Kotlin (coroutines), and Swift
(5.5).
h2. Motivation
Modern JVM applications are overwhelmingly concurrent. Web services, data
pipelines, and reactive systems spend most of their time waiting for network
I/O, database queries, or downstream services. The JVM offers powerful but
low-level concurrency
primitives ({{CompletableFuture}}, {{Flow.Publisher}}, {{ExecutorService}}),
and while libraries like RxJava and Project Reactor raise the abstraction
level, they introduce their own learning curve and cannot alter the language's
control-flow syntax.
Today, a typical three-step async workflow in Groovy looks like this:
{code:groovy}
CompletableFuture.supplyAsync { fetchUserId() }
.thenCompose { id -> CompletableFuture.supplyAsync { lookupName(id) } }
.thenCompose { name -> CompletableFuture.supplyAsync { loadProfile(name)
} }
.exceptionally { ex -> handleError(ex) }
{code}
The business logic is obscured by plumbing. Exception handling is decoupled
from the code that raises exceptions, and the control flow reads inside-out.
With {{async}}/{{await}}, the same logic becomes:
{code:groovy}
def profile = async {
def id = await fetchUserIdAsync()
def name = await lookupNameAsync(id)
return await loadProfileAsync(name)
}
{code}
This reads identically to synchronous code. Standard {{try}}/{{catch}},
{{for}}, {{if}}, and variable assignment all compose naturally — no callbacks,
no chained lambdas.
h2. Scope
This proposal introduces the following language constructs and runtime APIs:
h3. Language Constructs
||Construct||Syntax||Description||
|Async closure|async \{ ... \}|Starts a closure on a background thread,
returning an {{Awaitable}} (or {{Iterable}} when the body contains {{yield
return}})|
|Await expression|await expr / await(expr)|Blocks until the awaited
computation completes; transparently unwraps the result|
|Multi-arg await|await(a, b, c) \\ await a, b, c|Syntactic sugar for
{{Awaitable.all(a, b, c)}}|
|For-await loop|for await (item in source) \{ ... \}|Iterates over an async
source (generator, channel, reactive type), with automatic resource cleanup via
try-finally|
|Yield return|yield return expr|Emits a value from an async generator
closure, producing an {{Iterable}} with natural back-pressure|
|Defer|defer \{ cleanup \} \\ defer cleanup|Schedules a cleanup block to
execute on closure exit (LIFO order), inspired by Go's {{defer}}|
h3. Public API ({{groovy.concurrent}} package)
||Class/Interface||Role||
|{{Awaitable}}|Core promise abstraction (analogous to C#'s {{Task}} / JS's
{{Promise}}). Provides static combinators ({{all}}, {{any}}, {{first}},
{{allSettled}}, {{delay}}, {{orTimeout}}, {{completeOnTimeout}}), factories
({{of}}, {{failed}}, {{from}},
{{go}}), and instance continuation methods ({{then}}, {{thenCompose}},
{{thenAccept}}, {{exceptionally}}, {{whenComplete}}, {{handle}}, {{orTimeout}},
{{completeOnTimeout}})|
|{{AsyncChannel}}|Go-style inter-task communication channel with optional
buffering. Supports unbuffered (rendezvous) and buffered modes. Implements
{{Iterable}}, so works with {{for await}} and regular {{for}} loops|
|{{AsyncScope}}|Structured concurrency scope — binds child task lifetimes to
a scope with fail-fast cancellation. All children are guaranteed complete (or
cancelled) before the scope exits|
|{{AwaitResult}}|Outcome wrapper returned by {{allSettled()}} — carries
either a success value or a failure throwable|
|{{AwaitableAdapter}}|SPI interface for adapting third-party async types
(RxJava, Reactor, etc.) to {{Awaitable}}|
|{{AwaitableAdapterRegistry}}|Central adapter registry with {{ServiceLoader}}
auto-discovery and runtime registration|
|{{ChannelClosedException}}|Thrown when sending to or receiving from a closed
channel|
h3. Internal Runtime ({{org.apache.groovy.runtime.async}} package)
||Class||Role||
|{{AsyncSupport}}|Central runtime entry point — all {{await}} overloads,
{{async}} execution, {{defer}} scope management, combinator implementation,
timeout scheduling, {{yield return}} dispatch, channel and scope support|
|{{GroovyPromise}}|Default {{Awaitable}} implementation backed by
{{CompletableFuture}}. Sole bridge between the public API and JDK async
infrastructure|
|{{GeneratorBridge}}|Producer/consumer bridge for async generators ({{yield
return}}). Uses {{SynchronousQueue}} for zero-buffered back-pressure with
cooperative cancellation via thread tracking. Implements {{Iterator}} and
{{Closeable}}|
|{{DefaultAsyncChannel}}|Default {{AsyncChannel}} implementation with
buffered and unbuffered modes|
|{{DefaultAsyncScope}}|Default {{AsyncScope}} implementation with fail-fast
child cancellation|
h2. Design Principles
Readability first. Async code should be visually indistinguishable from
synchronous code. All standard Groovy control-flow constructs
({{try}}/{{catch}}, {{for}}, {{if}}/{{else}}, closures) must work naturally
inside async closures.
Exception transparency. {{await}} automatically unwraps
{{CompletionException}}, {{ExecutionException}}, and other JVM wrapper layers.
The original exception type, message, and stack trace are preserved — callers
see the same exceptions they would in
synchronous code.
API decoupling. User code depends on {{Awaitable}}, not on
{{CompletableFuture}}. The public API ({{groovy.concurrent}}) is separated from
the internal implementation ({{org.apache.groovy.runtime.async}}). If the JDK's
async infrastructure evolves (e.g.,
structured concurrency), only the internal layer changes.
Minimal grammar footprint. {{async}}, {{await}}, {{defer}}, and {{yield}} are
contextual keywords — they remain valid identifiers in non-async contexts,
preserving backward compatibility. Grammar changes to {{GroovyLexer.g4}} and
{{GroovyParser.g4}} are
minimal.
Thread safety is the framework's responsibility. All concurrency control
(atomics, volatile, CAS) is encapsulated in the runtime. Application code never
needs explicit locks, synchronization, or volatile annotations.
JVM ecosystem integration. Built-in adapters handle {{CompletableFuture}},
{{CompletionStage}}, and {{Future}} out of the box. Third-party frameworks
integrate via the {{AwaitableAdapterRegistry}} SPI.
h2. Execution Model
On {*}JDK 21+{*}, each {{async}} closure runs on a virtual thread. When the
thread blocks on {{await}}, the JVM parks the virtual thread and releases the
carrier (OS) thread. This achieves the practical scalability of
compiler-generated state machines (as
in C# and Kotlin) without requiring control-flow rewriting — stack traces
remain complete, and standard debuggers work unmodified.
On {*}JDK 17–20{*}, a bounded cached thread pool (default 256, configurable
via {{groovy.async.parallelism}}) with a caller-runs back-pressure policy
provides stable performance.
The executor is fully configurable at runtime via
{{Awaitable.setExecutor(executor)}}.
h2. Key Features in Detail
h3. Combinators
||Method||Analogy||Behavior||
|{{Awaitable.all(a, b, c)}}|{{Promise.all()}} \\ {{Task.WhenAll()}}|Waits for
all to succeed; fails immediately on first error (fail-fast)|
|{{Awaitable.any(a, b)}}|{{Promise.any()}}|Returns the first to complete
(success or failure)|
|{{Awaitable.first(a, b, c)}}|{{Promise.race()}} \\
{{Task.WhenAny()}}|Returns the first to succeed; fails only when all sources
fail (aggregate error)|
|{{Awaitable.allSettled(a, b)}}|{{Promise.allSettled()}}|Waits for all to
complete (success or fail); captures outcomes in {{AwaitResult}} list without
throwing|
|{{Awaitable.delay(ms)}}|{{Task.Delay()}} \\ {{setTimeout}}|Non-blocking
timer|
|{{Awaitable.orTimeoutMillis(source, ms)}}|{{withTimeout}}|Fails with
{{TimeoutException}} on expiry|
|{{Awaitable.completeOnTimeoutMillis(source, fallback, ms)}}|—|Completes with
fallback value on expiry|
h3. Async Generators and Back-Pressure
Closures containing {{yield return}} produce an {{Iterable}}, consumable via
{{for await}} or a regular {{for}} loop. The producer and consumer coordinate
through the {{GeneratorBridge}}, which uses a {{SynchronousQueue}} — the
producer blocks on each
{{yield return}} until the consumer pulls the next element, providing natural
back-pressure without unbounded buffering. If the consumer exits early
({{break}}, {{return}}, exception), the producer thread is interrupted via
cooperative cancellation,
preventing resource leaks. {{for await}} wraps the loop in a try-finally to
ensure generator cleanup.
h3. Channels (Go-Style Inter-Task Communication)
{{AsyncChannel}} provides CSP-style communication between tasks. A producer
sends values into a channel; a consumer receives them. The channel handles
synchronization and optional buffering — no shared mutable state needed.
Channels support unbuffered
(rendezvous, {{create()}}) and buffered ({{create\(n)}}) modes. Sending
blocks when the buffer is full; receiving blocks when empty. Since channels
implement {{Iterable}}, they work with {{for await}}, regular {{for}} loops,
and Groovy collection methods.
h3. Structured Concurrency
{{AsyncScope}} binds the lifetime of child tasks to a scope. When the scope
exits, all children are guaranteed complete (or cancelled). This prevents
orphaned tasks and silent failures. By default, the scope uses fail-fast
semantics — if any child fails,
all siblings are cancelled immediately. On JDK 25+, scope tracking uses
{{ScopedValue}} for optimal virtual thread performance; on JDK 17–24, a
{{ThreadLocal}} fallback is used transparently.
h3. Defer (Go-Style Cleanup)
The {{defer}} keyword schedules cleanup actions that execute in LIFO order
when the enclosing async closure returns, regardless of success or failure. If
multiple deferred blocks throw, the first exception is primary and subsequent
ones are attached via
{{addSuppressed()}}. If a deferred action returns an {{Awaitable}} or
{{Future}}, the result is awaited before the next deferred action runs,
ensuring orderly cleanup of asynchronous resources. This provides deterministic
resource cleanup without deeply
nested {{try}}/{{finally}} blocks.
h3. Adapter Registry (Third-Party Integration)
The {{AwaitableAdapterRegistry}} is an SPI-based extension point. Adapters
can be registered:
- At class-load time via {{ServiceLoader}}
({{META-INF/services/groovy.concurrent.AwaitableAdapter}})
- At runtime via {{AwaitableAdapterRegistry.register(adapter)}}
This enables {{await}} to work transparently with RxJava
{{Single}}/{{Observable}}, Reactor {{Mono}}/{{Flux}}, or any custom async type
— a single {{await}} keyword, regardless of the underlying library. Drop-in
adapter modules are provided for:
- groovy-reactor — {{await}} on {{Mono}}, {{for await}} over {{Flux}}
- groovy-rxjava — {{await}} on {{Single}}/{{Maybe}}/{{Completable}}, {{for
await}} over {{Observable}}/{{Flowable}}
h2. Thread Safety Mechanisms
All concurrency control is internal and transparent to users:
- Lock-free synchronization — {{volatile}} fields, {{AtomicInteger}},
{{AtomicReference}}, {{CopyOnWriteArrayList}} used throughout; no
{{synchronized}} blocks in the async runtime
- TOCTOU prevention — {{GeneratorBridge.yield()}} sets {{producerThread}}
before checking the {{closed}} flag, then re-checks after blocking operations,
closing a race window with concurrent {{close()}}
- Cooperative cancellation — {{GeneratorBridge.close()}} atomically sets a
closed flag, drains any pending handoff, and interrupts the producer thread
- Idempotent close — All close operations are safe to call multiple times
from any thread
h2. Cross-Language Comparison
||Aspect||Groovy||JavaScript||C#||Kotlin||Swift||
|Async declaration|async \{ ... \}|{{async function foo()}}|{{async Task
Foo()}}|{{suspend fun foo(): T}}|{{func foo() async throws -> T}}|
|Await|{{await expr}}|{{await expr}}|{{await
expr}}|{{deferred.await()}}|{{try await expr}}|
|Async iteration|{{for await (x in src)}}|{{for await (x of src)}}|{{await
foreach (x in src)}}|manual ({{Flow.collect}})|{{for try await x in seq}}|
|Async generator|{{yield return expr}}|{{yield}} in {{async
function*}}|{{yield return}} in {{IAsyncEnumerable}}|flow { emit( x )
}|AsyncStream { yield( x ) }|
|Defer|{{defer}}|(none)|{{await using}}|{{use}}|{{defer}}|
|Channels|{{AsyncChannel}}|(none)|{{Channel}}|{{Channel}}|{{AsyncStream}}
(limited)|
|Structured
concurrency|{{AsyncScope}}|(none)|(none)|{{coroutineScope}}|{{TaskGroup}}|
|Implementation|Thread-per-task (VT on 21+)|Event loop|State
machine|Coroutine SM|Async SM|
h2. Backward Compatibility
- {{async}}, {{await}}, {{defer}}, and {{yield}} are contextual keywords —
they act as keywords only in specific syntactic positions and remain valid
identifiers elsewhere. Existing code using these as variable names, method
names, or field names
continues to compile and run without modification.
- No existing public APIs are modified or removed.
- The feature is purely additive: code that does not use {{async}}/{{await}}
is entirely unaffected.
was:
h2. Summary
Introduce first-class {{async}}/{{await}} language support to Groovy,
enabling developers to write asynchronous code in a sequential, readable style
— on par with the async/await facilities in JavaScript (ES2017), C# (5.0),
Kotlin (coroutines), and Swift
(5.5).
h2. Motivation
Modern JVM applications are overwhelmingly concurrent. Web services, data
pipelines, and reactive systems spend most of their time waiting for network
I/O, database queries, or downstream services. The JVM offers powerful but
low-level concurrency
primitives ({{CompletableFuture}}, {{Flow.Publisher}}, {{ExecutorService}}),
and while libraries like RxJava and Project Reactor raise the abstraction
level, they introduce their own learning curve and cannot alter the language's
control-flow syntax.
Today, a typical three-step async workflow in Groovy looks like this:
{code:groovy}
CompletableFuture.supplyAsync { fetchUserId() }
.thenCompose { id -> CompletableFuture.supplyAsync { lookupName(id) } }
.thenCompose { name -> CompletableFuture.supplyAsync { loadProfile(name)
} }
.exceptionally { ex -> handleError(ex) }
{code}
The business logic is obscured by plumbing. Exception handling is decoupled
from the code that raises exceptions, and the control flow reads inside-out.
With {{async}}/{{await}}, the same logic becomes:
{code:groovy}
def profile = async {
def id = await fetchUserIdAsync()
def name = await lookupNameAsync(id)
return await loadProfileAsync(name)
}
{code}
This reads identically to synchronous code. Standard {{try}}/{{catch}},
{{for}}, {{if}}, and variable assignment all compose naturally — no callbacks,
no chained lambdas.
h2. Scope
This proposal introduces the following language constructs and runtime APIs:
h3. Language Constructs
||Construct||Syntax||Description||
|Async closure|async \{ ... \}|Starts a closure on a background thread,
returning an {{Awaitable}} (or {{Iterable}} when the body contains {{yield
return}})|
|Await expression|await expr / await(expr)|Blocks until the awaited
computation completes; transparently unwraps the result|
|Multi-arg await|await(a, b, c) \\ await a, b, c|Syntactic sugar for
{{Awaitable.all(a, b, c)}}|
|For-await loop|for await (item in source) \{ ... \}|Iterates over an async
source (generator, channel, reactive type), with automatic resource cleanup via
try-finally|
|Yield return|yield return expr|Emits a value from an async generator
closure, producing an {{Iterable}} with natural back-pressure|
|Defer|defer \{ cleanup \} \\ defer cleanup|Schedules a cleanup block to
execute on closure exit (LIFO order), inspired by Go's {{defer}}|
h3. Public API ({{groovy.concurrent}} package)
||Class/Interface||Role||
|{{Awaitable}}|Core promise abstraction (analogous to C#'s {{Task}} / JS's
{{Promise}}). Provides static combinators ({{all}}, {{any}}, {{first}},
{{allSettled}}, {{delay}}, {{orTimeout}}, {{completeOnTimeout}}), factories
({{of}}, {{failed}}, {{from}},
{{go}}), and instance continuation methods ({{then}}, {{thenCompose}},
{{thenAccept}}, {{exceptionally}}, {{whenComplete}}, {{handle}}, {{orTimeout}},
{{completeOnTimeout}})|
|{{AsyncChannel}}|Go-style inter-task communication channel with optional
buffering. Supports unbuffered (rendezvous) and buffered modes. Implements
{{Iterable}}, so works with {{for await}} and regular {{for}} loops|
|{{AsyncScope}}|Structured concurrency scope — binds child task lifetimes to
a scope with fail-fast cancellation. All children are guaranteed complete (or
cancelled) before the scope exits|
|{{AwaitResult}}|Outcome wrapper returned by {{allSettled()}} — carries
either a success value or a failure throwable|
|{{AwaitableAdapter}}|SPI interface for adapting third-party async types
(RxJava, Reactor, etc.) to {{Awaitable}}|
|{{AwaitableAdapterRegistry}}|Central adapter registry with {{ServiceLoader}}
auto-discovery and runtime registration|
|{{ChannelClosedException}}|Thrown when sending to or receiving from a closed
channel|
h3. Internal Runtime ({{org.apache.groovy.runtime.async}} package)
||Class||Role||
|{{AsyncSupport}}|Central runtime entry point — all {{await}} overloads,
{{async}} execution, {{defer}} scope management, combinator implementation,
timeout scheduling, {{yield return}} dispatch, channel and scope support|
|{{GroovyPromise}}|Default {{Awaitable}} implementation backed by
{{CompletableFuture}}. Sole bridge between the public API and JDK async
infrastructure|
|{{GeneratorBridge}}|Producer/consumer bridge for async generators ({{yield
return}}). Uses {{SynchronousQueue}} for zero-buffered back-pressure with
cooperative cancellation via thread tracking. Implements {{Iterator}} and
{{Closeable}}|
|{{DefaultAsyncChannel}}|Default {{AsyncChannel}} implementation with
buffered and unbuffered modes|
|{{DefaultAsyncScope}}|Default {{AsyncScope}} implementation with fail-fast
child cancellation|
h2. Design Principles
Readability first. Async code should be visually indistinguishable from
synchronous code. All standard Groovy control-flow constructs
({{try}}/{{catch}}, {{for}}, {{if}}/{{else}}, closures) must work naturally
inside async closures.
Exception transparency. {{await}} automatically unwraps
{{CompletionException}}, {{ExecutionException}}, and other JVM wrapper layers.
The original exception type, message, and stack trace are preserved — callers
see the same exceptions they would in
synchronous code.
API decoupling. User code depends on {{Awaitable}}, not on
{{CompletableFuture}}. The public API ({{groovy.concurrent}}) is separated from
the internal implementation ({{org.apache.groovy.runtime.async}}). If the JDK's
async infrastructure evolves (e.g.,
structured concurrency), only the internal layer changes.
Minimal grammar footprint. {{async}}, {{await}}, {{defer}}, and {{yield}} are
contextual keywords — they remain valid identifiers in non-async contexts,
preserving backward compatibility. Grammar changes to {{GroovyLexer.g4}} and
{{GroovyParser.g4}} are
minimal.
Thread safety is the framework's responsibility. All concurrency control
(atomics, volatile, CAS) is encapsulated in the runtime. Application code never
needs explicit locks, synchronization, or volatile annotations.
JVM ecosystem integration. Built-in adapters handle {{CompletableFuture}},
{{CompletionStage}}, and {{Future}} out of the box. Third-party frameworks
integrate via the {{AwaitableAdapterRegistry}} SPI.
h2. Execution Model
On {*}JDK 21+{*}, each {{async}} closure runs on a virtual thread. When the
thread blocks on {{await}}, the JVM parks the virtual thread and releases the
carrier (OS) thread. This achieves the practical scalability of
compiler-generated state machines (as
in C# and Kotlin) without requiring control-flow rewriting — stack traces
remain complete, and standard debuggers work unmodified.
On {*}JDK 17–20{*}, a bounded cached thread pool (default 256, configurable
via {{groovy.async.parallelism}}) with a caller-runs back-pressure policy
provides stable performance.
The executor is fully configurable at runtime via
{{Awaitable.setExecutor(executor)}}.
h2. Key Features in Detail
h3. Combinators
||Method||Analogy||Behavior||
|{{Awaitable.all(a, b, c)}}|{{Promise.all()}} \\ {{Task.WhenAll()}}|Waits for
all to succeed; fails immediately on first error (fail-fast)|
|{{Awaitable.any(a, b)}}|{{Promise.race()}} \\ {{Task.WhenAny()}}|Returns the
first to complete (success or failure)|
|{{Awaitable.first(a, b, c)}}|{{Promise.any()}}|Returns the first to succeed;
fails only when all sources fail (aggregate error)|
|{{Awaitable.allSettled(a, b)}}|{{Promise.allSettled()}}|Waits for all to
complete (success or fail); captures outcomes in {{AwaitResult}} list without
throwing|
|{{Awaitable.delay(ms)}}|{{Task.Delay()}} \\ {{setTimeout}}|Non-blocking
timer|
|{{Awaitable.orTimeoutMillis(source, ms)}}|{{withTimeout}}|Fails with
{{TimeoutException}} on expiry|
|{{Awaitable.completeOnTimeoutMillis(source, fallback, ms)}}|—|Completes with
fallback value on expiry|
h3. Async Generators and Back-Pressure
Closures containing {{yield return}} produce an {{Iterable}}, consumable via
{{for await}} or a regular {{for}} loop. The producer and consumer coordinate
through the {{GeneratorBridge}}, which uses a {{SynchronousQueue}} — the
producer blocks on each
{{yield return}} until the consumer pulls the next element, providing natural
back-pressure without unbounded buffering. If the consumer exits early
({{break}}, {{return}}, exception), the producer thread is interrupted via
cooperative cancellation,
preventing resource leaks. {{for await}} wraps the loop in a try-finally to
ensure generator cleanup.
h3. Channels (Go-Style Inter-Task Communication)
{{AsyncChannel}} provides CSP-style communication between tasks. A producer
sends values into a channel; a consumer receives them. The channel handles
synchronization and optional buffering — no shared mutable state needed.
Channels support unbuffered
(rendezvous, {{create()}}) and buffered ({{create\(n)}}) modes. Sending
blocks when the buffer is full; receiving blocks when empty. Since channels
implement {{Iterable}}, they work with {{for await}}, regular {{for}} loops,
and Groovy collection methods.
h3. Structured Concurrency
{{AsyncScope}} binds the lifetime of child tasks to a scope. When the scope
exits, all children are guaranteed complete (or cancelled). This prevents
orphaned tasks and silent failures. By default, the scope uses fail-fast
semantics — if any child fails,
all siblings are cancelled immediately. On JDK 25+, scope tracking uses
{{ScopedValue}} for optimal virtual thread performance; on JDK 17–24, a
{{ThreadLocal}} fallback is used transparently.
h3. Defer (Go-Style Cleanup)
The {{defer}} keyword schedules cleanup actions that execute in LIFO order
when the enclosing async closure returns, regardless of success or failure. If
multiple deferred blocks throw, the first exception is primary and subsequent
ones are attached via
{{addSuppressed()}}. If a deferred action returns an {{Awaitable}} or
{{Future}}, the result is awaited before the next deferred action runs,
ensuring orderly cleanup of asynchronous resources. This provides deterministic
resource cleanup without deeply
nested {{try}}/{{finally}} blocks.
h3. Adapter Registry (Third-Party Integration)
The {{AwaitableAdapterRegistry}} is an SPI-based extension point. Adapters
can be registered:
- At class-load time via {{ServiceLoader}}
({{META-INF/services/groovy.concurrent.AwaitableAdapter}})
- At runtime via {{AwaitableAdapterRegistry.register(adapter)}}
This enables {{await}} to work transparently with RxJava
{{Single}}/{{Observable}}, Reactor {{Mono}}/{{Flux}}, or any custom async type
— a single {{await}} keyword, regardless of the underlying library. Drop-in
adapter modules are provided for:
- groovy-reactor — {{await}} on {{Mono}}, {{for await}} over {{Flux}}
- groovy-rxjava — {{await}} on {{Single}}/{{Maybe}}/{{Completable}}, {{for
await}} over {{Observable}}/{{Flowable}}
h2. Thread Safety Mechanisms
All concurrency control is internal and transparent to users:
- Lock-free synchronization — {{volatile}} fields, {{AtomicInteger}},
{{AtomicReference}}, {{CopyOnWriteArrayList}} used throughout; no
{{synchronized}} blocks in the async runtime
- TOCTOU prevention — {{GeneratorBridge.yield()}} sets {{producerThread}}
before checking the {{closed}} flag, then re-checks after blocking operations,
closing a race window with concurrent {{close()}}
- Cooperative cancellation — {{GeneratorBridge.close()}} atomically sets a
closed flag, drains any pending handoff, and interrupts the producer thread
- Idempotent close — All close operations are safe to call multiple times
from any thread
h2. Cross-Language Comparison
||Aspect||Groovy||JavaScript||C#||Kotlin||Swift||
|Async declaration|async \{ ... \}|{{async function foo()}}|{{async Task
Foo()}}|{{suspend fun foo(): T}}|{{func foo() async throws -> T}}|
|Await|{{await expr}}|{{await expr}}|{{await
expr}}|{{deferred.await()}}|{{try await expr}}|
|Async iteration|{{for await (x in src)}}|{{for await (x of src)}}|{{await
foreach (x in src)}}|manual ({{Flow.collect}})|{{for try await x in seq}}|
|Async generator|{{yield return expr}}|{{yield}} in {{async
function*}}|{{yield return}} in {{IAsyncEnumerable}}|flow { emit( x )
}|AsyncStream { yield( x ) }|
|Defer|{{defer}}|(none)|{{await using}}|{{use}}|{{defer}}|
|Channels|{{AsyncChannel}}|(none)|{{Channel}}|{{Channel}}|{{AsyncStream}}
(limited)|
|Structured
concurrency|{{AsyncScope}}|(none)|(none)|{{coroutineScope}}|{{TaskGroup}}|
|Implementation|Thread-per-task (VT on 21+)|Event loop|State
machine|Coroutine SM|Async SM|
h2. Backward Compatibility
- {{async}}, {{await}}, {{defer}}, and {{yield}} are contextual keywords —
they act as keywords only in specific syntactic positions and remain valid
identifiers elsewhere. Existing code using these as variable names, method
names, or field names
continues to compile and run without modification.
- No existing public APIs are modified or removed.
- The feature is purely additive: code that does not use {{async}}/{{await}}
is entirely unaffected.
> Add native async/await support
> ------------------------------
>
> Key: GROOVY-9381
> URL: https://issues.apache.org/jira/browse/GROOVY-9381
> Project: Groovy
> Issue Type: New Feature
> Reporter: Daniel Sun
> Assignee: Daniel Sun
> Priority: Major
> Fix For: 6.x
>
>
> h2. Summary
>
>
>
>
> Introduce first-class {{async}}/{{await}} language support to Groovy,
> enabling developers to write asynchronous code in a sequential, readable
> style — on par with the async/await facilities in JavaScript (ES2017), C#
> (5.0), Kotlin (coroutines), and Swift
> (5.5).
>
>
>
>
> h2. Motivation
> Modern JVM applications are overwhelmingly concurrent. Web services, data
> pipelines, and reactive systems spend most of their time waiting for network
> I/O, database queries, or downstream services. The JVM offers powerful but
> low-level concurrency
> primitives ({{CompletableFuture}}, {{Flow.Publisher}},
> {{ExecutorService}}), and while libraries like RxJava and Project Reactor
> raise the abstraction level, they introduce their own learning curve and
> cannot alter the language's control-flow syntax.
>
>
>
>
> Today, a typical three-step async workflow in Groovy looks like this:
>
>
>
> {code:groovy}
> CompletableFuture.supplyAsync { fetchUserId() }
>
>
>
> .thenCompose { id -> CompletableFuture.supplyAsync { lookupName(id) } }
>
>
>
> .thenCompose { name -> CompletableFuture.supplyAsync {
> loadProfile(name) } }
> .exceptionally { ex -> handleError(ex) }
>
>
>
> {code}
> The business logic is obscured by plumbing. Exception handling is decoupled
> from the code that raises exceptions, and the control flow reads inside-out.
>
>
>
>
>
>
> With {{async}}/{{await}}, the same logic becomes:
>
>
>
> {code:groovy}
>
>
>
> def profile = async {
>
>
>
> def id = await fetchUserIdAsync()
> def name = await lookupNameAsync(id)
> return await loadProfileAsync(name)
> }
>
>
>
> {code}
> This reads identically to synchronous code. Standard {{try}}/{{catch}},
> {{for}}, {{if}}, and variable assignment all compose naturally — no
> callbacks, no chained lambdas.
>
>
>
>
>
> h2. Scope
>
>
>
>
> This proposal introduces the following language constructs and runtime APIs:
> h3. Language Constructs
> ||Construct||Syntax||Description||
> |Async closure|async \{ ... \}|Starts a closure on a background thread,
> returning an {{Awaitable}} (or {{Iterable}} when the body contains {{yield
> return}})|
> |Await expression|await expr / await(expr)|Blocks until the awaited
> computation completes; transparently unwraps the result|
>
>
> |Multi-arg await|await(a, b, c) \\ await a, b, c|Syntactic sugar for
> {{Awaitable.all(a, b, c)}}|
> |For-await loop|for await (item in source) \{ ... \}|Iterates over an async
> source (generator, channel, reactive type), with automatic resource cleanup
> via try-finally|
>
> |Yield return|yield return expr|Emits a value from an async generator
> closure, producing an {{Iterable}} with natural back-pressure|
>
>
> |Defer|defer \{ cleanup \} \\ defer cleanup|Schedules a cleanup block to
> execute on closure exit (LIFO order), inspired by Go's {{defer}}|
>
>
>
>
>
>
> h3. Public API ({{groovy.concurrent}} package)
>
>
>
> ||Class/Interface||Role||
>
>
>
> |{{Awaitable}}|Core promise abstraction (analogous to C#'s {{Task}} / JS's
> {{Promise}}). Provides static combinators ({{all}}, {{any}}, {{first}},
> {{allSettled}}, {{delay}}, {{orTimeout}}, {{completeOnTimeout}}), factories
> ({{of}}, {{failed}}, {{from}},
> {{go}}), and instance continuation methods ({{then}}, {{thenCompose}},
> {{thenAccept}}, {{exceptionally}}, {{whenComplete}}, {{handle}},
> {{orTimeout}}, {{completeOnTimeout}})|
>
> |{{AsyncChannel}}|Go-style inter-task communication channel with optional
> buffering. Supports unbuffered (rendezvous) and buffered modes. Implements
> {{Iterable}}, so works with {{for await}} and regular {{for}} loops|
> |{{AsyncScope}}|Structured concurrency scope — binds child task lifetimes
> to a scope with fail-fast cancellation. All children are guaranteed complete
> (or cancelled) before the scope exits|
>
> |{{AwaitResult}}|Outcome wrapper returned by {{allSettled()}} — carries
> either a success value or a failure throwable|
> |{{AwaitableAdapter}}|SPI interface for adapting third-party async types
> (RxJava, Reactor, etc.) to {{Awaitable}}|
>
>
> |{{AwaitableAdapterRegistry}}|Central adapter registry with
> {{ServiceLoader}} auto-discovery and runtime registration|
>
>
> |{{ChannelClosedException}}|Thrown when sending to or receiving from a
> closed channel|
>
>
>
>
>
>
> h3. Internal Runtime ({{org.apache.groovy.runtime.async}} package)
> ||Class||Role||
>
>
>
> |{{AsyncSupport}}|Central runtime entry point — all {{await}} overloads,
> {{async}} execution, {{defer}} scope management, combinator implementation,
> timeout scheduling, {{yield return}} dispatch, channel and scope support|
> |{{GroovyPromise}}|Default {{Awaitable}} implementation backed by
> {{CompletableFuture}}. Sole bridge between the public API and JDK async
> infrastructure|
>
> |{{GeneratorBridge}}|Producer/consumer bridge for async generators ({{yield
> return}}). Uses {{SynchronousQueue}} for zero-buffered back-pressure with
> cooperative cancellation via thread tracking. Implements {{Iterator}} and
> {{Closeable}}|
> |{{DefaultAsyncChannel}}|Default {{AsyncChannel}} implementation with
> buffered and unbuffered modes|
>
>
> |{{DefaultAsyncScope}}|Default {{AsyncScope}} implementation with fail-fast
> child cancellation|
>
>
>
>
>
>
> h2. Design Principles
>
>
>
> Readability first. Async code should be visually indistinguishable from
> synchronous code. All standard Groovy control-flow constructs
> ({{try}}/{{catch}}, {{for}}, {{if}}/{{else}}, closures) must work naturally
> inside async closures.
>
>
>
>
> Exception transparency. {{await}} automatically unwraps
> {{CompletionException}}, {{ExecutionException}}, and other JVM wrapper
> layers. The original exception type, message, and stack trace are preserved —
> callers see the same exceptions they would in
> synchronous code.
>
>
>
>
> API decoupling. User code depends on {{Awaitable}}, not on
> {{CompletableFuture}}. The public API ({{groovy.concurrent}}) is separated
> from the internal implementation ({{org.apache.groovy.runtime.async}}). If
> the JDK's async infrastructure evolves (e.g.,
> structured concurrency), only the internal layer changes.
>
>
>
>
> Minimal grammar footprint. {{async}}, {{await}}, {{defer}}, and {{yield}}
> are contextual keywords — they remain valid identifiers in non-async
> contexts, preserving backward compatibility. Grammar changes to
> {{GroovyLexer.g4}} and {{GroovyParser.g4}} are
> minimal.
>
>
>
>
> Thread safety is the framework's responsibility. All concurrency control
> (atomics, volatile, CAS) is encapsulated in the runtime. Application code
> never needs explicit locks, synchronization, or volatile annotations.
>
>
> JVM ecosystem integration. Built-in adapters handle {{CompletableFuture}},
> {{CompletionStage}}, and {{Future}} out of the box. Third-party frameworks
> integrate via the {{AwaitableAdapterRegistry}} SPI.
>
>
> h2. Execution Model
>
>
>
>
> On {*}JDK 21+{*}, each {{async}} closure runs on a virtual thread. When the
> thread blocks on {{await}}, the JVM parks the virtual thread and releases the
> carrier (OS) thread. This achieves the practical scalability of
> compiler-generated state machines (as
> in C# and Kotlin) without requiring control-flow rewriting — stack traces
> remain complete, and standard debuggers work unmodified.
>
>
>
>
> On {*}JDK 17–20{*}, a bounded cached thread pool (default 256, configurable
> via {{groovy.async.parallelism}}) with a caller-runs back-pressure policy
> provides stable performance.
>
>
> The executor is fully configurable at runtime via
> {{Awaitable.setExecutor(executor)}}.
>
>
>
> h2. Key Features in Detail
>
>
>
>
> h3. Combinators
>
>
>
> ||Method||Analogy||Behavior||
> |{{Awaitable.all(a, b, c)}}|{{Promise.all()}} \\ {{Task.WhenAll()}}|Waits
> for all to succeed; fails immediately on first error (fail-fast)|
> |{{Awaitable.any(a, b)}}|{{Promise.any()}}|Returns the first to complete
> (success or failure)|
>
>
> |{{Awaitable.first(a, b, c)}}|{{Promise.race()}} \\
> {{Task.WhenAny()}}|Returns the first to succeed; fails only when all sources
> fail (aggregate error)|
>
> |{{Awaitable.allSettled(a, b)}}|{{Promise.allSettled()}}|Waits for all to
> complete (success or fail); captures outcomes in {{AwaitResult}} list without
> throwing|
>
> |{{Awaitable.delay(ms)}}|{{Task.Delay()}} \\ {{setTimeout}}|Non-blocking
> timer|
>
>
> |{{Awaitable.orTimeoutMillis(source, ms)}}|{{withTimeout}}|Fails with
> {{TimeoutException}} on expiry|
>
>
> |{{Awaitable.completeOnTimeoutMillis(source, fallback, ms)}}|—|Completes
> with fallback value on expiry|
>
>
>
>
>
>
> h3. Async Generators and Back-Pressure
>
>
>
>
>
>
>
> Closures containing {{yield return}} produce an {{Iterable}}, consumable
> via {{for await}} or a regular {{for}} loop. The producer and consumer
> coordinate through the {{GeneratorBridge}}, which uses a {{SynchronousQueue}}
> — the producer blocks on each
> {{yield return}} until the consumer pulls the next element, providing
> natural back-pressure without unbounded buffering. If the consumer exits
> early ({{break}}, {{return}}, exception), the producer thread is interrupted
> via cooperative cancellation,
> preventing resource leaks. {{for await}} wraps the loop in a try-finally to
> ensure generator cleanup.
>
>
>
> h3. Channels (Go-Style Inter-Task Communication)
> {{AsyncChannel}} provides CSP-style communication between tasks. A producer
> sends values into a channel; a consumer receives them. The channel handles
> synchronization and optional buffering — no shared mutable state needed.
> Channels support unbuffered
> (rendezvous, {{create()}}) and buffered ({{create\(n)}}) modes. Sending
> blocks when the buffer is full; receiving blocks when empty. Since channels
> implement {{Iterable}}, they work with {{for await}}, regular {{for}} loops,
> and Groovy collection methods.
>
>
>
>
> h3. Structured Concurrency
>
>
>
>
> {{AsyncScope}} binds the lifetime of child tasks to a scope. When the scope
> exits, all children are guaranteed complete (or cancelled). This prevents
> orphaned tasks and silent failures. By default, the scope uses fail-fast
> semantics — if any child fails,
> all siblings are cancelled immediately. On JDK 25+, scope tracking uses
> {{ScopedValue}} for optimal virtual thread performance; on JDK 17–24, a
> {{ThreadLocal}} fallback is used transparently.
>
>
>
>
> h3. Defer (Go-Style Cleanup)
> The {{defer}} keyword schedules cleanup actions that execute in LIFO order
> when the enclosing async closure returns, regardless of success or failure.
> If multiple deferred blocks throw, the first exception is primary and
> subsequent ones are attached via
> {{addSuppressed()}}. If a deferred action returns an {{Awaitable}} or
> {{Future}}, the result is awaited before the next deferred action runs,
> ensuring orderly cleanup of asynchronous resources. This provides
> deterministic resource cleanup without deeply
> nested {{try}}/{{finally}} blocks.
>
>
>
>
> h3. Adapter Registry (Third-Party Integration)
> The {{AwaitableAdapterRegistry}} is an SPI-based extension point. Adapters
> can be registered:
>
>
> - At class-load time via {{ServiceLoader}}
> ({{META-INF/services/groovy.concurrent.AwaitableAdapter}})
> - At runtime via {{AwaitableAdapterRegistry.register(adapter)}}
>
>
>
>
> This enables {{await}} to work transparently with RxJava
> {{Single}}/{{Observable}}, Reactor {{Mono}}/{{Flux}}, or any custom async
> type — a single {{await}} keyword, regardless of the underlying library.
> Drop-in adapter modules are provided for:
> - groovy-reactor — {{await}} on {{Mono}}, {{for await}} over {{Flux}}
>
>
>
> - groovy-rxjava — {{await}} on {{Single}}/{{Maybe}}/{{Completable}}, {{for
> await}} over {{Observable}}/{{Flowable}}
>
>
>
>
>
>
> h2. Thread Safety Mechanisms
>
>
>
>
> All concurrency control is internal and transparent to users:
> - Lock-free synchronization — {{volatile}} fields, {{AtomicInteger}},
> {{AtomicReference}}, {{CopyOnWriteArrayList}} used throughout; no
> {{synchronized}} blocks in the async runtime
> - TOCTOU prevention — {{GeneratorBridge.yield()}} sets {{producerThread}}
> before checking the {{closed}} flag, then re-checks after blocking
> operations, closing a race window with concurrent {{close()}}
>
> - Cooperative cancellation — {{GeneratorBridge.close()}} atomically sets a
> closed flag, drains any pending handoff, and interrupts the producer thread
>
> - Idempotent close — All close operations are safe to call multiple times
> from any thread
>
>
>
>
>
>
> h2. Cross-Language Comparison
>
>
>
> ||Aspect||Groovy||JavaScript||C#||Kotlin||Swift||
>
>
>
> |Async declaration|async \{ ... \}|{{async function foo()}}|{{async Task
> Foo()}}|{{suspend fun foo(): T}}|{{func foo() async throws -> T}}|
> |Await|{{await expr}}|{{await expr}}|{{await
> expr}}|{{deferred.await()}}|{{try await expr}}|
>
>
> |Async iteration|{{for await (x in src)}}|{{for await (x of src)}}|{{await
> foreach (x in src)}}|manual ({{Flow.collect}})|{{for try await x in seq}}|
>
>
> |Async generator|{{yield return expr}}|{{yield}} in {{async
> function*}}|{{yield return}} in {{IAsyncEnumerable}}|flow { emit( x )
> }|AsyncStream { yield( x ) }|
>
> |Defer|{{defer}}|(none)|{{await using}}|{{use}}|{{defer}}|
>
>
>
> |Channels|{{AsyncChannel}}|(none)|{{Channel}}|{{Channel}}|{{AsyncStream}}
> (limited)|
>
>
> |Structured
> concurrency|{{AsyncScope}}|(none)|(none)|{{coroutineScope}}|{{TaskGroup}}|
>
>
>
> |Implementation|Thread-per-task (VT on 21+)|Event loop|State
> machine|Coroutine SM|Async SM|
>
>
>
>
>
>
> h2. Backward Compatibility
>
>
>
> - {{async}}, {{await}}, {{defer}}, and {{yield}} are contextual keywords —
> they act as keywords only in specific syntactic positions and remain valid
> identifiers elsewhere. Existing code using these as variable names, method
> names, or field names
> continues to compile and run without modification.
>
>
>
> - No existing public APIs are modified or removed.
> - The feature is purely additive: code that does not use
> {{async}}/{{await}} is entirely unaffected.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
