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"]
