Skip to content

Commit

Permalink
Merge pull request #947 from grails/grailsAsyncMerge
Browse files Browse the repository at this point in the history
#13552 - Move Async docs to grails-doc
  • Loading branch information
jdaugherty authored Jan 9, 2025
2 parents 7a1b618 + d757f03 commit 218cfda
Show file tree
Hide file tree
Showing 22 changed files with 865 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set current date as env variable
run: echo "NOW=$(date +'%Y-%m-%dT%H%M%S')" >> $GITHUB_ENV
- uses: actions/setup-java@v4
with: { java-version: 17, distribution: temurin }
with: { java-version: 17, distribution: liberica }
- name: Extract branch name
id: extract_branch
run: echo "value=${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v2
- uses: actions/setup-java@v4
with: { java-version: 17, distribution: temurin }
with: { java-version: 17, distribution: liberica }
- name: Extract branch name
if: success() && github.event_name == 'workflow_dispatch'
id: extract_branch
Expand Down
1 change: 1 addition & 0 deletions .sdkmanrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
java=17.0.13-librca
2 changes: 0 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,6 @@ tasks.register('publishGuide', grails.doc.gradle.PublishGuide) {
'groovyapi' : "https://docs.groovy-lang.org/${groovyVersion}/html/gapi/",
'springapi' : "https://docs.spring.io/spring/docs/${springVersion}/javadoc-api/",
'springdocs' : "https://docs.spring.io/spring/docs/${springVersion}/",
'asyncdocs' : "https://async.grails.org/latest",
'asyncApiDocs' : "https://async.grails.org/latest/api/",
'gspdocs' : "https://gsp.grails.org/${gspVersion}",
'gspApiDocs' : "https://gsp.grails.org/${gspVersion}/api/",
'gormApiDocs' : "https://gorm.grails.org/${gormVersion}/api/",
Expand Down
5 changes: 5 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ gormVersion=9.0.0-M2
groovyVersion=4.0.24
gspVersion=7.0.0-M1

#gpars docs is currently not available with https
gparsdocs=http://gpars.org/1.2.1/groovydoc
rxjavadocs=https://reactivex.io/RxJava/1.x/javadoc
rxjava2docs=https://reactivex.io/RxJava/2.x/javadoc

org.gradle.caching=true
org.gradle.daemon=true
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1536M -XX:MaxMetaspaceSize=768M
4 changes: 1 addition & 3 deletions src/en/guide/async.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ Popular asynchronous libraries include:
* GPars - http://gpars.org
* Reactor - https://projectreactor.io
By building on top of these various libraries the https://async.grails.org[Async features of Grails] aim to simplify concurrent programming within the framework, include the concept of Promises, and a unified event model.
By building on top of these various libraries the Async features of Grails aim to simplify concurrent programming within the framework, include the concept of Promises, and a unified event model.

In general, since the Reactive programming model is an evolving space, Grails tries to provide generic support for integrating a range of asynchronous libraries and doesn't recommend any single library as they all have various advantages and disadvantages.

For more information on Asynchronous programming with Grails see the user guide for the https://async.grails.org[Grails Asynchronous Framework].
145 changes: 145 additions & 0 deletions src/en/guide/async/asyncGorm.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
Since Grails 2.3, GORM features an asynchronous programming model that works across all supported datastores (Hibernate, MongoDB etc.).

NOTE: Although GORM executes persistence operations asynchronously, these operations still block as the underlying database drivers are not asynchronous. Asynchronous GORM is designed to allow you to isolate these blocking operations onto a separate thread you can scale and control allowing your controller layer to remain non-blocking.

=== The AsyncEntity Trait

Since Grails 3.3, the asynchronous part of GORM is optional. To enable it you first need to add the `grails-datastore-gorm-async` dependency to `build.gradle`:

[source,groovy]
.build.gradle
----
implementation "org.grails:grails-datastore-gorm-async"
----

Then in your domain classes you wish to allow asynchronous processing you should use the `AsyncEntity` trait:

[source,groovy]
----
import grails.gorm.async.*
class MyEntity implements AsyncEntity<MyEntity> {
//...
}
----

=== Async Namespace


The `AsyncEntity` entity trait provides an `async` namespace that exposes all of the GORM methods in an asynchronous manner.

For example, the following code listing reads 3 objects from the database asynchronously:

[source,groovy]
----
import static grails.async.Promises.*
def p1 = Person.async.get(1L)
def p2 = Person.async.get(2L)
def p3 = Person.async.get(3L)
def results = waitAll(p1, p2, p3)
----

Using the `async` namespace, all the regular GORM methods are available (even dynamic finders), but instead of executing synchronously, the query is run in the background and a `Promise` instance is returned.

The following code listing shows a few common examples of GORM queries executed asynchronously:

[source,groovy]
----
import static grails.async.Promises.*
Person.async.list().onComplete { List results ->
println "Got people = ${results}"
}
def p = Person.async.getAll(1L, 2L, 3L)
List results = p.get()
def p1 = Person.async.findByFirstName("Homer")
def p2 = Person.async.findByFirstName("Bart")
def p3 = Person.async.findByFirstName("Barney")
results = waitAll(p1, p2, p3)
----


=== Async and the Session


When using GORM async each promise is executed in a different thread. Since the Hibernate session is not concurrency safe, a new session is bound per thread.

This is an important consideration when using GORM async (particularly with Hibernate as the persistence engine). The objects returned from asynchronous queries will be detached entities.

This means you cannot save objects returned from asynchronous queries without first merging them back into session. For example the following will not work:

[source,groovy]
----
def promise = Person.async.findByFirstName("Homer")
def person = promise.get()
person.firstName = "Bart"
person.save()
----

Instead you need to merge the object with the session bound to the calling thread. The above code needs to be written as:

[source,groovy]
----
def promise = Person.async.findByFirstName("Homer")
def person = promise.get()
person.merge()
person.firstName = "Bart"
----

Note that `merge()` is called first because it may refresh the object from the cache or database, which would result in the change being lost. In general it is not recommended to read and write objects in different threads and you should avoid this technique unless absolutely necessary.

Finally, another issue with detached objects is that association lazy loading *will not* work and you will encounter `LazyInitializationException` errors if you do so. If you plan to access the associated objects of those returned from asynchronous queries you should use eager queries (which is recommended anyway to avoid N+1 problems).


=== Multiple Asynchronous GORM calls


As discussed in the previous section you should avoid reading and writing objects in different threads as merging tends to be inefficient.

However, if you wish to do more complex GORM work asynchronously then the GORM async namespace provides a `task` method that makes this possible. For example:

[source,groovy]
----
def promise = Person.async.task {
withTransaction {
def person = findByFirstName("Homer")
person.firstName = "Bart"
person.save(flush:true)
}
}
Person updatedPerson = promise.get()
----

Note that the GORM `task` method differs from the static `Promises.task` method in that it deals with binding a new session to the asynchronous thread for you. If you do not use the GORM version and do asynchronous work with GORM then you need to do this manually. Example:

[source,groovy]
----
import static grails.async.Promises.*
def promise = task {
Person.withNewSession {
// your logic here
}
}
----


=== Async DetachedCriteria


The `DetachedCriteria` class also supports the `async` namespace. For example you can do the following:

[source,groovy]
----
DetachedCriteria query = Person.where {
lastName == "Simpson"
}
def promise = query.async.list()
----



Loading

0 comments on commit 218cfda

Please sign in to comment.