codeconsole commented on code in PR #16619: URL: https://github.com/apache/grails-core/pull/16619#discussion_r4326946228
########## grails-doc/src/en/guide/async/rxjava.adoc: ########## @@ -17,23 +17,98 @@ specific language governing permissions and limitations under the License. //// -Since Grails 3.2, you can use https://github.com/ReactiveX/RxJava[RxJava] to write reactive logic in your Grails controllers that leverages the underlying containers asynchronous processing capabilities. +Grails integrates https://github.com/ReactiveX/RxJava[RxJava] 3 at three points. Each is a separate module that an application adds to `build.gradle`; Grails itself does not depend on RxJava. -To get started simply declare a dependency on the plugin in `build.gradle`: +==== Promises on RxJava + +`grails-async-rxjava3` provides a `grails.async.PromiseFactory` backed by RxJava 3, so every `Promise` the application creates is an RxJava subscription: + +[source,groovy] +.build.gradle +---- +runtimeOnly 'org.apache.grails.async:grails-async-rxjava3' +---- + +See <<asyncPromises,Promises>>. + +==== Events on RxJava + +`grails-events-rxjava3` provides an `EventBus` that delivers events through RxJava 3 subjects and schedulers: + +[source,groovy] +.build.gradle +---- +runtimeOnly 'org.apache.grails.events:grails-events-rxjava3' +---- + +See <<events,Events>>. + +==== RxJava Data Services + +With `grails-datamapping-rx` on the classpath, a GORM data service can declare methods that return `io.reactivex.rxjava3.core.Observable` or `io.reactivex.rxjava3.core.Single`. GORM implements the method as it does any other data service method: the blocking query runs when the result is subscribed to, on the subscribing thread, and the result is delivered on RxJava's IO scheduler, so the caller consumes it reactively without the datastore itself being non-blocking: [source,groovy] .build.gradle ---- -dependencies { - //... - implementation 'org.apache.grails:grails-rxjava3' +implementation 'org.apache.grails.data:grails-datamapping-rx' +---- + +[source,groovy] +---- +import grails.gorm.services.Service +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single + +@Service(Book) +interface BookService { + + Single<Book> findOne(String title) + + Observable<Book> findAllByTitle(String title) + + Observable<Book> findAllByAuthor(String author) +} +---- + +A `Single` that finds nothing signals a `java.util.NoSuchElementException`, since an RxJava 3 `Single` cannot complete without a value; an `Observable` that finds nothing completes without emitting. The `grails.gorm.rx.services.RxSchedule` annotation selects the scheduler a method delivers its result on. Neither it nor the default moves the query itself off the subscribing thread, so subscribe from a task, as <<asyncRequests,Asynchronous Request Handling>> describes, to keep a blocking query off a request thread. See the RxJava Support section of the GORM for link:{hibernate7Guide}index.html#dataServicesRxJava[Hibernate 7] or link:{hibernate5Guide}index.html#dataServicesRxJava[Hibernate 5] guide. + +==== RxJava Results from Controllers + +An RxJava 3 `Flowable` is a Reactive Streams `Publisher`, so an action can return one and Grails streams every item it emits to the client as a server-sent event, with no plugin and with the container thread released while the stream produces. See <<asyncStreaming,Streaming Responses>>: Review Comment: In this example, the query runs on the container thread: - `PublisherActionResultTransformer.stream()` calls `publisher.subscribe(subscriber)` on the request thread (line 170). - Subscribing runs the data service's `defer` supplier on that same thread, which is the behavior `RxServiceSupportSpec` now pins. So `findAllByAuthor` has fetched every book before the action returns, and only the delivery and the writes happen on the IO scheduler. That contradicts the paragraph above, which says to subscribe from a task to keep the query off a request thread. There are two ways to fix it: - Say here that the query runs on the request thread. - Add `subscribeOn(Schedulers.io())` before `toFlowable(..)`. This one also needs the data service's transaction and session to work on the IO thread. The first is the smaller change. ########## grails-doc/src/en/guide/upgrading/upgrading90x.adoc: ########## @@ -305,3 +324,58 @@ It applies nothing else. Null properties are written whatever `@JsonInclude` or `spring.jackson.default-property-inclusion` say, properties are written in the converter's order rather than by `@JsonPropertyOrder` or sorted alphabetically, and `@JsonView`, `@JsonFilter`, `@JsonAnyGetter`, `@JsonIgnoreType`, `@JsonFormat` and `@JsonSerialize` on a property, and mix-ins, other than the property names they give, do not apply. + +==== 6. RxGORM Runs on RxJava 3 + +RxGORM, the reactive variant of GORM in `org.apache.grails.data:grails-datamapping-rx`, is built on RxJava 3 instead of RxJava 1. +Its dependency is now `io.reactivex.rxjava3:rxjava`, and the RxJava 1 artifact `io.reactivex:rxjava` is no longer on the classpath of an application that only uses RxGORM. +Every RxGORM API that returned `rx.Observable` returns `io.reactivex.rxjava3.core.Observable`, and an `@RxSchedule` service method that returned `rx.Single` returns `io.reactivex.rxjava3.core.Single`. + +Update the imports of every class that works with RxGORM results: + +[cols="1,1",options="header"] +|=== +| RxJava 1 | RxJava 3 + +| `rx.Observable` | `io.reactivex.rxjava3.core.Observable` +| `rx.Single` | `io.reactivex.rxjava3.core.Single` +| `rx.Subscriber` | `io.reactivex.rxjava3.core.Observer` +| `rx.Subscription` | `io.reactivex.rxjava3.disposables.Disposable` +| `rx.Scheduler`, `rx.schedulers.Schedulers` | `io.reactivex.rxjava3.core.Scheduler`, `io.reactivex.rxjava3.schedulers.Schedulers` +| `rx.functions.Func1` | `io.reactivex.rxjava3.functions.Function` +|=== + +The blocking helpers that tests and scripts use moved onto the `Observable` and `Single` types themselves: + +[cols="1,1",options="header"] +|=== +| RxJava 1 | RxJava 3 + +| `observable.toBlocking().first()` | `observable.blockingFirst()` +| `observable.toBlocking().single()` | `observable.blockingSingle()` +| `observable.toList().toBlocking().single()` | `observable.toList().blockingGet()` +| `single.toBlocking().value()` | `single.blockingGet()` +|=== + +`Observable.toList()` returns a `Single<List<T>>` in RxJava 3. +The RxGORM methods that emit a list, such as `list()`, `findAll().toList()` on a criteria query and `toListObservable()` on a lazy association, still return an `Observable<List<T>>`, so code that subscribes to them is unchanged. + +Subscribing with a closure, for example `Book.get(id).subscribe { Book book -> ... }`, is unchanged. +A class that subscribes with a full subscriber implements `io.reactivex.rxjava3.core.Observer` instead of `rx.Subscriber`: it gains `onSubscribe(Disposable)`, which receives the handle that cancels the subscription, and `onCompleted()` is named `onComplete()`. +`DetachedCriteria`, `ObservableProxy` and the lazy association collections implement `io.reactivex.rxjava3.core.ObservableSource`, so their `subscribe(Observer)` method returns `void` rather than a `Subscription`; they can be passed anywhere RxJava 3 accepts an `ObservableSource`, such as `Observable.wrap(..)` or `concatWith(..)`. + +RxJava 3 does not allow `null` to be emitted. +An RxGORM operation that has no result completes without emitting, as queries already did; for example `save()` and `insert()` complete empty, instead of emitting `null`, when the datastore reports no identifier for the instance. +An `@RxSchedule` service method that returns a `Single` and produces no result now signals a `java.util.NoSuchElementException`, and one that returns an `Observable` completes empty. +Replace a test that compares `toBlocking().first()` with `null` by one that expects `blockingFirst()` to throw `NoSuchElementException`, or use `firstElement().blockingGet()`, which returns `null` for an empty result. + +A datastore implementation of RxGORM built for Grails 8 or earlier, such as the RxGORM for MongoDB plugin, is compiled against RxJava 1 and must be rebuilt against this release before it can be used. +The `RxEntity` domain model has had no datastore implementation for several releases and its removal is under discussion for Grails 9, so an application that implements `RxEntity` should plan to move to `GormEntity` with the RxJava data services, which work on every GORM datastore. The import and blocking-call changes above apply to both. +The `rx-mongodb` profile feature is removed, because the `org.grails.plugins:rx-mongodb` plugin it added has had no release since GORM 6.1 and has never been published to Maven Central, so an application generated with it could not resolve its dependencies. + +==== 7. RxJava 1 and RxJava 2 Async and Events Modules Are Deprecated + +The RxJava 1 and RxJava 2 implementations of the Grails promise and event bus APIs, `grails-async-rxjava`, `grails-async-rxjava2`, `grails-events-rxjava` and `grails-events-rxjava2`, are deprecated for removal. +They keep working in Grails 9 and log a warning when their promise factory or event bus is created. +Replace either dependency with the RxJava 3 module, `grails-async-rxjava3` or `grails-events-rxjava3`; the `Promise` and `EventBus` APIs an application uses are the same. Review Comment: Switching from `grails-async-rxjava` to `grails-async-rxjava3` isn't a no-code-change swap once a promise holds `null`. I ran the same calls against both factories: | Call | `grails-async-rxjava` | `grails-async-rxjava3` | |---|---|---| | `createPromise()` | promise of `null` | `NullPointerException: item is null` | | `createPromise(String)` | promise of `null` | `NullPointerException: item is null` | | `createPromise { null }.get()` | `null` | `java.lang.Void` (the class) | | `onComplete([createBoundPromise(null)]) { .. }` | callback gets `[null]` | `NullPointerException: item is null` | Where these come from in the rxjava3 module: - The exceptions come from `Single.just(null)` and `Observable.just(null)` in its `RxPromiseFactory`, at lines 45, 50 and 82. - `RxPromise.runCallable` turns `null` into `Void` as a sentinel, but nothing turns it back. `createPromise()` is what `Promises.createPromise()` and `WebPromises.createPromise()` call. RxJava 2 users already see this behavior, so only the RxJava 1 users this section points at rxjava3 would hit it. The same "the API is the same" claim appears in `asyncPromises.adoc:181`, `whatsNew.adoc:153` and `SKILL.md:196`. Could the rxjava3 factory carry `null` through one sentinel on the way in and turn it back into `null` on the way out, with a spec for each row above? Otherwise these pages need to list the differences. ########## grails-doc/src/en/guide/upgrading/upgrading90x.adoc: ########## @@ -305,3 +324,58 @@ It applies nothing else. Null properties are written whatever `@JsonInclude` or `spring.jackson.default-property-inclusion` say, properties are written in the converter's order rather than by `@JsonPropertyOrder` or sorted alphabetically, and `@JsonView`, `@JsonFilter`, `@JsonAnyGetter`, `@JsonIgnoreType`, `@JsonFormat` and `@JsonSerialize` on a property, and mix-ins, other than the property names they give, do not apply. + +==== 6. RxGORM Runs on RxJava 3 + +RxGORM, the reactive variant of GORM in `org.apache.grails.data:grails-datamapping-rx`, is built on RxJava 3 instead of RxJava 1. +Its dependency is now `io.reactivex.rxjava3:rxjava`, and the RxJava 1 artifact `io.reactivex:rxjava` is no longer on the classpath of an application that only uses RxGORM. +Every RxGORM API that returned `rx.Observable` returns `io.reactivex.rxjava3.core.Observable`, and an `@RxSchedule` service method that returned `rx.Single` returns `io.reactivex.rxjava3.core.Single`. + +Update the imports of every class that works with RxGORM results: + +[cols="1,1",options="header"] +|=== +| RxJava 1 | RxJava 3 + +| `rx.Observable` | `io.reactivex.rxjava3.core.Observable` +| `rx.Single` | `io.reactivex.rxjava3.core.Single` +| `rx.Subscriber` | `io.reactivex.rxjava3.core.Observer` +| `rx.Subscription` | `io.reactivex.rxjava3.disposables.Disposable` +| `rx.Scheduler`, `rx.schedulers.Schedulers` | `io.reactivex.rxjava3.core.Scheduler`, `io.reactivex.rxjava3.schedulers.Schedulers` +| `rx.functions.Func1` | `io.reactivex.rxjava3.functions.Function` +|=== + +The blocking helpers that tests and scripts use moved onto the `Observable` and `Single` types themselves: + +[cols="1,1",options="header"] +|=== +| RxJava 1 | RxJava 3 + +| `observable.toBlocking().first()` | `observable.blockingFirst()` +| `observable.toBlocking().single()` | `observable.blockingSingle()` +| `observable.toList().toBlocking().single()` | `observable.toList().blockingGet()` +| `single.toBlocking().value()` | `single.blockingGet()` +|=== + +`Observable.toList()` returns a `Single<List<T>>` in RxJava 3. +The RxGORM methods that emit a list, such as `list()`, `findAll().toList()` on a criteria query and `toListObservable()` on a lazy association, still return an `Observable<List<T>>`, so code that subscribes to them is unchanged. Review Comment: `findAll()` on an RxGORM `DetachedCriteria` returns `Observable<T>` (`DetachedCriteria.groovy:77`). So `findAll().toList()` is RxJava's own `toList()`, and it returns `Single<List<T>>`, as the sentence just before says. The methods that keep `Observable<List<T>>` are `toList()` and `list()` on the criteria itself (lines 111 and 123). "`toList()` on a `DetachedCriteria`" would fit here. ########## grails-async/rxjava/src/test/groovy/org/grails/async/factory/rxjava/RxPromiseFactoryDeprecationSpec.groovy: ########## @@ -0,0 +1,93 @@ +/* + * 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.rxjava + +import java.lang.reflect.Field +import java.util.concurrent.atomic.AtomicBoolean + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import org.slf4j.LoggerFactory +import spock.lang.Specification + +import grails.async.PromiseFactory +import grails.async.Promises + +class RxPromiseFactoryDeprecationSpec extends Specification { + + Logger factoryLogger = LoggerFactory.getLogger(RxPromiseFactory) as Logger + ListAppender<ILoggingEvent> appender = new ListAppender<>() + + void setup() { + resetDeprecationWarning() + appender.start() + factoryLogger.addAppender(appender) + } + + void cleanup() { + factoryLogger.detachAppender(appender) + appender.stop() + } + + void 'the RxJava 1 promise factory is deprecated for removal since Grails 9.0'() { + expect: + RxPromiseFactory.getAnnotation(Deprecated).forRemoval() + RxPromiseFactory.getAnnotation(Deprecated).since() == '9.0' + } + + void 'creating the RxJava 1 promise factory warns that it is deprecated'() { + when: 'the factory is created' + PromiseFactory factory = new RxPromiseFactory() + + then: 'the factory still works' + factory.createPromise { 1 + 1 }.get() == 2 + + and: 'a single warning names the module and its replacement' + appender.list.size() == 1 + appender.list[0].level == Level.WARN + appender.list[0].formattedMessage.contains('grails-async-rxjava') Review Comment: `contains('grails-async-rxjava')` is also true for `grails-async-rxjava2` and `grails-async-rxjava3`. So this line can't fail while the next one passes, and a v1 message copied from the v2 one would pass all three checks. `contains('(grails-async-rxjava)')` would pin the module. The events spec has the same check at line 72. Two more things in this spec: - **The service-loaded feature.** It repeats `RxJavaPromiseSpec:36`. `Promises.promiseFactory` is cached, so the feature may not create anything. `new PromiseFactoryBuilder().build()` loads the factory through `ServiceLoader` every time, so the feature could assert the warning too, as the events spec does with `EventBusBuilder`. - **The annotation feature.** It checks only `RxPromiseFactory`, so removing `@Deprecated` from `RxPromise` fails nothing. `GparsPromiseSpec` checks `GparsPromise` as well. The rxjava2 copy has the same three gaps. ########## grails-doc/src/en/guide/async/asyncStreaming.adoc: ########## @@ -34,7 +34,9 @@ class ClockController { } ---- -A publisher that emits at most one item, such as a Reactor `Mono`, is not streamed. As Spring MVC does, Grails resolves it and treats its value as the action's result, the way a returned `Promise`'s value is: a `Map` is the model of the view, anything else leaves the response to what the action rendered, and a failure fails the request. Which publishers are single-valued is what Spring's `ReactiveAdapterRegistry` knows, so a Reactor `Mono`, a `CompletableFuture` adapted to one or an RxJava `Single` all resolve rather than stream. +A publisher that emits at most one item, such as a Reactor `Mono`, is not streamed. As Spring MVC does, Grails resolves it and treats its value as the action's result, the way a returned `Promise`'s value is: a `Map` is the model of the view, anything else leaves the response to what the action rendered, and a failure fails the request. Which publishers are single-valued is what Spring's `ReactiveAdapterRegistry` knows, so a Reactor `Mono` and a `CompletableFuture` adapted to one both resolve rather than stream. + +Only a result that implements `Publisher` is streamed or resolved. An RxJava 3 `Observable` or `Single` is not a `Publisher`, so it is left alone and the action renders its view as before; convert it to one with `toFlowable(..)`, as <<rxjava,RxJava Support>> shows. A `Flowable` is multi-valued whatever it emits, so a `Flowable` of one item is streamed as a single event rather than resolved. Review Comment: The action doesn't render its view here: - `PublisherActionResultTransformer` returns the `Observable` unchanged. - `UrlMappingsInfoHandlerAdapter` (lines 164-187) only builds a `ModelAndView` for four results: a `MODEL_AND_VIEW` attribute, a `Map`, a `ModelAndView`, or `null` with `renderView` set. - Any other non-null result reaches `return null`. So the response is empty unless the action rendered something itself. Something like "so Grails neither streams it nor renders a view" would be accurate. ########## grails-doc/src/en/guide/introduction/whatsNew.adoc: ########## @@ -126,6 +126,32 @@ A controller action can return a Reactive Streams `Publisher`, such as a Reactor Items are rendered as text or JSON, or through a `StreamItemRenderer` bean for a type of its own, which is how `grails-spring-ai` streams the chat responses of a model. Such an action streams instead of rendering its view as it did before; set `grails.async.streaming.enabled` to `false` to keep the previous behaviour. A publisher that emits at most one item, such as a Reactor `Mono`, is not streamed but resolved, its value being the action's result as a returned `Promise`'s value is. See xref:upgrading#upgrading81x[the upgrade guide]. See xref:async#asyncStreaming[Streaming Responses]. +==== RxJava Data Services on RxJava 3 Review Comment: This heading needs a blank line before it. Without one, Asciidoctor keeps it inside the paragraph above. I rendered lines 118-135 with AsciidoctorJ 3.0.1, and it gave `…[Streaming Responses].\n==== RxJava Data Services on RxJava 3</p>`. So the section is missing from the TOC, and its text runs on from the Streaming Responses section. ########## grails-skills/upgrade-guide-9/skills/grails-9-upgrade/SKILL.md: ########## @@ -0,0 +1,223 @@ +--- +name: grails-9-upgrade +description: Guide for upgrading Grails applications from Grails 8.x to Grails 9, covering the Groovy 6 baseline, the GPars deprecation in favour of groovy.concurrent, invokedynamic compilation, the Jackson-backed JSON converter and domain class serializer, RxGORM on RxJava 3 (imports, blocking calls, subscribers, empty results), and the deprecated RxJava 1 and RxJava 2 async and events modules +license: Apache-2.0 +--- + +<!-- +SPDX-License-Identifier: Apache-2.0 + +Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0. +--> + +## What I Do + +- Guide upgrades of Grails applications from Grails 8.x to Grails 9. +- Turn the Grails 9 upgrade guide into a practical migration checklist. +- Identify the Groovy 6, GPars, invokedynamic, JSON rendering, RxGORM, and RxJava changes that can break an application. +- Keep migration work focused on public application behavior, not internal Grails framework implementation details. + +## When to Use Me + +Activate this skill when: + +- Updating an application to Grails 9. +- Reviewing a Grails 9 upgrade branch or pull request. +- Fixing tests, build failures, runtime failures, or behavior changes after moving from Grails 8.x to Grails 9. +- Porting code that uses RxGORM, `grails-async-rxjava`, `grails-async-rxjava2`, `grails-events-rxjava`, or `grails-events-rxjava2` to RxJava 3. +- Replacing GPars (`grails-async-gpars`, `grails-events-gpars`, or direct `GParsPool` use) with Groovy's `groovy.concurrent` toolkit. +- Checking an application's JSON output before or after setting `grails.converters.json.legacy` to `false`. + +For an application still on Grails 7, apply the `grails-8-upgrade` skill first and move to Grails 8 before using this one. + +## Primary Sources + +Use the published Grails documentation as the source of truth before changing an application. The URLs below use `<version>`, which stands for the Grails 9 version the application is upgrading to, such as the `grailsVersion` in its `gradle.properties`; use `snapshot` for a snapshot version. + +| URL | Use for | +|-----|---------| +| `https://grails.apache.org/docs/<version>/guide/upgrading.html#upgrading90x` | Main Grails 8 to Grails 9 upgrade guide | +| `https://grails.apache.org/docs/<version>/guide/introduction.html#whatsNew` | Grails 9 feature and platform overview | +| `https://grails.apache.org/docs/<version>/guide/introduction.html#dependencyUpgrades` | Platform dependency baseline | +| `https://grails.apache.org/docs/<version>/guide/async.html` | Promises and events, including the RxJava 3 modules | +| `https://grails.apache.org/docs/<version>/guide/theWebLayer.html` | JSON rendering and the converters | +| `https://groovy-lang.org/releasenotes/groovy-6.0.html#concurrency-toolkit` | The `groovy.concurrent` toolkit that replaces GPars | +| `https://grails.apache.org/docs/<version>/ref/Versions/Grails%20BOM.html` | Dependency versions of the Grails BOM | +| `https://groovy-lang.org/releasenotes/groovy-6.0.html` | Groovy 6 language and runtime changes | +| `https://github.com/ReactiveX/RxJava/wiki/What's-different-in-3.0` | RxJava 3 API differences, with the RxJava 1 to 2 differences it links to | + +## Upgrade Baseline + +Verify these before touching application code; `gradle.properties`, the Gradle wrapper, and the Grails BOM are the sources of truth: + +- Java 21 remains the minimum to build and run. +- Groovy moves from 5.x to 6.0, and Spock to the `groovy-6.0` variant. Do not pin `groovy-all` or Spock yourself; the Grails BOM manages both. +- Spring Boot 4.1 and Spring Framework 7 are unchanged from Grails 8; a Grails 8 application's Boot configuration carries over. +- Gradle 9.8 or later for the wrapper. +- RxGORM (`grails-datamapping-rx`) depends on `io.reactivex.rxjava3:rxjava`. The RxJava 1 artifact `io.reactivex:rxjava` is no longer transitively available through it. +- Groovy source sets compile with invokedynamic by default; `grails { indy = false }` was the Grails 8 default and is now unset. +- `grails.converters.json.legacy` defaults to `true`, so JSON renders as it did in Grails 8 until the application opts in to the Jackson-backed converter. + +## Migration Workflow + +1. Update `grailsVersion` in `gradle.properties` and refresh the Gradle wrapper; let the Grails BOM manage Groovy, Spock, and RxJava versions. +2. Build without tests first (`./gradlew compileGroovy compileTestGroovy`) and fix compilation errors module by module. +3. Replace GPars with `groovy.concurrent`, following the sections below. +4. Port RxGORM and RxJava code, following the sections below. +5. Replace the deprecated RxJava 1 and 2 async and events modules with their RxJava 3 counterparts. +6. Check the application's JSON with `grails.converters.json.legacy` set to `false`, then set it explicitly either way. +7. Run the full test suite, then start the application and read its log for `[DEPRECATED]` and `deprecated` warnings from Grails. +8. Review the published upgrade guide section by section, confirming every item either does not apply or has been handled. + +## Groovy 6 + +Groovy 6 is a major Groovy release. Read its release notes before assuming that a Groovy 5 construct behaves the same way. Compile the application statically where it was already static; a construct that compiles dynamically but fails under `@CompileStatic` is a sign of a Groovy change rather than a Grails one. Keep the Spock version the Grails BOM supplies, which is built for Groovy 6. + +## GPars Deprecated in Favour of Groovy Concurrency + +Groovy 6 brings the concurrency and parallel-processing patterns of GPars into the language as `groovy.concurrent`, so Grails no longer needs the separate GPars library. `grails-async-gpars` and `grails-events-gpars` still work in Grails 9, carry `@Deprecated(since = '9.0', forRemoval = true)`, log a warning when selected, and will be removed in a future release. + +Replace the dependency, one for one: + +| Grails 8 | Grails 9 | +|----------|----------| +| `org.apache.grails.async:grails-async-gpars` | `org.apache.grails.async:grails-async-groovy-concurrent` | +| `org.apache.grails.events:grails-events-gpars` | `org.apache.grails.events:grails-events-groovy-concurrent` | + +Do not leave both on the classpath: only the first service-loaded `PromiseFactory` is used, and the event bus builder refuses to start when it finds more than one `EventBus` implementation. + +The Groovy promise factory follows the failure contract of the default `CompletableFuture` factory rather than the GPars one. A direct or chained `get()` throws `ExecutionException` with the original failure as its cause, a single-promise `onError` recovers with its callback result, the aggregate `onError` observes the failure and leaves the returned promise failed, and `onComplete` and `then` return a new promise instead of the receiver. A test that asserted the GPars behaviour needs updating. + +The Groovy actor event bus delivers each event to its subscribers in publication order on one actor. A subscriber that throws without a reply handler is logged and does not stop delivery to the remaining subscribers or to later events. + +Application code that uses GPars directly moves to the Groovy equivalents: + +| GPars | `groovy.concurrent` | +|-------|---------------------| +| `GParsPool.withPool { list.eachParallel { } }` | `list.eachParallel { }`, which needs no pool | +| `GParsPool.withPool(n) { list.collectParallel { } }` | `ParallelScope.withPool(n) { scope -> list.collectParallel { } }` | +| `GParsConfig.setPoolFactory(..)` | `Awaitable.setExecutor(..)`, which also hosts the actor event bus | + +Promises the Groovy factory creates are Groovy `Awaitable` instances, so they can be passed to `await` and to combinators such as `Awaitable.all` without conversion. Once the deprecated modules are removed, Grails no longer brings GPars onto the classpath, so an application that still needs it declares `org.codehaus.gpars:gpars` itself. + +Groovy marks the toolkit as incubating. The Grails modules on top of it expose only the Grails `Promise` and `EventBus` contracts, so later Groovy changes are absorbed by Grails rather than by application code. + +## Groovy Code Compiles with invokedynamic + +Grails 8 compiled every Groovy source set without invokedynamic, because `grails { indy = false }` was the default. Grails 9 leaves the setting unset, so the Groovy compiler's own default applies, and on Groovy 6 that is invokedynamic. + +- Remove `grails { indy = true }`; it is now the effective default. +- Build logic that read the property must allow for it having no value: use `grails.indy.getOrElse(true)` or check `grails.indy.present` instead of `grails.indy.get()`. +- An application that sets `indy = false` to keep classic call sites must also declare `implementation 'org.apache.groovy:groovy-callsite'` itself, at the BOM's version, and again for every source set that does not see implementation dependencies, such as `java-test-fixtures` test fixtures. Without it the Groovy compiler stops with a message naming the module. +- An application that applies GraalVM's native image plugin cannot set `indy = false`; the build fails with the reason, because an image needs invokedynamic. +- A library compiled with classic call sites needs `groovy-callsite` whatever the application's setting, or loading one of its classes fails with `NoClassDefFoundError: org/codehaus/groovy/runtime/callsite/CallSiteArray`. Spock 2.5, Geb 8 and the Asset Pipeline 5.2 are compiled with invokedynamic; `org.testcontainers:testcontainers-spock` is not, so declare `testRuntimeOnly 'org.apache.groovy:groovy-callsite'` next to it. + +## JSON Rendering Can Use Spring Boot's JsonMapper + +`grails.converters.JSON` still renders as it did in Grails 8 by default, because `grails.converters.json.legacy` defaults to `true`. Grails logs a warning once at startup while the setting is unset, because a later release makes `false` the default. Set it explicitly either way to silence the warning, and check the application's JSON with `false` before that release: + +```yaml +grails: + converters: + json: + legacy: false +``` + +With `legacy: false`, the converter writes through the application's Jackson `JsonMapper`, so `spring.jackson.*` settings and registered Jackson modules apply. Check these values in the application's JSON contract and in any test that asserts JSON text, because they render differently: `OffsetTime`, `Month` (a number, not its name), `Locale` (a language tag), `char` and `Character`, `byte[]` and `char[]` (Base64 and a string), `UUID`, `URI` and other `@JsonValue` types (their value, not a bean), `Pattern`, `Charset`, `InetAddress`, `File`, `Path`, `ByteBuffer`, `JsonNode`, `Optional` (its value or `null`), a `NaN` or infinite `double` (a string), and the layout of pretty printed JSON. Enums still render by `name()`, and strings are still escaped for an HTML `<script>` element, deliberately differing from Spring Boot. + +Parsing does not change: `JSON.parse`, `request.JSON` and `new JSONObject(String)` accept the same syntax and return the same types. + +A marshaller an application registers still takes precedence over the mapper, so keeping one type's Grails 8 rendering under `legacy: false` is a matter of registering a marshaller for it, for example in `BootStrap.init`: + +```groovy +JSON.registerObjectMarshaller(Month) { Month month -> month.name() } +``` + +Subclassing `org.grails.web.json.JSONWriter` needs review whatever the setting: it now writes through a Jackson `JsonGenerator`, its `array()`, `object()`, `key()` and `value` methods no longer call the protected `append` and `comma` methods, and its text reaches the `Writer` as the buffer fills rather than per value. `PrettyPrintJSONWriter` and the JavaCC parser in `org.grails.web.json.parser` are deprecated. + +With `legacy: false`, domain classes render through `DomainClassSerializer`, which honours `@JsonIgnore` and `@JsonProperty(access = WRITE_ONLY)` where the marshaller did not, so a property that was rendered before may now be absent. Set `grails.converters.json.domain.jackson.enabled` to `true` to have the application's own `JsonMapper` render domain classes the same way, for example when a Spring MVC controller returns one. + +## RxGORM on RxJava 3 + +RxGORM returns RxJava 3 types. Every `rx.Observable` becomes `io.reactivex.rxjava3.core.Observable`, and an `@RxSchedule` service method that returned `rx.Single` returns `io.reactivex.rxjava3.core.Single`. + +### Imports + +| RxJava 1 | RxJava 3 | +|----------|----------| +| `rx.Observable` | `io.reactivex.rxjava3.core.Observable` | +| `rx.Single` | `io.reactivex.rxjava3.core.Single` | +| `rx.Subscriber` | `io.reactivex.rxjava3.core.Observer` | +| `rx.Subscription` | `io.reactivex.rxjava3.disposables.Disposable` | +| `rx.Scheduler`, `rx.schedulers.Schedulers` | `io.reactivex.rxjava3.core.Scheduler`, `io.reactivex.rxjava3.schedulers.Schedulers` | +| `rx.functions.Func1`, `Func2`, `FuncN` | `io.reactivex.rxjava3.functions.Function`, `BiFunction`, `Function<Object[], R>` | +| `rx.observers.TestSubscriber` | `io.reactivex.rxjava3.observers.TestObserver` | + +A declared return type such as `rx.Observable<Book>` in a `@Service` interface must change too, or the service implementer no longer recognises the method as reactive. + +### Blocking Calls + +| RxJava 1 | RxJava 3 | +|----------|----------| +| `observable.toBlocking().first()` | `observable.blockingFirst()` | +| `observable.toBlocking().single()` | `observable.blockingSingle()` | +| `observable.toList().toBlocking().single()` | `observable.toList().blockingGet()` | +| `single.toBlocking().value()` | `single.blockingGet()` | +| `observable.toBlocking().first()` compared with `null` | `observable.firstElement().blockingGet()`, which returns `null` when empty, or expect `blockingFirst()` to throw `NoSuchElementException` | + +`Observable.toList()` returns a `Single<List<T>>` in RxJava 3. The RxGORM methods that emit a list, such as `list()`, `DetachedCriteria.toList()`, criteria `list()` and `toListObservable()` on a lazy association, still return `Observable<List<T>>`, so subscribers to them do not change. + +### Subscribers + +- Closure subscriptions, `observable.subscribe { Book book -> ... }` and the two-closure `subscribe(onNext, onError)`, are unchanged. +- A full subscriber implements `Observer<T>`: add `onSubscribe(Disposable d)`, rename `onCompleted()` to `onComplete()`, and keep the `Disposable` to cancel instead of calling `unsubscribe()` on a `Subscription`. +- `DetachedCriteria`, `ObservableProxy` and the lazy association collections implement `ObservableSource<T>`. Their `subscribe(Observer)` returns `void`; code that kept the returned `Subscription` keeps the `Disposable` from `onSubscribe` instead. They can be passed to `Observable.wrap(..)`, `concatWith(..)` and any other operator that takes an `ObservableSource`. +- `TestSubscriber` tests become `TestObserver` tests: `awaitTerminalEvent(..)` is `await(..)`, `assertCompleted()` is `assertComplete()`, and `onNextEvents` is `values()`. + +### Empty Results and Null + +RxJava 3 never emits `null`. + +- A query with no result completes empty, as before. +- `save()`, `insert()` and `persist()` on an instance the datastore did not write complete empty instead of emitting `null`. +- An `@RxSchedule` service method returning `Single` whose body returns `null` signals `java.util.NoSuchElementException`; one returning `Observable` completes empty. Where a missing result is expected, declare `Observable` and use `firstElement()`, which gives a `Maybe` whose `blockingGet()` returns `null` when empty, rather than catching the exception. RxJava 3's own zero-or-one type, `Maybe`, is not yet recognised as a data service return type. +- A `switchMap` or `flatMap` function must return an `ObservableSource`, never `null`; use `Observable.empty()`. + +### Datastore Implementations + +A datastore implementation of RxGORM compiled for Grails 8 or earlier, such as the RxGORM for MongoDB plugin, links against RxJava 1 and fails at runtime with `NoClassDefFoundError: rx/Observable`. It must be rebuilt against this release. The `rx-mongodb` profile feature is removed, because the plugin it generated has had no release since GORM 6.1 and was never published to Maven Central. `RxEntity` has had no datastore implementation for several releases, so an application that implements it should move to `GormEntity` with the RxJava data services, which work on every GORM datastore. + +## Deprecated RxJava 1 and RxJava 2 Modules + +`grails-async-rxjava`, `grails-async-rxjava2`, `grails-events-rxjava` and `grails-events-rxjava2` are deprecated for removal. They still work and log a warning such as `The RxJava 1 promise factory (grails-async-rxjava) is deprecated` when the promise factory or event bus is created. + +- Replace `org.apache.grails.async:grails-async-rxjava` or `grails-async-rxjava2` with `grails-async-rxjava3`. +- Replace `org.apache.grails.events:grails-events-rxjava` or `grails-events-rxjava2` with `grails-events-rxjava3`. +- The `Promise` and `EventBus` APIs are the same for every implementation, so an application that only uses the Grails APIs needs no code change. Code that reaches into the RxJava types behind them, such as `RxPromise.toObservable()`, moves to the RxJava 3 types as described above. Review Comment: `RxPromise` is `@PackageScope` in all three modules (`rxjava3/RxPromise.groovy:52`), so an application can't name it. Also, as the comment on `upgrading90x.adoc` shows, "needs no code change" doesn't hold for a promise of `null`. -- 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]
