Skip to content

Commit 097004d

Browse files
committed
Minor tweaks
1 parent cd1900c commit 097004d

15 files changed

Lines changed: 768 additions & 118 deletions

File tree

src/main/java/groovy/concurrent/AsyncStream.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,36 @@ public interface AsyncStream<T> extends AutoCloseable {
7575
default void close() {
7676
}
7777

78+
/**
79+
* Converts the given source to an {@code AsyncStream}.
80+
* <p>
81+
* If the source is already an {@code AsyncStream}, it is returned as-is.
82+
* Otherwise, the {@link AwaitableAdapterRegistry} is consulted to find a
83+
* suitable adapter. Built-in adapters handle {@link Iterable} and
84+
* {@link java.util.Iterator}; the auto-discovered {@code FlowPublisherAdapter}
85+
* handles {@link java.util.concurrent.Flow.Publisher}; third-party frameworks
86+
* can register additional adapters via the registry.
87+
* <p>
88+
* This is the recommended entry point for converting external collection or
89+
* reactive types to {@code AsyncStream}:
90+
* <pre>
91+
* AsyncStream&lt;String&gt; stream = AsyncStream.from(myList)
92+
* AsyncStream&lt;Integer&gt; stream2 = AsyncStream.from(myFlowPublisher)
93+
* </pre>
94+
*
95+
* @param source the source object; must not be {@code null}
96+
* @param <T> the element type
97+
* @return an async stream backed by the source
98+
* @throws IllegalArgumentException if {@code source} is {@code null}
99+
* or no adapter supports the source type
100+
* @see AwaitableAdapterRegistry#toAsyncStream(Object)
101+
* @since 6.0.0
102+
*/
103+
@SuppressWarnings("unchecked")
104+
static <T> AsyncStream<T> from(Object source) {
105+
return AwaitableAdapterRegistry.toAsyncStream(source);
106+
}
107+
78108
/**
79109
* Returns an empty {@code AsyncStream} that completes immediately.
80110
*/

src/main/java/groovy/concurrent/Awaitable.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,18 @@
5454
* {@code Promise.allSettled()}</li>
5555
* <li>{@link #delay(long) Awaitable.delay(ms)} — like
5656
* {@code Task.Delay()} / {@code setTimeout}</li>
57-
* <li>{@link #timeout(Object, long) Awaitable.timeout(task, ms)} — like
57+
* <li>{@link #orTimeoutMillis(Object, long) Awaitable.orTimeoutMillis(task, ms)} — like
5858
* Kotlin's {@code withTimeout} or a JavaScript promise raced against a timer</li>
59-
* <li>{@link #timeoutOr(Object, Object, long) Awaitable.timeoutOr(task, fallback, ms)} —
59+
* <li>{@link #completeOnTimeoutMillis(Object, Object, long)
60+
* Awaitable.completeOnTimeoutMillis(task, fallback, ms)} —
6061
* like a timeout with fallback/default value</li>
6162
* </ul>
6263
* <p>
63-
* <b>Static factories:</b>
64+
* <b>Static factories and conversion:</b>
6465
* <ul>
66+
* <li>{@link #from(Object) Awaitable.from(source)} — converts any supported
67+
* async type (CompletableFuture, CompletionStage, Future, Flow.Publisher, etc.)
68+
* to an {@code Awaitable}</li>
6569
* <li>{@link #of(Object) Awaitable.of(value)} — like
6670
* {@code Task.FromResult()} / {@code Promise.resolve()}</li>
6771
* <li>{@link #failed(Throwable) Awaitable.failed(error)} — like
@@ -297,6 +301,35 @@ default Awaitable<T> completeOnTimeout(T fallback, long duration, TimeUnit unit)
297301

298302
// ---- Static factories ----
299303

304+
/**
305+
* Converts the given source to an {@code Awaitable}.
306+
* <p>
307+
* If the source is already an {@code Awaitable}, it is returned as-is.
308+
* Otherwise, the {@link AwaitableAdapterRegistry} is consulted to find a
309+
* suitable adapter. Built-in adapters handle {@link CompletableFuture},
310+
* {@link java.util.concurrent.CompletionStage}, {@link java.util.concurrent.Future},
311+
* and {@link java.util.concurrent.Flow.Publisher}; third-party frameworks
312+
* can register additional adapters via the registry.
313+
* <p>
314+
* This is the recommended entry point for converting external async types
315+
* to {@code Awaitable}:
316+
* <pre>
317+
* Awaitable&lt;String&gt; aw = Awaitable.from(someCompletableFuture)
318+
* Awaitable&lt;Integer&gt; aw2 = Awaitable.from(someReactorMono)
319+
* </pre>
320+
*
321+
* @param source the source object; must not be {@code null}
322+
* @param <T> the result type
323+
* @return an awaitable backed by the source
324+
* @throws IllegalArgumentException if {@code source} is {@code null}
325+
* or no adapter supports the source type
326+
* @see AwaitableAdapterRegistry#toAwaitable(Object)
327+
* @since 6.0.0
328+
*/
329+
static <T> Awaitable<T> from(Object source) {
330+
return AwaitableAdapterRegistry.toAwaitable(source);
331+
}
332+
300333
/**
301334
* Returns an already-completed {@code Awaitable} with the given value.
302335
* Analogous to C#'s {@code Task.FromResult()} or JavaScript's

src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,18 @@ public static void setBlockingExecutor(Executor executor) {
111111
/**
112112
* Converts the given source to an {@link Awaitable}.
113113
* If the source is already an {@code Awaitable}, it is returned as-is.
114+
* <p>
115+
* <b>Tip:</b> user code should generally prefer {@link Awaitable#from(Object)},
116+
* which delegates to this method but is more discoverable from the
117+
* {@code Awaitable} type itself.
114118
*
115119
* @param source the source object; must not be {@code null}
116120
* @throws IllegalArgumentException if {@code source} is {@code null}
117121
* or no adapter supports the source type
122+
* @see Awaitable#from(Object)
118123
*/
119124
@SuppressWarnings("unchecked")
120-
public static <T> Awaitable<T> toAwaitable(Object source) {
125+
static <T> Awaitable<T> toAwaitable(Object source) {
121126
if (source == null) {
122127
throw new IllegalArgumentException("Cannot convert null to Awaitable");
123128
}
@@ -136,13 +141,18 @@ public static <T> Awaitable<T> toAwaitable(Object source) {
136141
/**
137142
* Converts the given source to an {@link AsyncStream}.
138143
* If the source is already an {@code AsyncStream}, it is returned as-is.
144+
* <p>
145+
* <b>Tip:</b> user code should generally prefer {@link AsyncStream#from(Object)},
146+
* which delegates to this method but is more discoverable from the
147+
* {@code AsyncStream} type itself.
139148
*
140149
* @param source the source object; must not be {@code null}
141150
* @throws IllegalArgumentException if {@code source} is {@code null}
142151
* or no adapter supports the source type
152+
* @see AsyncStream#from(Object)
143153
*/
144154
@SuppressWarnings("unchecked")
145-
public static <T> AsyncStream<T> toAsyncStream(Object source) {
155+
static <T> AsyncStream<T> toAsyncStream(Object source) {
146156
if (source == null) {
147157
throw new IllegalArgumentException("Cannot convert null to AsyncStream");
148158
}

src/main/java/groovy/transform/Async.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@
107107
* <ul>
108108
* <li>Cannot be applied to abstract methods</li>
109109
* <li>Cannot be applied to constructors</li>
110-
* <li>Cannot be applied to methods that already return {@code Awaitable}</li>
110+
* <li>Cannot be applied to methods that already return an async type
111+
* ({@code Awaitable}, {@code AsyncStream}, {@code CompletableFuture},
112+
* {@code CompletionStage}, or {@code Future})</li>
111113
* </ul>
112114
*
113115
* @see groovy.concurrent.Awaitable

src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,10 @@ public ExpressionStatement visitYieldReturnStmtAlt(final YieldReturnStmtAltConte
932932

933933
@Override
934934
public ExpressionStatement visitDeferStmtAlt(final DeferStmtAltContext ctx) {
935+
if (asyncContextDepth == 0) {
936+
throw createParsingFailedException(
937+
"`defer` can only be used inside an async method or async closure", ctx);
938+
}
935939
Expression action;
936940
ExpressionStatement stmtExprStmt = (ExpressionStatement) this.visit(ctx.statementExpression());
937941
Expression expr = stmtExprStmt.getExpression();
@@ -1773,8 +1777,15 @@ public MethodNode visitMethodDeclaration(final MethodDeclarationContext ctx) {
17731777
Parameter[] parameters = this.visitFormalParameters(ctx.formalParameters());
17741778
ClassNode[] exceptions = this.visitQualifiedClassNameList(ctx.qualifiedClassNameList());
17751779

1780+
boolean isAsync = modifierManager.containsAny(ASYNC);
1781+
if (isAsync) asyncContextDepth++;
17761782
anonymousInnerClassesDefinedInMethodStack.push(new LinkedList<>());
1777-
Statement code = this.visitMethodBody(ctx.methodBody());
1783+
Statement code;
1784+
try {
1785+
code = this.visitMethodBody(ctx.methodBody());
1786+
} finally {
1787+
if (isAsync) asyncContextDepth--;
1788+
}
17781789
List<InnerClassNode> anonymousInnerClassList = anonymousInnerClassesDefinedInMethodStack.pop();
17791790

17801791
MethodNode methodNode;
@@ -1789,7 +1800,7 @@ public MethodNode visitMethodDeclaration(final MethodDeclarationContext ctx) {
17891800
}
17901801

17911802
// Inject @Async annotation for methods declared with the 'async' keyword modifier
1792-
if (modifierManager.containsAny(ASYNC)) {
1803+
if (isAsync) {
17931804
methodNode.addAnnotation(new AnnotationNode(ClassHelper.make("groovy.transform.Async")));
17941805
}
17951806

@@ -2985,7 +2996,13 @@ public Expression visitAwaitExprAlt(final AwaitExprAltContext ctx) {
29852996

29862997
@Override
29872998
public Expression visitAsyncClosureExprAlt(final AsyncClosureExprAltContext ctx) {
2988-
ClosureExpression closure = this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression());
2999+
asyncContextDepth++;
3000+
ClosureExpression closure;
3001+
try {
3002+
closure = this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression());
3003+
} finally {
3004+
asyncContextDepth--;
3005+
}
29893006
boolean hasUserParams = closure.getParameters() != null && closure.getParameters().length > 0;
29903007
boolean hasYieldReturn = AsyncTransformHelper.containsYieldReturn(closure.getCode());
29913008
boolean hasDefer = AsyncTransformHelper.containsDefer(closure.getCode());
@@ -4886,6 +4903,9 @@ public List<DeclarationExpression> getDeclarationExpressions() {
48864903
private int visitingClosureCount;
48874904
private int visitingAssertStatementCount;
48884905
private int visitingArrayInitializerCount;
4906+
/** Tracks nesting depth of async contexts (async methods and async closures)
4907+
* to validate that {@code defer} is only used within an async body. */
4908+
private int asyncContextDepth;
48894909

48904910
private static final int SLL_THRESHOLD = SystemUtil.getIntegerSafe("groovy.antlr4.sll.threshold", -1);
48914911

src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import groovy.concurrent.AsyncStream;
2222
import groovy.concurrent.AwaitResult;
2323
import groovy.concurrent.Awaitable;
24-
import groovy.concurrent.AwaitableAdapterRegistry;
2524
import groovy.lang.Closure;
2625

2726
import java.lang.invoke.MethodHandle;
@@ -64,8 +63,9 @@
6463
* <ul>
6564
* <li><b>Async execution</b> — {@code executeAsync()}, {@code executeAsyncVoid()},
6665
* and {@code wrapAsync()} run closures on the configured executor</li>
67-
* <li><b>Await</b> — all {@code await()} overloads go through the
68-
* {@link AwaitableAdapterRegistry} so that third-party async types
66+
* <li><b>Await</b> — all {@code await()} overloads use
67+
* {@link groovy.concurrent.Awaitable#from(Object) Awaitable.from()} so
68+
* that third-party async types
6969
* (RxJava {@code Single}, Reactor {@code Mono}, etc.) are supported
7070
* transparently once an adapter is registered</li>
7171
* <li><b>Async generators</b> — {@code generateAsyncStream()} manages the
@@ -112,7 +112,6 @@
112112
*
113113
* @see groovy.concurrent.Awaitable
114114
* @see groovy.transform.Async
115-
* @see Awaitable
116115
* @since 6.0.0
117116
*/
118117
public class AsyncSupport {
@@ -267,8 +266,8 @@ public static <T> T await(Future<T> future) {
267266
}
268267

269268
/**
270-
* Awaits an arbitrary object by adapting it to {@link Awaitable} via the
271-
* {@link AwaitableAdapterRegistry}. This is the fallback overload called
269+
* Awaits an arbitrary object by adapting it to {@link Awaitable} via
270+
* {@link Awaitable#from(Object)}. This is the fallback overload called
272271
* by the {@code await} expression when the compile-time type is not one
273272
* of the other supported types. Returns {@code null} for a {@code null}
274273
* argument.
@@ -285,7 +284,7 @@ public static <T> T await(Object source) {
285284
if (source instanceof CompletionStage) return await((CompletionStage<T>) source);
286285
if (source instanceof Future) return await((Future<T>) source);
287286
if (source instanceof Closure) return awaitClosure((Closure<?>) source);
288-
return await(AwaitableAdapterRegistry.<T>toAwaitable(source));
287+
return await(Awaitable.<T>from(source));
289288
}
290289

291290
/**
@@ -543,7 +542,7 @@ private static CompletableFuture<?> toCompletableFuture(Object source) {
543542
if (source instanceof CompletableFuture<?> cf) return cf;
544543
if (source instanceof Awaitable<?> a) return a.toCompletableFuture();
545544
if (source instanceof CompletionStage<?> cs) return cs.toCompletableFuture();
546-
return AwaitableAdapterRegistry.toAwaitable(source).toCompletableFuture();
545+
return Awaitable.from(source).toCompletableFuture();
547546
}
548547

549548
// ---- non-blocking combinators (return Awaitable) --------------------
@@ -813,7 +812,7 @@ public static Awaitable<Void> delay(long duration, TimeUnit unit) {
813812
public static <T> AsyncStream<T> toAsyncStream(Object source) {
814813
if (source == null) return AsyncStream.empty();
815814
if (source instanceof AsyncStream) return (AsyncStream<T>) source;
816-
return AwaitableAdapterRegistry.toAsyncStream(source);
815+
return AsyncStream.from(source);
817816
}
818817

819818
/**

src/main/java/org/apache/groovy/runtime/async/FlowPublisherAdapter.java

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,13 @@
6464
* <ul>
6565
* <li>§2.5 — duplicate {@code onSubscribe} cancels the second subscription</li>
6666
* <li>§2.13 — {@code null} items in {@code onNext} are rejected immediately</li>
67-
* <li>Terminal signals ({@code onError}/{@code onComplete}) use
67+
* <li>All signals ({@code onNext}, {@code onError}, {@code onComplete}) use
6868
* blocking {@code put()} to guarantee delivery even under queue
6969
* contention</li>
70+
* <li>Back-pressure is enforced by requesting exactly one item after
71+
* each consumed element; demand is signalled <em>before</em> the
72+
* consumer's {@code moveNext()} awaitable completes, preventing
73+
* livelock when producer and consumer share the same thread pool</li>
7074
* </ul>
7175
*
7276
* @see groovy.concurrent.AwaitableAdapterRegistry
@@ -209,14 +213,10 @@ public void onComplete() {
209213
// Signal wrapper types allow us to distinguish values, errors, and
210214
// completion in a single queue without type confusion.
211215

212-
private static final class ValueSignal<T> {
213-
final T value;
214-
ValueSignal(T value) { this.value = value; }
216+
private record ValueSignal<T>(T value) {
215217
}
216218

217-
private static final class ErrorSignal {
218-
final Throwable error;
219-
ErrorSignal(Throwable error) { this.error = error; }
219+
private record ErrorSignal(Throwable error) {
220220
}
221221

222222
/** Singleton sentinel for stream completion. */
@@ -227,9 +227,16 @@ private static final class ErrorSignal {
227227
* providing a pull-based iteration interface over a push-based source.
228228
*
229229
* <p>Back-pressure is enforced by requesting exactly one item after
230-
* each consumed element. The internal bounded queue (capacity
231-
* {@value QUEUE_CAPACITY}) absorbs minor timing jitter between
232-
* producer and consumer.</p>
230+
* each consumed element. Demand is signalled <em>before</em> the
231+
* consumer's {@code moveNext()} awaitable completes, so the publisher
232+
* can begin producing the next value while the consumer processes the
233+
* current one — this prevents livelock when producer and consumer
234+
* share the same thread pool.</p>
235+
*
236+
* <p>The internal bounded queue (capacity {@value QUEUE_CAPACITY})
237+
* absorbs minor timing jitter between producer and consumer. All
238+
* signals use blocking {@code put()}, ensuring no items or terminal
239+
* events are silently dropped.</p>
233240
*
234241
* <p><b>Resource management:</b> When the consumer calls
235242
* {@link AsyncStream#close()} (e.g. via {@code break} in a
@@ -277,10 +284,16 @@ public void onNext(T item) {
277284
return;
278285
}
279286
if (!closedRef.get()) {
280-
// Use offer() for value signals — if the queue is full (publisher
281-
// misbehaving with respect to demand), the item is dropped rather
282-
// than blocking the publisher thread indefinitely.
283-
queue.offer(new ValueSignal<>(item));
287+
try {
288+
// Blocking put() guarantees the item reaches the consumer.
289+
// Since demand is capped at 1 (one request(1) per moveNext),
290+
// a well-behaved publisher will never overflow the queue; put()
291+
// still protects against misbehaving publishers by blocking
292+
// rather than silently dropping the value.
293+
queue.put(new ValueSignal<>(item));
294+
} catch (InterruptedException ie) {
295+
Thread.currentThread().interrupt();
296+
}
284297
}
285298
}
286299

@@ -326,10 +339,16 @@ public Awaitable<Boolean> moveNext() {
326339

327340
if (signal instanceof ValueSignal) {
328341
current = ((ValueSignal<T>) signal).value;
329-
cf.complete(Boolean.TRUE);
330-
// Request the next item — back-pressure: one-at-a-time
342+
// Signal demand for the next item BEFORE completing
343+
// the awaitable, so the publisher can begin producing
344+
// the next value while the consumer processes this one.
345+
// Ordering here is critical: if request(1) were called
346+
// after cf.complete(), the consumer could re-enter
347+
// moveNext() and block in take() before demand was
348+
// signalled, creating a livelock.
331349
Flow.Subscription sub = subRef.get();
332350
if (sub != null) sub.request(1);
351+
cf.complete(Boolean.TRUE);
333352
} else if (signal instanceof ErrorSignal) {
334353
streamClosed.set(true);
335354
cf.completeExceptionally(((ErrorSignal) signal).error);
@@ -366,10 +385,14 @@ public void close() {
366385
if (sub != null) sub.cancel();
367386
// Drain the queue and inject a sentinel to unblock a
368387
// concurrent moveNext() that may be blocked in take().
369-
// Using offer() after clear() performs a non-blocking,
370-
// best-effort delivery of the sentinel to any waiter.
388+
// Using blocking put() after clear() guarantees delivery;
389+
// since the queue is freshly cleared, put() will not block.
371390
queue.clear();
372-
queue.offer(COMPLETE_SENTINEL);
391+
try {
392+
queue.put(COMPLETE_SENTINEL);
393+
} catch (InterruptedException ie) {
394+
Thread.currentThread().interrupt();
395+
}
373396
}
374397
}
375398
};

src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,12 @@ public static Expression buildYieldReturnCall(Expression arg) {
147147

148148
/**
149149
* Builds {@code AsyncSupport.defer($__deferScope__, action)}.
150-
* The synthetic defer-scope variable is injected automatically.
150+
* The synthetic defer-scope variable is injected by
151+
* {@link #wrapWithDeferScope(Statement)} during AST transformation.
152+
* <p>
153+
* The parser validates that {@code defer} only appears inside an async
154+
* context (async method or async closure); using it elsewhere is a
155+
* compile-time error.
151156
*
152157
* @param action the deferred action expression (typically a closure)
153158
* @return an AST node representing the static call

0 commit comments

Comments
 (0)