[
https://issues.apache.org/jira/browse/GROOVY-9381?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King resolved GROOVY-9381.
-------------------------------
Fix Version/s: 6.0.0-alpha-1
(was: 6.x)
Resolution: Fixed
> 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.0.0-alpha-1
>
>
> 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)
