This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch feat/virtual-thread-promise-factory-seed
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 5722a426e543282de147bd3f6ebb4864eddfc5ec
Author: James Fredley <[email protected]>
AuthorDate: Fri Jul 10 12:57:58 2026 -0400

    Add VirtualThreadPromiseFactory seed
    
    Introduce virtual-thread PromiseFactory and document GPars deprecation path.
    
    Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]
---
 .../async/factory/PromiseFactoryBuilder.groovy     |  11 +-
 .../future/VirtualThreadPromiseFactory.groovy      | 111 +++++++++++++++++++++
 .../async/VirtualThreadPromiseFactorySpec.groovy   |  53 ++++++++++
 grails-doc/src/en/guide/async/asyncPromises.adoc   |   4 +
 4 files changed, 177 insertions(+), 2 deletions(-)

diff --git 
a/grails-async/core/src/main/groovy/org/grails/async/factory/PromiseFactoryBuilder.groovy
 
b/grails-async/core/src/main/groovy/org/grails/async/factory/PromiseFactoryBuilder.groovy
index 6960d24b16..6bdce0ad8d 100644
--- 
a/grails-async/core/src/main/groovy/org/grails/async/factory/PromiseFactoryBuilder.groovy
+++ 
b/grails-async/core/src/main/groovy/org/grails/async/factory/PromiseFactoryBuilder.groovy
@@ -23,6 +23,7 @@ import grails.async.PromiseFactory
 import groovy.transform.CompileStatic
 import groovy.util.logging.Slf4j
 import org.grails.async.factory.future.CachedThreadPoolPromiseFactory
+import org.grails.async.factory.future.VirtualThreadPromiseFactory
 
 /**
  * Constructs the default promise factory
@@ -43,8 +44,14 @@ class PromiseFactoryBuilder {
 
         PromiseFactory promiseFactory
         if (promiseFactories.isEmpty()) {
-            log.debug('No PromiseFactory implementation found. Using default 
ExecutorService promise factory.')
-            promiseFactory = new CachedThreadPoolPromiseFactory()
+            if (System.getProperty('grails.async.promiseFactory') == 
'virtual-thread') {
+                log.debug('No PromiseFactory implementation found. Using 
virtual thread promise factory.')
+                promiseFactory = new VirtualThreadPromiseFactory()
+            }
+            else {
+                log.debug('No PromiseFactory implementation found. Using 
default ExecutorService promise factory.')
+                promiseFactory = new CachedThreadPoolPromiseFactory()
+            }
         }
         else {
             promiseFactory = promiseFactories.first()
diff --git 
a/grails-async/core/src/main/groovy/org/grails/async/factory/future/VirtualThreadPromiseFactory.groovy
 
b/grails-async/core/src/main/groovy/org/grails/async/factory/future/VirtualThreadPromiseFactory.groovy
new file mode 100644
index 0000000000..39b163d175
--- /dev/null
+++ 
b/grails-async/core/src/main/groovy/org/grails/async/factory/future/VirtualThreadPromiseFactory.groovy
@@ -0,0 +1,111 @@
+/*
+ *  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
+ *
+ *    https://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.grails.async.factory.future
+
+import java.util.concurrent.Callable
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+import groovy.transform.AutoFinal
+import groovy.transform.CompileStatic
+
+import jakarta.annotation.PreDestroy
+
+import grails.async.Promise
+import grails.async.PromiseList
+import grails.async.factory.AbstractPromiseFactory
+import org.grails.async.factory.BoundPromise
+
+/**
+ * PromiseFactory implementation backed by Java virtual threads.
+ *
+ * @since 8.1
+ */
+@AutoFinal
+@CompileStatic
+class VirtualThreadPromiseFactory extends AbstractPromiseFactory implements 
Closeable {
+
+    private final ExecutorService executorService = 
Executors.newVirtualThreadPerTaskExecutor()
+
+    @Override
+    <T> Promise<T> createPromise(Class<T> returnType) {
+        return new BoundPromise<T>(null)
+    }
+
+    @Override
+    Promise<Object> createPromise() {
+        return new BoundPromise<Object>(null)
+    }
+
+    @Override
+    <T> Promise<T> createPromise(Closure<T>... closures) {
+        if (closures.length == 1) {
+            Closure<T> decoratedCallable = applyDecorators(closures[0], null)
+            FutureTaskPromise<T> promise = new FutureTaskPromise<T>(this, 
decoratedCallable as Callable<T>)
+            executorService.execute(promise)
+            return promise
+        }
+
+        PromiseList<T> list = new PromiseList<>()
+        for (Closure<T> closure : closures) {
+            list.add(closure)
+        }
+        return list as Promise<T>
+    }
+
+    @Override
+    <T> List<T> waitAll(List<Promise<T>> promises) {
+        return promises.collect { Promise<T> promise -> promise.get() }
+    }
+
+    @Override
+    <T> List<T> waitAll(List<Promise<T>> promises, long timeout, TimeUnit 
units) {
+        return promises.collect { Promise<T> promise -> promise.get(timeout, 
units) }
+    }
+
+    @Override
+    <T> Promise<List<T>> onComplete(List<Promise<T>> promises, Closure<T> 
callable) {
+        return createPromise({
+            List<T> values = waitAll(promises)
+            callable.call(values)
+        } as Closure<List<T>>)
+    }
+
+    @Override
+    <T> Promise<List<T>> onError(List<Promise<T>> promises, Closure<?> 
callable) {
+        return createPromise({
+            try {
+                waitAll(promises)
+                return null
+            }
+            catch (Throwable e) {
+                callable.call(e)
+                return e
+            }
+        } as Closure<List<T>>)
+    }
+
+    @Override
+    @PreDestroy
+    void close() {
+        executorService.shutdown()
+    }
+}
diff --git 
a/grails-async/core/src/test/groovy/grails/async/VirtualThreadPromiseFactorySpec.groovy
 
b/grails-async/core/src/test/groovy/grails/async/VirtualThreadPromiseFactorySpec.groovy
new file mode 100644
index 0000000000..2b4ee86949
--- /dev/null
+++ 
b/grails-async/core/src/test/groovy/grails/async/VirtualThreadPromiseFactorySpec.groovy
@@ -0,0 +1,53 @@
+/*
+ *  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
+ *
+ *    https://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 grails.async
+
+import org.grails.async.factory.PromiseFactoryBuilder
+import org.grails.async.factory.future.VirtualThreadPromiseFactory
+import spock.lang.Specification
+
+class VirtualThreadPromiseFactorySpec extends Specification {
+
+    def cleanup() {
+        System.clearProperty('grails.async.promiseFactory')
+        Promises.promiseFactory = null
+    }
+
+    void 'builder can opt in to virtual thread promise factory'() {
+        given:
+        System.setProperty('grails.async.promiseFactory', 'virtual-thread')
+
+        expect:
+        PromiseFactoryBuilder.build() instanceof VirtualThreadPromiseFactory
+    }
+
+    void 'virtual thread factory executes promises'() {
+        given:
+        def factory = new VirtualThreadPromiseFactory()
+
+        when:
+        Promise<Integer> promise = factory.createPromise { 21 * 2 }
+
+        then:
+        promise.get() == 42
+
+        cleanup:
+        factory.close()
+    }
+}
diff --git a/grails-doc/src/en/guide/async/asyncPromises.adoc 
b/grails-doc/src/en/guide/async/asyncPromises.adoc
index 4328a3661f..ef7924f702 100644
--- a/grails-doc/src/en/guide/async/asyncPromises.adoc
+++ b/grails-doc/src/en/guide/async/asyncPromises.adoc
@@ -93,6 +93,10 @@ def result = p.get(1,MINUTES)
 
 By default, the `Promises` static methods use an instance of `PromiseFactory`. 
This `PromiseFactory` interface has various implementations. The default 
implementation is 
link:{api}org/grails/async/factory/future/CachedThreadPoolPromiseFactory.html[CachedThreadPoolPromiseFactory]
 which uses a thread pool that will create threads as needed (the same as 
`java.util.concurrent.Executors.newCachedThreadPool()`)
 
+Grails 8.1 also includes an opt-in Java 21 virtual-thread seed implementation, 
`org.grails.async.factory.future.VirtualThreadPromiseFactory`.
+Set the JVM system property `grails.async.promiseFactory=virtual-thread` to 
select it when no service-loaded `PromiseFactory` is present.
+The GPars module remains available for compatibility, but new applications 
should prefer the core promise factories or the virtual-thread opt-in while 
GPars support is prepared for deprecation.
+
 However, the design of the Grails promises framework is such that you can swap 
out the underlying implementation for your own or one of the pre-supported 
implementations. For example to use RxJava 1.x simply add the RxJava dependency 
to `build.gradle`:
 
 [source,groovy,subs="attributes"]

Reply via email to