Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/main/java/groovy/concurrent/AwaitResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ private AwaitResult(T value, Throwable error, boolean success) {
* @param <T> the value type
* @return a success result wrapping the value
*/
@SuppressWarnings("unchecked")
public static <T> AwaitResult<T> success(Object value) {
return new AwaitResult<>((T) value, null, true);
public static <T> AwaitResult<T> success(T value) {
return new AwaitResult<>(value, null, true);
}

/**
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/groovy/concurrent/Awaitable.java
Original file line number Diff line number Diff line change
Expand Up @@ -439,9 +439,10 @@ static <T> Awaitable<T> any(Object... sources) {
* Returns an {@code Awaitable} that completes with the result of the first
* source that succeeds. Individual failures are silently absorbed; only
* when <em>all</em> sources have failed does the returned awaitable reject
* with an {@link IllegalStateException} whose
* {@linkplain Throwable#getSuppressed() suppressed} array contains every
* individual error.
* with an aggregate {@link java.util.concurrent.CompletionException}
* (message {@code "All N tasks failed"}, cause = first failure, remaining
* failures as suppressed). Under {@code await} exception transparency the
* cause is rethrown.
* <p>
* This is the Groovy equivalent of JavaScript's {@code Promise.any()}.
* Contrast with {@link #any(Object...)} which returns the first result to
Expand Down
17 changes: 15 additions & 2 deletions src/main/java/groovy/concurrent/package-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,23 @@
* Structured concurrency abstractions for asynchronous and parallel programming.
*
* <p>
* Provides high-level concurrency primitives: {@link groovy.concurrent.Actor} (message-passing),
* {@link groovy.concurrent.AsyncScope} (structured concurrency), {@link groovy.concurrent.Pool} (thread management),
* Language-facing async types:
* <ul>
* <li>{@link groovy.concurrent.Awaitable} — async computation handle and
* static combinator surface used with the {@code await} keyword</li>
* <li>{@link groovy.concurrent.AwaitResult} — success/failure value from
* {@link groovy.concurrent.Awaitable#allSettled(Object...)}</li>
* <li>{@link groovy.concurrent.AsyncScope} — structured concurrency</li>
* <li>{@link groovy.concurrent.AsyncChannel} — CSP-style async channel</li>
* <li>{@link groovy.concurrent.AwaitableAdapter} /
* {@link groovy.concurrent.AwaitableAdapterRegistry} — SPI for
* third-party async types (RxJava, Reactor, …)</li>
* </ul>
* Additional high-level primitives: {@link groovy.concurrent.Actor}
* (message-passing), {@link groovy.concurrent.Pool} (thread management),
* and {@link groovy.concurrent.ParallelScope} (combined async+parallel).
* Supports virtual threads (JDK 21+) and fail-fast exception handling.
* Runtime implementation details live in {@code org.apache.groovy.runtime.async}.
* </p>
*/
package groovy.concurrent;
49 changes: 2 additions & 47 deletions src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -539,20 +539,7 @@
visitAnnotationsOpt(ctx.annotationsOpt()).forEach(forStatement::addStatementAnnotation);

if (isForAwait) {
// Transform collection expression: wrap in AsyncSupport.toIterable()
// and wrap the loop in try/finally to ensure cleanup on break/exception
Expression original = forStatement.getCollectionExpression();
String tempVar = "__forAwaitSource" + ctx.hashCode();
Expression toIterableCall = AsyncTransformHelper.buildToIterableCall(original);

// var $temp = AsyncSupport.toIterable(original)
Statement declStmt = stmt(declX(varX(tempVar), toIterableCall));
forStatement.setCollectionExpression(varX(tempVar));

// try { for (...) { body } } finally { AsyncSupport.closeIterable($temp) }
Statement finallyStmt = stmt(AsyncTransformHelper.buildCloseIterableCall(varX(tempVar)));
TryCatchStatement tryCatch = new TryCatchStatement(forStatement, finallyStmt);
return configureAST(block(declStmt, tryCatch), ctx);
return configureAST(AsyncTransformHelper.wrapForAwaitLoop(forStatement), ctx);
}

return forStatement;
Expand Down Expand Up @@ -924,7 +911,7 @@
public Statement visitLabeledStmtAlt(final LabeledStmtAltContext ctx) {
Statement statement = (Statement) this.visit(ctx.statement());

statement.addStatementLabel(this.visitIdentifier(ctx.identifier()));

Check failure on line 914 in src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Fix this access that will throw a NullPointerException when executed.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ-AnyQDgDQPfj9TTXAa&open=AZ-AnyQDgDQPfj9TTXAa&pullRequest=2725

return statement;
}
Expand Down Expand Up @@ -964,7 +951,7 @@
public ExpressionStatement visitDeferStmtAlt(final DeferStmtAltContext ctx) {
Expression action;
ExpressionStatement stmtExprStmt = (ExpressionStatement) this.visit(ctx.statementExpression());
Expression expr = stmtExprStmt.getExpression();

Check failure on line 954 in src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Fix this access that will throw a NullPointerException when executed.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ-AnyQDgDQPfj9TTXAb&open=AZ-AnyQDgDQPfj9TTXAb&pullRequest=2725
if (expr instanceof ClosureExpression) {
action = expr;
} else {
Expand Down Expand Up @@ -3090,39 +3077,7 @@
@Override
public Expression visitAsyncClosureExprAlt(final AsyncClosureExprAltContext ctx) {
ClosureExpression closure = this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression());
boolean hasYieldReturn = AsyncTransformHelper.containsYieldReturn(closure.getCode());
boolean hasDefer = AsyncTransformHelper.containsDefer(closure.getCode());

if (hasDefer) {
Statement wrappedBody = AsyncTransformHelper.wrapWithDeferScope(closure.getCode());
ClosureExpression newClosure = new ClosureExpression(closure.getParameters(), wrappedBody);
newClosure.setVariableScope(closure.getVariableScope());
newClosure.setSourcePosition(closure);
closure = newClosure;
}

if (hasYieldReturn) {
// Inject synthetic $__asyncGen__ as first parameter
Parameter genParam = AsyncTransformHelper.createGenParam();
Parameter[] existingParams = closure.getParameters();
boolean hasUserParams = existingParams != null && existingParams.length > 0;
Parameter[] newParams;
if (hasUserParams) {
newParams = new Parameter[existingParams.length + 1];
newParams[0] = genParam;
System.arraycopy(existingParams, 0, newParams, 1, existingParams.length);
} else {
newParams = new Parameter[]{genParam};
}
ClosureExpression genClosure = new ClosureExpression(newParams, closure.getCode());
genClosure.setVariableScope(closure.getVariableScope());
genClosure.setSourcePosition(closure);
return configureAST(AsyncTransformHelper.buildAsyncGeneratorCall(
new ArgumentListExpression(genClosure)), ctx);
} else {
return configureAST(AsyncTransformHelper.buildAsyncCall(
new ArgumentListExpression(closure)), ctx);
}
return configureAST(AsyncTransformHelper.transformAsyncClosure(closure), ctx);
}

@Override
Expand Down Expand Up @@ -3354,7 +3309,7 @@
new BinaryExpression(
this.visitVariableNames(ctx.left),
this.createGroovyToken(ctx.op),
((ExpressionStatement) this.visit(ctx.right)).getExpression()),

Check failure on line 3312 in src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Fix this access that will throw a NullPointerException when executed.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ-AnyQDgDQPfj9TTXAd&open=AZ-AnyQDgDQPfj9TTXAd&pullRequest=2725
ctx);
}

Expand Down Expand Up @@ -4660,7 +4615,7 @@
return (ConstantExpression) expression;
}

return configureAST(new ConstantExpression(expression.getText()), expression);

Check failure on line 4618 in src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Fix this access that will throw a NullPointerException when executed.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ-AnyQDgDQPfj9TTXAc&open=AZ-AnyQDgDQPfj9TTXAc&pullRequest=2725
}

private BinaryExpression createBinaryExpression(final ExpressionContext left, final Token op, final ExpressionContext right) {
Expand Down
128 changes: 128 additions & 0 deletions src/main/java/org/apache/groovy/runtime/async/AsyncExecutors.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.groovy.runtime.async;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
* Executor and scheduler configuration for the async runtime.
* <p>
* Package-private implementation detail of {@link AsyncSupport}. On JDK&nbsp;21+
* the default executor is a virtual-thread-per-task executor; earlier JDKs use a
* bounded cached daemon pool sized by {@code groovy.async.parallelism}.
*
* @since 6.0.0
*/
final class AsyncExecutors {

private static final boolean VIRTUAL_THREADS_AVAILABLE;
private static final Executor VIRTUAL_THREAD_EXECUTOR;
private static final Executor FALLBACK_EXECUTOR;

static {
Executor vtExecutor = null;
boolean vtAvailable = false;
try {
MethodHandle mh = MethodHandles.lookup().findStatic(
Executors.class, "newVirtualThreadPerTaskExecutor",
MethodType.methodType(ExecutorService.class));
vtExecutor = (Executor) mh.invoke();
vtAvailable = true;
} catch (Throwable ignored) {
// JDK < 21 — virtual threads not available
}
VIRTUAL_THREAD_EXECUTOR = vtExecutor;
VIRTUAL_THREADS_AVAILABLE = vtAvailable;

int maxThreads = getIntegerSafe("groovy.async.parallelism", 256);
if (!VIRTUAL_THREADS_AVAILABLE) {
FALLBACK_EXECUTOR = new ThreadPoolExecutor(
0, maxThreads,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
r -> {
Thread t = new Thread(r);
t.setDaemon(true);
@SuppressWarnings("deprecation")
long id = t.getId();
t.setName("groovy-async-" + id);
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy());
} else {
FALLBACK_EXECUTOR = null;
}
}

private static volatile Executor defaultExecutor = createDefaultExecutor();

Check warning on line 82 in src/main/java/org/apache/groovy/runtime/async/AsyncExecutors.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a thread-safe type; adding "volatile" is not enough to make this field thread-safe.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ-AnyVGgDQPfj9TTXAe&open=AZ-AnyVGgDQPfj9TTXAe&pullRequest=2725

private static final ScheduledExecutorService SCHEDULER =
Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "groovy-async-scheduler");
t.setDaemon(true);
return t;
});

private AsyncExecutors() { }

static boolean isVirtualThreadsAvailable() {
return VIRTUAL_THREADS_AVAILABLE;
}

static Executor getExecutor() {
return defaultExecutor;
}

/**
* Sets the executor used for async tasks. Passing {@code null} restores the
* platform default (virtual threads on JDK&nbsp;21+, cached pool otherwise).
*/
static void setExecutor(Executor executor) {
defaultExecutor = executor != null ? executor : createDefaultExecutor();
}

static void resetExecutor() {
defaultExecutor = createDefaultExecutor();
}

static ScheduledExecutorService getScheduler() {
return SCHEDULER;
}

private static Executor createDefaultExecutor() {
return VIRTUAL_THREADS_AVAILABLE ? VIRTUAL_THREAD_EXECUTOR : FALLBACK_EXECUTOR;
}

private static int getIntegerSafe(String name, int defaultValue) {
try {
return Integer.getInteger(name, defaultValue);
} catch (SecurityException ignore) {
return defaultValue;
}
}
}
Loading
Loading