Copilot commented on code in PR #2386: URL: https://github.com/apache/groovy/pull/2386#discussion_r2869217476
########## src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy: ########## @@ -0,0 +1,1138 @@ +/* + * 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.codehaus.groovy.transform + +import org.junit.jupiter.api.Test + +import static groovy.test.GroovyAssert.assertScript + +/** + * Tests for virtual thread integration, executor configuration, exception + * handling consistency across async methods/closures/lambdas, and coverage + * improvements for the async/await feature. + * + * @since 6.0.0 + */ +final class AsyncVirtualThreadTest { + + // ---- Virtual thread detection and usage ---- + + @Test + void testVirtualThreadsAvailable() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + // JDK 21+ should have virtual threads + def jdkVersion = Runtime.version().feature() + if (jdkVersion >= 21) { + assert isVirtualThreadsAvailable() + } + ''' + } + + @Test + void testAsyncMethodRunsOnVirtualThread() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + class VTService { + async checkThread() { + return Thread.currentThread().isVirtual() + } + } + + if (isVirtualThreadsAvailable()) { + def service = new VTService() + def result = service.checkThread().get() + assert result == true + } + ''' + } + + @Test + void testAsyncClosureRunsOnVirtualThread() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + if (isVirtualThreadsAvailable()) { + def asyncVirtual = async { Thread.currentThread().isVirtual() } + def awaitable = asyncVirtual() + assert await(awaitable) == true + } + ''' + } + + @Test + void testAsyncLambdaRunsOnVirtualThread() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + if (isVirtualThreadsAvailable()) { + def asyncFn = async { x -> Thread.currentThread().isVirtual() } + def result = await(asyncFn(42)) + assert result == true + } + ''' + } + + @Test + void testForAwaitRunsOnVirtualThread() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.AsyncStream + + class VTGenerator { + async generate() { + yield return Thread.currentThread().isVirtual() + } + } + + if (isVirtualThreadsAvailable()) { + def gen = new VTGenerator() + def stream = gen.generate() + def results = [] + for await (item in stream) { + results << item + } + assert results == [true] + } + ''' + } + + @Test + void testHighConcurrencyWithVirtualThreads() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + class HighConcurrency { + async compute(int n) { + Thread.sleep(10) + return n * 2 + } + } + + if (isVirtualThreadsAvailable()) { + def svc = new HighConcurrency() + // Launch 1000 concurrent tasks — trivial with virtual threads + def awaitables = (1..1000).collect { svc.compute(it) } + def results = awaitAll(awaitables as Object[]) + assert results.size() == 1000 + assert results[0] == 2 + assert results[999] == 2000 + } + ''' + } + + // ---- Executor configuration ---- + + @Test + void testCustomExecutorOverride() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.Executors + import java.util.concurrent.atomic.AtomicReference + + def savedExecutor = getExecutor() + try { + def customPool = Executors.newFixedThreadPool(2, { r -> + def t = new Thread(r) + t.setName("custom-async-" + t.getId()) + t + }) + setExecutor(customPool) + + def asyncName = async { + Thread.currentThread().getName() + } + def awaitable = asyncName() + def threadName = await(awaitable) + assert threadName.startsWith("custom-async-") + } finally { + setExecutor(savedExecutor) + } + ''' + } + + @Test + void testSetExecutorNullResetsToDefault() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.Executors + + def originalExecutor = getExecutor() + // Set a custom executor + setExecutor(Executors.newSingleThreadExecutor()) + assert getExecutor() != originalExecutor + // Reset to null — should restore default + setExecutor(null) + def restored = getExecutor() + assert restored != null + // Verify it works + def awaitable = groovy.concurrent.AsyncUtils.async { 42 } + assert groovy.concurrent.AsyncUtils.await(awaitable) == 42 + // Restore original + setExecutor(originalExecutor) + ''' + } + + @Test + void testAsyncMethodWithCustomExecutorField() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.Executor + import java.util.concurrent.Executors + + class CustomExecutorService { + static Executor myPool = Executors.newFixedThreadPool(1, { r -> + def t = new Thread(r) + t.setName("my-pool-thread") + t + }) + + @Async(executor = "myPool") + def doWork() { + return Thread.currentThread().getName() + } + } + + def svc = new CustomExecutorService() + def result = svc.doWork().get() + assert result.startsWith("my-pool-thread") + ''' + } + + // ---- Exception handling consistency: async method vs closure vs lambda ---- + + @Test + void testCheckedExceptionConsistencyAcrossAsyncMethods() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + class ExcService { + async failWithChecked() { + throw new java.io.IOException("async method error") + } + } + + def svc = new ExcService() + try { + await(svc.failWithChecked()) + assert false : "Should have thrown" + } catch (java.io.IOException e) { + assert e.message == "async method error" + } + ''' + } + + @Test + void testCheckedExceptionConsistencyAcrossClosures() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + def asyncIOError = async { throw new java.io.IOException("closure error") } + def awaitable = asyncIOError() + try { + await(awaitable) + assert false : "Should have thrown" + } catch (java.io.IOException e) { + assert e.message == "closure error" + } + ''' + } + + @Test + void testCheckedExceptionConsistencyAcrossLambdas() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + def asyncFn = async { x -> throw new java.io.IOException("lambda error: ${x}") } + try { + await(asyncFn("test")) + assert false : "Should have thrown" + } catch (java.io.IOException e) { + assert e.message == "lambda error: test" + } + ''' + } + + @Test + void testRuntimeExceptionConsistencyAllForms() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + // Form 1: async method + class Svc { + async failMethod() { throw new IllegalStateException("from method") } + } + + // Form 2: async closure + def asyncClosure = async { throw new IllegalArgumentException("from closure") } + def closure = asyncClosure() + + // Form 3: async lambda with params + def lambda = async { x -> throw new UnsupportedOperationException("from lambda") } + + // All should throw the exact exception type (no wrapping) + try { await(new Svc().failMethod()); assert false } + catch (IllegalStateException e) { assert e.message == "from method" } + + try { await(closure); assert false } + catch (IllegalArgumentException e) { assert e.message == "from closure" } + + try { await(lambda("x")); assert false } + catch (UnsupportedOperationException e) { assert e.message == "from lambda" } + ''' + } + + @Test + void testErrorPropagationConsistencyAllForms() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + class ErrorSvc { + async fail() { throw new StackOverflowError("method") } + } + + // async method + try { await(new ErrorSvc().fail()); assert false } + catch (StackOverflowError e) { assert e.message == "method" } + + // async closure + def asyncSOE = async { throw new StackOverflowError("closure") } + try { await(asyncSOE()); assert false } + catch (StackOverflowError e) { assert e.message == "closure" } + + // async lambda + def fn = async { x -> throw new StackOverflowError("lambda") } + try { await(fn(1)); assert false } + catch (StackOverflowError e) { assert e.message == "lambda" } + ''' + } + + // ---- Async stream (yield return) consistency ---- + + @Test + void testYieldReturnConsistencyMethodVsClosure() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + // Form 1: async method with yield return + class GenSvc { + async range(int n) { + for (int i = 1; i <= n; i++) { + yield return i + } + } + } + + // Form 2: async closure with yield return + def asyncClosureGen = async { for (int i = 1; i <= 3; i++) { yield return i * 10 } } + def closureGen = asyncClosureGen() + + // Form 3: async lambda with yield return + params + def lambdaGen = async { n -> for (int i = 1; i <= n; i++) { yield return i * 100 } } + + // Verify all produce correct streams + def methodResults = [] + for await (item in new GenSvc().range(3)) { methodResults << item } + assert methodResults == [1, 2, 3] + + def closureResults = [] + for await (item in closureGen) { closureResults << item } + assert closureResults == [10, 20, 30] + + def lambdaResults = [] + for await (item in lambdaGen(3)) { lambdaResults << item } + assert lambdaResults == [100, 200, 300] + ''' + } + + @Test + void testYieldReturnExceptionConsistencyAllForms() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + // async method generator with error + class FailGen { + async failing() { + yield return 1 + throw new java.io.IOException("gen method error") + } + } + + // async closure generator with error + def asyncFailGen = async { + yield return 10 + throw new java.io.IOException("gen closure error") + } + def closureFailGen = asyncFailGen() + + // async lambda generator with error + def lambdaFailGen = async { x -> + yield return x + throw new java.io.IOException("gen lambda error") + } + + // All should propagate IOException through for-await + for (gen in [new FailGen().failing(), closureFailGen, lambdaFailGen(100)]) { + def items = [] + try { + for await (item in gen) { items << item } + assert false : "Should have thrown" + } catch (java.io.IOException e) { + assert e.message.contains("gen") + assert items.size() == 1 + } + } + ''' + } + + // ---- executeAsync / executeAsyncVoid (unified path) ---- + + @Test + void testExecuteAsyncWithCustomExecutor() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.Executors + + def pool = Executors.newSingleThreadExecutor({ r -> + def t = new Thread(r) + t.setName("exec-async-test") + t + }) + + def awaitable = executeAsync({ -> + Thread.currentThread().getName() + }, pool) + def result = await(awaitable) + assert result.startsWith("exec-async-test") + pool.shutdown() + ''' + } + + @Test + void testExecuteAsyncVoidWithCustomExecutor() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.Executors + import java.util.concurrent.atomic.AtomicBoolean + + def pool = Executors.newSingleThreadExecutor() + def executed = new AtomicBoolean(false) + + def awaitable = executeAsyncVoid({ -> + executed.set(true) + }, pool) + def _v1 = await(awaitable) + assert executed.get() + pool.shutdown() + ''' + } + + @Test + void testAsyncVoidMethodReturnsAwaitable() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + import java.util.concurrent.atomic.AtomicInteger + + class VoidService { + static AtomicInteger counter = new AtomicInteger(0) + + @Async + void increment() { + counter.incrementAndGet() + } + } + + def svc = new VoidService() + def awaitable = svc.increment() + assert awaitable instanceof Awaitable + def _v2 = await(awaitable) + assert VoidService.counter.get() == 1 + ''' + } + + // ---- Edge cases and coverage improvements ---- + + @Test + void testAwaitNull() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + def result = await((Object) null) + assert result == null + ''' + } + + @Test + void testAwaitAlreadyCompletedAwaitable() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + + def awaitable = Awaitable.of(42) + assert awaitable.isDone() + def result = await(awaitable) + assert result == 42 + ''' + } + + @Test + void testAwaitFailedAwaitable() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + + def awaitable = Awaitable.failed(new RuntimeException("pre-failed")) + assert awaitable.isCompletedExceptionally() + try { + await(awaitable) + assert false + } catch (RuntimeException e) { + assert e.message == "pre-failed" + } + ''' + } + + @Test + void testAwaitCancelledAwaitable() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import org.apache.groovy.runtime.async.GroovyPromise + import java.util.concurrent.CompletableFuture + import java.util.concurrent.CancellationException + + def cf = new CompletableFuture() + def awaitable = GroovyPromise.of(cf) + awaitable.cancel() + assert awaitable.isCancelled() + try { + await(awaitable) + assert false + } catch (CancellationException e) { + // expected + } + ''' + } + + @Test + void testAsyncWithReturnValue() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + def asyncHello = async { return "hello" } + def awaitable = asyncHello() + assert await(awaitable) == "hello" + ''' + } + + @Test + void testAsyncWithNullReturnValue() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + def asyncNull = async { return null } + def awaitable = asyncNull() + assert await(awaitable) == null + ''' + } + + @Test + void testMultipleConcurrentAsyncGenerators() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + def asyncGen1 = async { for (int i = 0; i < 5; i++) { yield return "A${i}" } } + def gen1 = asyncGen1() + def asyncGen2 = async { for (int i = 0; i < 5; i++) { yield return "B${i}" } } + def gen2 = asyncGen2() + + def results1 = [] + def results2 = [] + for await (item in gen1) { results1 << item } + for await (item in gen2) { results2 << item } + assert results1 == ["A0", "A1", "A2", "A3", "A4"] + assert results2 == ["B0", "B1", "B2", "B3", "B4"] + ''' + } + + @Test + void testEmptyAsyncStream() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + class EmptyGen { + async empty() { + // A generator must have yield return to be detected as one. + // This has one but it's unreachable at runtime. + if (false) yield return "unreachable" + } + } + + def results = [] + for await (item in new EmptyGen().empty()) { + results << item + } + assert results.isEmpty() + ''' + } + + @Test + void testAwaitableCompositionWithThen() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + + def asyncOriginal = async { 10 } + def original = asyncOriginal() + def doubled = original.then { it * 2 } + def result = await(doubled) + assert result == 20 + ''' + } + + @Test + void testAwaitableCompositionWithThenCompose() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + + def asyncFirst = async { 5 } + def first = asyncFirst() + def chained = first.thenCompose { v -> def asyncCompose = async { v + 10 }; asyncCompose() } + def result = await(chained) + assert result == 15 + ''' + } + + @Test + void testAwaitableExceptionally() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + + def asyncFailing = async { throw new RuntimeException("oops") } + def failing = asyncFailing() + def recovered = failing.exceptionally { e -> "recovered: ${e.message}" } + def result = await(recovered) + assert result == "recovered: oops" + ''' + } + + @Test + void testAwaitWithCompletionStage() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.CompletableFuture + import java.util.concurrent.CompletionStage + + CompletionStage stage = CompletableFuture.supplyAsync { "from stage" } + def result = await(stage) + assert result == "from stage" + ''' + } + + @Test + void testAwaitWithFuture() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.Executors + import java.util.concurrent.Future + + def pool = Executors.newSingleThreadExecutor() + Future future = pool.submit({ "from future" } as java.util.concurrent.Callable) + def result = await(future) + assert result == "from future" + pool.shutdown() + ''' + } + + @Test + void testAwaitAllWithMixedTypes() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + import java.util.concurrent.CompletableFuture + + def asyncA1 = async { 1 } + def a1 = asyncA1() + def a2 = CompletableFuture.supplyAsync { 2 } + def a3 = Awaitable.of(3) + + def results = awaitAll(a1, a2, a3) + assert results == [1, 2, 3] + ''' + } + + @Test + void testAwaitAnyReturnsFirst() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + + def fast = Awaitable.of("fast") + def asyncSlow = async { Thread.sleep(500); "slow" } + def slow = asyncSlow() + + def result = awaitAny(fast, slow) + assert result == "fast" + ''' + } + + @Test + void testAwaitAllSettledMixed() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + def asyncSuccess = async { "ok" } + def success = asyncSuccess() + def asyncFailure = async { throw new RuntimeException("fail") } + def failure = asyncFailure() + + def results = awaitAllSettled(success, failure) + assert results.size() == 2 + assert results[0].isSuccess() + assert results[0].getValue() == "ok" + assert results[1].isFailure() + assert results[1].getError().message == "fail" + ''' + } + + @Test + void testAsyncMethodWithPrimitiveReturnType() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + class PrimService { + @Async + int computeInt() { return 42 } + + @Async + boolean checkBool() { return true } + + @Async + double computeDouble() { return 3.14 } + } + + def svc = new PrimService() + assert await(svc.computeInt()) == 42 + assert await(svc.checkBool()) == true + assert Math.abs(await(svc.computeDouble()) - 3.14) < 0.001 + ''' + } + + @Test + void testDeepUnwrapNestedExceptions() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.CompletionException + import java.util.concurrent.ExecutionException + + // Create deeply nested exception chain + def original = new java.io.IOException("deep") + def wrapped = new CompletionException(new ExecutionException( + new java.lang.reflect.UndeclaredThrowableException( + new java.lang.reflect.InvocationTargetException(original)))) + + def cf = new java.util.concurrent.CompletableFuture() + cf.completeExceptionally(wrapped) + + try { + await(cf) + assert false + } catch (java.io.IOException e) { + assert e.message == "deep" + } + ''' + } + + @Test + void testAsyncWithNestedAwait() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + class NestedService { + async inner(int v) { return v * 2 } + + async outer(int v) { + def intermediate = await inner(v) + return await inner(intermediate) + } + } + + def svc = new NestedService() + def result = await(svc.outer(5)) + assert result == 20 // 5 * 2 * 2 + ''' + } + + @Test + void testAsyncClosureWithNestedAwait() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + def inner = async { x -> x + 1 } + def asyncOuter = async { + def v1 = await(inner(10)) + def v2 = await(inner(v1)) + return v2 + } + def outer = asyncOuter() + assert await(outer) == 12 // 10 + 1 + 1 + ''' + } + + @Test + void testParallelAwaitInAsyncMethod() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.CompletableFuture + + class ParallelService { + CompletableFuture<Integer> compute(int v) { + return CompletableFuture.supplyAsync { + Thread.sleep(50) + return v * 2 + } + } + + async parallel() { + def f1 = compute(1) + def f2 = compute(2) + def f3 = compute(3) + return await(f1) + await(f2) + await(f3) + } + } + + def svc = new ParallelService() + long start = System.currentTimeMillis() + def result = await(svc.parallel()) + long elapsed = System.currentTimeMillis() - start + assert result == 12 // 2 + 4 + 6 + assert elapsed < 500 // parallel, not sequential + ''' + } + + @Test + void testToAsyncStreamConversion() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.AsyncStream + import groovy.concurrent.Awaitable + import groovy.concurrent.AwaitableAdapter + import groovy.concurrent.AwaitableAdapterRegistry + + // Register an adapter for List that provides an AsyncStream + AwaitableAdapterRegistry.register(new AwaitableAdapter() { + boolean supportsAwaitable(Class type) { return false } + Awaitable toAwaitable(Object source) { return null } + boolean supportsAsyncStream(Class type) { return List.isAssignableFrom(type) } + AsyncStream toAsyncStream(Object source) { + def list = (List) source + def iter = list.iterator() + return new AsyncStream() { + def current + Awaitable<Boolean> moveNext() { + if (iter.hasNext()) { + current = iter.next() + return Awaitable.of(true) + } + return Awaitable.of(false) + } + Object getCurrent() { return current } + } + } + }) + + def results = [] + def stream = toAsyncStream([10, 20, 30]) + for await (item in stream) { + results << item + } + assert results == [10, 20, 30] + ''' + } + + @Test + void testAwaitObjectDispatchesToCorrectOverload() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + import java.util.concurrent.CompletableFuture + + // Test Object overload dispatch for each type + Object a = Awaitable.of("awaitable") + Object b = CompletableFuture.completedFuture("cf") + Object c = CompletableFuture.completedFuture("stage") as java.util.concurrent.CompletionStage + + assert await(a) == "awaitable" + assert await(b) == "cf" + assert await(c) == "stage" + ''' + } + + @Test + void testAsyncMethodWithParameters() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + + class ParamService { + async add(int a, int b) { + return a + b + } + + async greet(String name) { + return "Hello, ${name}!" + } + } + + def svc = new ParamService() + assert await(svc.add(3, 4)) == 7 + assert await(svc.greet("Groovy")) == "Hello, Groovy!" + ''' + } + + @Test + void testAsyncChainedMethods() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + + class Pipeline { + async step1() { return 1 } + + async step2(Awaitable<Integer> input) { + return await(input) + 10 + } + + async step3(Awaitable<Integer> input) { + return await(input) * 2 + } + } + + def p = new Pipeline() + def r1 = p.step1() + def r2 = p.step2(r1) + def r3 = p.step3(r2) + assert r3.get() == 22 + ''' + } + + @Test + void testAwaitSyntaxWithBinaryOperations() { + assertScript ''' + import groovy.transform.Async + import static groovy.concurrent.AsyncUtils.* + import groovy.concurrent.Awaitable + + class MathService { + async compute(int v) { return v } + } + + def svc = new MathService() + // Test that await properly integrates with binary operators + def a = svc.compute(10) + def b = svc.compute(20) + def r1 = await(a) + def r2 = await(b) + assert r1 + r2 == 30 + ''' + } + + @Test + void testAsyncClosureWithoutYieldReturnIsNotGenerator() { + assertScript ''' + import static groovy.concurrent.AsyncUtils.* + + // Closure with no yield return produces an Awaitable (not AsyncStream) + def asyncResult = async { 42 } + def result = asyncResult() + assert await(result) == 42 + + def asyncResultNull = async { /* empty body */ } + def resultNull = asyncResultNull() + assert await(resultNull) == null + ''' + } + + @Test + void testGroovyPromiseToString() { + assertScript ''' + import org.apache.groovy.runtime.async.GroovyPromise + import groovy.concurrent.Awaitable + import java.util.concurrent.CompletableFuture + + def pending = new CompletableFuture() + def promise = GroovyPromise.of(pending) + assert promise.toString() == "GroovyPromise{pending}" + + pending.complete(42) + assert promise.toString() == "GroovyPromise{completed}" + + def failed = GroovyPromise.of(CompletableFuture.failedFuture(new RuntimeException())) + assert failed.toString() == "GroovyPromise{failed}" + ''' + } + + @Test + void testGroovyPromiseGetWithTimeout() { + assertScript ''' + import org.apache.groovy.runtime.async.GroovyPromise + import static groovy.concurrent.AsyncUtils.* + import java.util.concurrent.TimeUnit + import java.util.concurrent.TimeoutException + + def asyncTimeout = async { Thread.sleep(5000); "never" } + def awaitable = asyncTimeout() + try { + awaitable.get(50, TimeUnit.MILLISECONDS) + assert false : "Should have timed out" + } catch (TimeoutException e) { + // expected Review Comment: This test starts an async task that sleeps for 5 seconds and never cancels or joins it. Even though the test only checks get(timeout), the background task will keep running and can slow down the suite and consume executor threads. Consider using a much smaller sleep, or cancel the Awaitable in the TimeoutException path to avoid leaving long-running tasks behind. ```suggestion // expected awaitable.cancel(true) ``` ########## src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java: ########## @@ -0,0 +1,349 @@ +/* + * 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 groovy.concurrent; + +import org.apache.groovy.runtime.async.GroovyPromise; + +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; + +/** + * Central registry for {@link AwaitableAdapter} instances. + * <p> + * On class-load, adapters are discovered via {@link ServiceLoader} from + * {@code META-INF/services/groovy.concurrent.AwaitableAdapter}. A built-in + * adapter is always present as the lowest-priority fallback, handling: + * <ul> + * <li>{@link CompletableFuture} and {@link CompletionStage}</li> + * <li>{@link Future} (adapted via a blocking wrapper)</li> + * <li>JDK {@link java.util.concurrent.Flow.Publisher} — single-value + * ({@link #toAwaitable}) and multi-value ({@link #toAsyncStream}) + * with backpressure support</li> + * </ul> + * <p> + * Additional adapters can be registered at runtime via {@link #register}. + * + * @see AwaitableAdapter + * @since 6.0.0 + */ +public class AwaitableAdapterRegistry { + + private static final List<AwaitableAdapter> ADAPTERS = new CopyOnWriteArrayList<>(); + + /** + * Optional executor supplier for blocking Future adaptation, to avoid + * starving the common pool. Defaults to null; when set, the provided + * executor is used instead of {@code CompletableFuture.runAsync}'s default. + */ + private static volatile Executor blockingExecutor; + + static { + // SPI-discovered adapters + for (AwaitableAdapter adapter : ServiceLoader.load(AwaitableAdapter.class)) { + ADAPTERS.add(adapter); + } + // Built-in fallback (lowest priority) + ADAPTERS.add(new BuiltInAdapter()); + } + + private AwaitableAdapterRegistry() { } + + /** + * Registers an adapter with higher priority than existing ones. + * + * @return an {@link AutoCloseable} that, when closed, removes this adapter + * from the registry. Useful for test isolation. + */ + public static AutoCloseable register(AwaitableAdapter adapter) { + ADAPTERS.add(0, adapter); + return () -> ADAPTERS.remove(adapter); + } + + /** + * Removes the given adapter from the registry. + * + * @return {@code true} if the adapter was found and removed + */ + public static boolean unregister(AwaitableAdapter adapter) { + return ADAPTERS.remove(adapter); + } + + /** + * Sets the executor used for blocking {@link Future#get()} adaptation. + * When non-null, this executor is used instead of + * {@link CompletableFuture#runAsync(Runnable)}'s default executor to avoid + * pool starvation when many blocking futures are being adapted + * simultaneously. + * + * @param executor the executor to use, or {@code null} to use the default + */ + public static void setBlockingExecutor(Executor executor) { + blockingExecutor = executor; + } + + /** + * Converts the given source to an {@link Awaitable}. + * If the source is already an {@code Awaitable}, it is returned as-is. + * + * @throws IllegalArgumentException if no adapter supports the source type + */ + @SuppressWarnings("unchecked") + public static <T> Awaitable<T> toAwaitable(Object source) { + if (source instanceof Awaitable) return (Awaitable<T>) source; + Class<?> type = source.getClass(); + for (AwaitableAdapter adapter : ADAPTERS) { + if (adapter.supportsAwaitable(type)) { + return adapter.toAwaitable(source); + } + } + throw new IllegalArgumentException( + "No AwaitableAdapter found for type: " + type.getName() + + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader."); + } + + /** + * Converts the given source to an {@link AsyncStream}. + * If the source is already an {@code AsyncStream}, it is returned as-is. + * + * @throws IllegalArgumentException if no adapter supports the source type + */ + @SuppressWarnings("unchecked") + public static <T> AsyncStream<T> toAsyncStream(Object source) { + if (source instanceof AsyncStream) return (AsyncStream<T>) source; + Class<?> type = source.getClass(); + for (AwaitableAdapter adapter : ADAPTERS) { + if (adapter.supportsAsyncStream(type)) { + return adapter.toAsyncStream(source); + } + } + throw new IllegalArgumentException( + "No AsyncStream adapter found for type: " + type.getName() + + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader."); + } + + /** + * Built-in adapter handling JDK {@link CompletableFuture}, {@link CompletionStage}, + * {@link Future}, {@link java.util.concurrent.Flow.Publisher}, + * and {@link Iterable}/{@link Iterator} (for async stream bridging). + * <p> + * {@link CompletionStage} support enables seamless integration with frameworks + * that return {@code CompletionStage} (e.g., Spring's async APIs, Reactor's + * {@code Mono.toFuture()}, etc.) without any additional adapter registration. + * <p> + * {@link java.util.concurrent.Flow.Publisher} support enables seamless + * consumption of reactive streams via {@code for await} without any adapter + * registration. This covers any reactive library that implements the JDK + * standard reactive-streams interface (Reactor, RxJava via adapters, etc.). + */ + private static class BuiltInAdapter implements AwaitableAdapter { + + @Override + public boolean supportsAwaitable(Class<?> type) { + return CompletionStage.class.isAssignableFrom(type) + || Future.class.isAssignableFrom(type) + || java.util.concurrent.Flow.Publisher.class.isAssignableFrom(type); + } + + @Override + @SuppressWarnings("unchecked") + public <T> Awaitable<T> toAwaitable(Object source) { + if (source instanceof CompletionStage) { + return new GroovyPromise<>(((CompletionStage<T>) source).toCompletableFuture()); + } + if (source instanceof java.util.concurrent.Flow.Publisher<?> pub) { + return publisherToAwaitable(pub); + } + if (source instanceof Future) { + Future<T> future = (Future<T>) source; + CompletableFuture<T> cf = new CompletableFuture<>(); + if (future.isDone()) { + completeFrom(cf, future); + } else { + Executor exec = blockingExecutor; + if (exec != null) { + CompletableFuture.runAsync(() -> completeFrom(cf, future), exec); + } else { + CompletableFuture.runAsync(() -> completeFrom(cf, future)); + } + } + return new GroovyPromise<>(cf); + } + throw new IllegalArgumentException("Cannot convert to Awaitable: " + source.getClass()); + } + + @Override + public boolean supportsAsyncStream(Class<?> type) { + return Iterable.class.isAssignableFrom(type) + || Iterator.class.isAssignableFrom(type) + || java.util.concurrent.Flow.Publisher.class.isAssignableFrom(type); + } + + @Override + @SuppressWarnings("unchecked") + public <T> AsyncStream<T> toAsyncStream(Object source) { + if (source instanceof java.util.concurrent.Flow.Publisher<?> pub) { + return publisherToAsyncStream((java.util.concurrent.Flow.Publisher<T>) pub); + } + final Iterator<T> iterator; + if (source instanceof Iterable) { + iterator = ((Iterable<T>) source).iterator(); + } else if (source instanceof Iterator) { + iterator = (Iterator<T>) source; + } else { + throw new IllegalArgumentException("Cannot convert to AsyncStream: " + source.getClass()); + } + return new AsyncStream<T>() { + private T current; + + @Override + public Awaitable<Boolean> moveNext() { + boolean hasNext = iterator.hasNext(); + if (hasNext) current = iterator.next(); + return Awaitable.of(hasNext); + } + + @Override + public T getCurrent() { + return current; + } + }; + } + + /** + * Adapts a {@link java.util.concurrent.Flow.Publisher} to an + * {@link AsyncStream} using a blocking queue to bridge the push-based + * reactive-streams protocol to the pull-based {@code moveNext}/{@code getCurrent} + * pattern. Backpressure is managed by requesting one item at a time: + * each {@code moveNext()} call requests the next item from the upstream + * subscription only after the previous item has been consumed. + */ + @SuppressWarnings("unchecked") + private static <T> AsyncStream<T> publisherToAsyncStream(java.util.concurrent.Flow.Publisher<T> publisher) { + java.util.concurrent.LinkedBlockingQueue<Object> queue = new java.util.concurrent.LinkedBlockingQueue<>(); + Object COMPLETE_SENTINEL = new Object(); + java.util.concurrent.atomic.AtomicReference<java.util.concurrent.Flow.Subscription> subRef = + new java.util.concurrent.atomic.AtomicReference<>(); + + publisher.subscribe(new java.util.concurrent.Flow.Subscriber<T>() { + @Override + public void onSubscribe(java.util.concurrent.Flow.Subscription s) { + subRef.set(s); + s.request(1); + } + + @Override + public void onNext(T item) { + queue.add(item); + } + + @Override + public void onError(Throwable t) { + queue.add(t instanceof Exception ? t : new RuntimeException(t)); + } + + @Override + public void onComplete() { + queue.add(COMPLETE_SENTINEL); + } + }); + + return new AsyncStream<T>() { + private T current; + + @Override + public Awaitable<Boolean> moveNext() { + CompletableFuture<Boolean> cf = new CompletableFuture<>(); + try { + Object item = queue.take(); + if (item == COMPLETE_SENTINEL) { + cf.complete(false); + } else if (item instanceof Throwable) { + cf.completeExceptionally((Throwable) item); + } else { + current = (T) item; + cf.complete(true); + // Request next item for the subsequent moveNext() call + java.util.concurrent.Flow.Subscription sub = subRef.get(); + if (sub != null) sub.request(1); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + cf.completeExceptionally(e); + } Review Comment: publisherToAsyncStream.moveNext() completes the returned Awaitable exceptionally with InterruptedException. When callers use AsyncUtils.await(...) this will bubble out as a (sneaky-thrown) InterruptedException rather than the documented CancellationException-with-interrupt-restored behavior used by other await paths. Consider converting interruption to a CancellationException (and preserving the interrupt flag) instead of completing exceptionally with InterruptedException. ########## src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java: ########## @@ -0,0 +1,349 @@ +/* + * 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 groovy.concurrent; + +import org.apache.groovy.runtime.async.GroovyPromise; + +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; + +/** + * Central registry for {@link AwaitableAdapter} instances. + * <p> + * On class-load, adapters are discovered via {@link ServiceLoader} from + * {@code META-INF/services/groovy.concurrent.AwaitableAdapter}. A built-in + * adapter is always present as the lowest-priority fallback, handling: + * <ul> + * <li>{@link CompletableFuture} and {@link CompletionStage}</li> + * <li>{@link Future} (adapted via a blocking wrapper)</li> + * <li>JDK {@link java.util.concurrent.Flow.Publisher} — single-value + * ({@link #toAwaitable}) and multi-value ({@link #toAsyncStream}) + * with backpressure support</li> + * </ul> + * <p> + * Additional adapters can be registered at runtime via {@link #register}. + * + * @see AwaitableAdapter + * @since 6.0.0 + */ +public class AwaitableAdapterRegistry { + + private static final List<AwaitableAdapter> ADAPTERS = new CopyOnWriteArrayList<>(); + + /** + * Optional executor supplier for blocking Future adaptation, to avoid + * starving the common pool. Defaults to null; when set, the provided + * executor is used instead of {@code CompletableFuture.runAsync}'s default. + */ + private static volatile Executor blockingExecutor; + + static { + // SPI-discovered adapters + for (AwaitableAdapter adapter : ServiceLoader.load(AwaitableAdapter.class)) { + ADAPTERS.add(adapter); + } + // Built-in fallback (lowest priority) + ADAPTERS.add(new BuiltInAdapter()); + } + + private AwaitableAdapterRegistry() { } + + /** + * Registers an adapter with higher priority than existing ones. + * + * @return an {@link AutoCloseable} that, when closed, removes this adapter + * from the registry. Useful for test isolation. + */ + public static AutoCloseable register(AwaitableAdapter adapter) { + ADAPTERS.add(0, adapter); + return () -> ADAPTERS.remove(adapter); + } + + /** + * Removes the given adapter from the registry. + * + * @return {@code true} if the adapter was found and removed + */ + public static boolean unregister(AwaitableAdapter adapter) { + return ADAPTERS.remove(adapter); + } + + /** + * Sets the executor used for blocking {@link Future#get()} adaptation. + * When non-null, this executor is used instead of + * {@link CompletableFuture#runAsync(Runnable)}'s default executor to avoid + * pool starvation when many blocking futures are being adapted + * simultaneously. + * + * @param executor the executor to use, or {@code null} to use the default + */ + public static void setBlockingExecutor(Executor executor) { + blockingExecutor = executor; + } + + /** + * Converts the given source to an {@link Awaitable}. + * If the source is already an {@code Awaitable}, it is returned as-is. + * + * @throws IllegalArgumentException if no adapter supports the source type + */ + @SuppressWarnings("unchecked") + public static <T> Awaitable<T> toAwaitable(Object source) { + if (source instanceof Awaitable) return (Awaitable<T>) source; + Class<?> type = source.getClass(); + for (AwaitableAdapter adapter : ADAPTERS) { + if (adapter.supportsAwaitable(type)) { + return adapter.toAwaitable(source); + } + } + throw new IllegalArgumentException( + "No AwaitableAdapter found for type: " + type.getName() + + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader."); + } + + /** + * Converts the given source to an {@link AsyncStream}. + * If the source is already an {@code AsyncStream}, it is returned as-is. + * + * @throws IllegalArgumentException if no adapter supports the source type + */ + @SuppressWarnings("unchecked") + public static <T> AsyncStream<T> toAsyncStream(Object source) { + if (source instanceof AsyncStream) return (AsyncStream<T>) source; + Class<?> type = source.getClass(); + for (AwaitableAdapter adapter : ADAPTERS) { + if (adapter.supportsAsyncStream(type)) { + return adapter.toAsyncStream(source); + } + } + throw new IllegalArgumentException( + "No AsyncStream adapter found for type: " + type.getName() + + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader."); + } + + /** + * Built-in adapter handling JDK {@link CompletableFuture}, {@link CompletionStage}, + * {@link Future}, {@link java.util.concurrent.Flow.Publisher}, + * and {@link Iterable}/{@link Iterator} (for async stream bridging). + * <p> + * {@link CompletionStage} support enables seamless integration with frameworks + * that return {@code CompletionStage} (e.g., Spring's async APIs, Reactor's + * {@code Mono.toFuture()}, etc.) without any additional adapter registration. + * <p> + * {@link java.util.concurrent.Flow.Publisher} support enables seamless + * consumption of reactive streams via {@code for await} without any adapter + * registration. This covers any reactive library that implements the JDK + * standard reactive-streams interface (Reactor, RxJava via adapters, etc.). + */ + private static class BuiltInAdapter implements AwaitableAdapter { + + @Override + public boolean supportsAwaitable(Class<?> type) { + return CompletionStage.class.isAssignableFrom(type) + || Future.class.isAssignableFrom(type) + || java.util.concurrent.Flow.Publisher.class.isAssignableFrom(type); + } + + @Override + @SuppressWarnings("unchecked") + public <T> Awaitable<T> toAwaitable(Object source) { + if (source instanceof CompletionStage) { + return new GroovyPromise<>(((CompletionStage<T>) source).toCompletableFuture()); + } + if (source instanceof java.util.concurrent.Flow.Publisher<?> pub) { + return publisherToAwaitable(pub); + } + if (source instanceof Future) { + Future<T> future = (Future<T>) source; + CompletableFuture<T> cf = new CompletableFuture<>(); + if (future.isDone()) { + completeFrom(cf, future); + } else { + Executor exec = blockingExecutor; + if (exec != null) { + CompletableFuture.runAsync(() -> completeFrom(cf, future), exec); + } else { + CompletableFuture.runAsync(() -> completeFrom(cf, future)); + } + } + return new GroovyPromise<>(cf); + } + throw new IllegalArgumentException("Cannot convert to Awaitable: " + source.getClass()); + } + + @Override + public boolean supportsAsyncStream(Class<?> type) { + return Iterable.class.isAssignableFrom(type) + || Iterator.class.isAssignableFrom(type) + || java.util.concurrent.Flow.Publisher.class.isAssignableFrom(type); + } + + @Override + @SuppressWarnings("unchecked") + public <T> AsyncStream<T> toAsyncStream(Object source) { + if (source instanceof java.util.concurrent.Flow.Publisher<?> pub) { + return publisherToAsyncStream((java.util.concurrent.Flow.Publisher<T>) pub); + } + final Iterator<T> iterator; + if (source instanceof Iterable) { + iterator = ((Iterable<T>) source).iterator(); + } else if (source instanceof Iterator) { + iterator = (Iterator<T>) source; + } else { + throw new IllegalArgumentException("Cannot convert to AsyncStream: " + source.getClass()); + } + return new AsyncStream<T>() { + private T current; + + @Override + public Awaitable<Boolean> moveNext() { + boolean hasNext = iterator.hasNext(); + if (hasNext) current = iterator.next(); + return Awaitable.of(hasNext); + } + + @Override + public T getCurrent() { + return current; + } + }; + } + + /** + * Adapts a {@link java.util.concurrent.Flow.Publisher} to an + * {@link AsyncStream} using a blocking queue to bridge the push-based + * reactive-streams protocol to the pull-based {@code moveNext}/{@code getCurrent} + * pattern. Backpressure is managed by requesting one item at a time: + * each {@code moveNext()} call requests the next item from the upstream + * subscription only after the previous item has been consumed. + */ + @SuppressWarnings("unchecked") + private static <T> AsyncStream<T> publisherToAsyncStream(java.util.concurrent.Flow.Publisher<T> publisher) { + java.util.concurrent.LinkedBlockingQueue<Object> queue = new java.util.concurrent.LinkedBlockingQueue<>(); + Object COMPLETE_SENTINEL = new Object(); + java.util.concurrent.atomic.AtomicReference<java.util.concurrent.Flow.Subscription> subRef = + new java.util.concurrent.atomic.AtomicReference<>(); + + publisher.subscribe(new java.util.concurrent.Flow.Subscriber<T>() { + @Override + public void onSubscribe(java.util.concurrent.Flow.Subscription s) { + subRef.set(s); + s.request(1); + } + + @Override + public void onNext(T item) { + queue.add(item); + } + + @Override + public void onError(Throwable t) { + queue.add(t instanceof Exception ? t : new RuntimeException(t)); + } Review Comment: In the Flow.Publisher→AsyncStream adapter, onError wraps non-Exception Throwables (including Error) into RuntimeException, which changes the propagated type and defeats the “Error passes through unwrapped” semantics used elsewhere (e.g., AsyncSupport.rethrowUnwrapped / AsyncStreamGenerator). Consider enqueueing the original Throwable and, when consuming, rethrowing Error directly so the original type is preserved. ########## src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java: ########## @@ -865,6 +913,35 @@ public ReturnStatement visitYieldStmtAlt(final YieldStmtAltContext ctx) { return configureAST(this.visitYieldStatement(ctx.yieldStatement()), ctx); } + @Override + public ExpressionStatement visitYieldReturnStmtAlt(final YieldReturnStmtAltContext ctx) { + Expression expr = (Expression) this.visit(ctx.expression()); + Expression yieldCall = callX( + AsyncTransformHelper.ASYNC_SUPPORT_TYPE, AsyncTransformHelper.YIELD_RETURN_METHOD, + new ArgumentListExpression(expr)); + return configureAST(new ExpressionStatement(yieldCall), ctx); + } + + @Override + public ExpressionStatement visitDeferStmtAlt(final DeferStmtAltContext ctx) { + Expression action; + if (ctx.closureOrLambdaExpression() != null) { + action = this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression()); + } else { + // Wrap the statement expression in a closure: { -> expr } + ExpressionStatement stmtExprStmt = (ExpressionStatement) this.visit(ctx.statementExpression()); + ClosureExpression wrapper = closureX(Parameter.EMPTY_ARRAY, + block(stmtExprStmt)); + wrapper.setSourcePosition(stmtExprStmt); + action = wrapper; + } + // Emit: AsyncSupport.defer($__deferScope__, action) + Expression deferCall = callX( + AsyncTransformHelper.ASYNC_SUPPORT_TYPE, AsyncTransformHelper.DEFER_METHOD, + new ArgumentListExpression(varX(AsyncTransformHelper.DEFER_SCOPE_VAR), action)); Review Comment: visitDeferStmtAlt always emits AsyncSupport.defer($__deferScope__, action), which relies on the synthetic $__deferScope__ variable being injected. If `defer` is used outside an async/defer-wrapped context, this likely fails with a confusing missing-variable/NPE instead of a clear error. Consider emitting a scope-less runtime call (like yieldReturn’s single-arg safety overload) and rewriting/injecting the scope only when the compiler has created it, or otherwise producing a compile-time error when defer is used in an unsupported context. ```suggestion // Emit: AsyncSupport.defer(action) Expression deferCall = callX( AsyncTransformHelper.ASYNC_SUPPORT_TYPE, AsyncTransformHelper.DEFER_METHOD, new ArgumentListExpression(action)); ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
