Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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.0
*/
@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) {
// callable's return value is intentionally discarded: the resolved value of the
// returned Promise is the waited-on values themselves (matching Promise<List<T>>),
// not whatever the T-typed callback happens to return.
FutureTaskPromise<List<T>> promise = new FutureTaskPromise<List<T>>(this, {
List<T> values = waitAll(promises)
callable.call(values)
return values
} as Callable<List<T>>)
executorService.execute(promise)
return promise
}

@Override
<T> Promise<List<T>> onError(List<Promise<T>> promises, Closure<?> callable) {
FutureTaskPromise<List<T>> promise = new FutureTaskPromise<List<T>>(this, {
try {
return waitAll(promises)
}
catch (Throwable e) {
callable.call(e)
return Collections.<T> emptyList()
}
} as Callable<List<T>>)
executorService.execute(promise)
return promise
}

@Override
@PreDestroy
void close() {
if (!executorService.isShutdown()) {
executorService.shutdown()
}
}
Comment thread
borinquenkid marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* 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 java.util.concurrent.ExecutionException

import org.grails.async.factory.PromiseFactoryBuilder
import org.grails.async.factory.future.VirtualThreadPromiseFactory
import spock.lang.Specification

class VirtualThreadPromiseFactorySpec extends Specification {

private String originalPromiseFactoryProperty
private PromiseFactory originalPromiseFactory

def setup() {
originalPromiseFactoryProperty = System.getProperty('grails.async.promiseFactory')
originalPromiseFactory = Promises.promiseFactory
}

def cleanup() {
if (originalPromiseFactoryProperty == null) {
System.clearProperty('grails.async.promiseFactory')
}
else {
System.setProperty('grails.async.promiseFactory', originalPromiseFactoryProperty)
}
Promises.promiseFactory = originalPromiseFactory
}

void 'builder can opt in to virtual thread promise factory'() {
given:
System.setProperty('grails.async.promiseFactory', 'virtual-thread')

when:
PromiseFactory factory = PromiseFactoryBuilder.build()

then:
factory instanceof VirtualThreadPromiseFactory

cleanup:
(factory as Closeable)?.close()
}

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()
}

void 'onComplete resolves to the waited values and invokes the callback for its side effect'() {
given:
def factory = new VirtualThreadPromiseFactory()
List<Promise<Integer>> promises = [factory.createPromise { 1 }, factory.createPromise { 2 }]
List<Integer> observed = null

when: 'the returned promise is consumed as a statically-typed List, not just Object'
Promise<List<Integer>> combined = factory.onComplete(promises) { List<Integer> values ->
observed = values
'a value that is not a List - the resolved value must not become this'
}
List<Integer> result = combined.get()

then:
result == [1, 2]
observed == [1, 2]

cleanup:
factory.close()
}

void 'onError resolves to the waited values without invoking the callback when nothing fails'() {
given:
def factory = new VirtualThreadPromiseFactory()
List<Promise<Integer>> promises = [factory.createPromise { 1 }, factory.createPromise { 2 }]
boolean callbackInvoked = false

when:
Promise<List<Integer>> combined = factory.onError(promises) { callbackInvoked = true }
List<Integer> result = combined.get()

then:
result == [1, 2]
!callbackInvoked

cleanup:
factory.close()
}

void 'onError invokes the callback and resolves to an empty list when a promise fails'() {
given:
def factory = new VirtualThreadPromiseFactory()
List<Promise<Integer>> promises = [factory.createPromise { throw new IllegalStateException('boom') }]
Throwable observed = null

when:
Promise<List<Integer>> combined = factory.onError(promises) { Throwable error -> observed = error }
List<Integer> result = combined.get()

then:
result == []
observed instanceof ExecutionException
observed.cause instanceof IllegalStateException
observed.cause.message == 'boom'

cleanup:
factory.close()
}

void 'close is idempotent'() {
given:
def factory = new VirtualThreadPromiseFactory()

when:
factory.close()
factory.close()

then:
noExceptionThrown()
}
}
4 changes: 4 additions & 0 deletions grails-doc/src/en/guide/async/asyncPromises.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 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. New applications may also consider the core promise factories or the virtual-thread opt-in.

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"]
Expand Down
Loading