codeconsole opened a new pull request, #16495:
URL: https://github.com/apache/grails-core/pull/16495
## Problem
With `-Dspring.context.checkpoint=onRefresh`, Spring takes the CRaC
checkpoint as the context refreshes: after every bean has been created and
before it starts any lifecycle bean. Before a checkpoint
`DefaultLifecycleProcessor` stops only the beans that are running, and at that
point none are, so nothing opened while beans were being created is closed. An
application using GORM for MongoDB never got past it. The checkpoint failed,
and the application did not start:
```
org.springframework.context.ApplicationContextException: Failed to take CRaC
checkpoint on refresh
Caused by: org.crac.CheckpointException
Suppressed:
jdk.internal.crac.mirror.impl.CheckpointOpenSocketException:
Socket[addr=host.docker.internal/0.250.250.254,port=27017,localport=49302]
Suppressed:
jdk.internal.crac.mirror.impl.CheckpointOpenSocketException:
Socket[addr=host.docker.internal/0.250.250.254,port=27017,localport=49286]
```
That is an ordinary MongoDB server, not the embedded one. Two things
connected while the beans were being created:
- `MongoConnectionSourceFactory` created the `MongoClient` as the datastore
was built, and the driver starts the monitors that connect to the server as
soon as a client exists.
- `MongoDatastore` built the indexes the domain classes declare in its
constructor.
An embedded MongoDB added a third: `EmbeddedMongoInitializer` binds the
server before the context refreshes, and `EmbeddedMongoLifecycle` is not
stopped before this checkpoint either. With `-Dspring.context.exit=onRefresh`,
Spring halts the JVM at the same point, which runs no shutdown hook, so a
flapdoodle `mongod` was left running and holding its port.
A checkpoint of a running application already worked, but the client the
application had been handed did not survive it. GORM closed every client it
owned before the checkpoint and built replacements after the restore, and the
`mongo` bean, which the guide shows injected into controllers and services,
kept the closed one: every call on it after a restore failed with
`IllegalStateException: state should be: open`.
## Change
- **`RestartableMongoClient`.** The `MongoClient` GORM creates for a
connection is now a handle on a driver client that it builds when it is first
used or started. `stop()` closes the driver client and refuses use until
`start()` builds a new one, and `close()` is final. The handle stays the same
object, so the `mongo` bean, a client obtained from
`MongoDatastore.getMongoClient()`, multi-tenancy and the Spring Data
integration go on working after a restore. `MongoConnectionSourceFactory`, the
`Supplier<MongoClient>` constructor the Boot auto-configuration uses and the
`MongoClientSettings.Builder` constructors all produce one. The connection
settings are still built and checked as the datastore is created, so a URL that
cannot be parsed is reported there. A client a custom connection source factory
builds itself is still closed and replaced as before, and the factory's Javadoc
says to wrap it instead.
- **`MongoDatastore` connects in `start()`.** The first start connects the
client of every connection GORM owns and builds the declared indexes, in the
datastore's lifecycle phase (`LIFECYCLE_PHASE`, -1000). Spring starts it before
it publishes `ContextRefreshedEvent`, so the indexes are in place before
`BootStrap` runs and before the web server accepts a request. A datastore that
nothing starts, such as one created outside an application context, starts
itself when the first session is opened on it, so code that creates one and
queries it finds its indexes as before. A datastore that has been stopped is
not started again by being used: its clients refuse until it is started, so a
request arriving during a checkpoint cannot reopen a socket. `isRunning()` is
`false` until the datastore has started.
- **`EmbeddedMongoInitializer`.** With `spring.context.checkpoint=onRefresh`
or `spring.context.exit=onRefresh`, it publishes the URL and leaves starting
the server to `EmbeddedMongoLifecycle`, which the context starts before the
datastore (phase -2000). A URL asking for port `0` is given a free port when it
is published, since the server binds later. Without either property nothing
changes: the server is listening as soon as the initializer has run, so code
that talks to MongoDB while beans are still being created finds it.
## Limits
A checkpoint taken as the context refreshes needs nothing else to have
connected either. These still do, and the CRaC section of the guide names them:
- A `MongoClient` the application hands to GORM, including the `mongo` bean
Spring Boot's own `MongoAutoConfiguration` defines, which the Boot
auto-configuration passes to GORM rather than building a client itself. It is
connected as it is created, and GORM neither starts nor stops a client it does
not own.
- Application code that queries MongoDB while beans are being created,
rather than in `BootStrap` or later.
- `MongoConnectionSources`, which reads the connections from MongoDB as the
datastore is created.
A `MongoDatabase`, `MongoCollection` or `ClientSession` obtained from the
client belongs to the driver client it came from, so one held across a
checkpoint is closed with it. The client itself is what to keep.
## Tests
- `RestartableMongoClientSpec`: nothing is built until the client is used
(naming, printing and comparing it are not uses); the first use builds one
driver client and later uses share it; `start()` builds it at once; `stop()`
closes it, use is refused with a message naming the connection, and `start()`
serves the same handle from a new one; a client stopped before it was ever used
builds nothing; `close()` is final; a driver client that fails to build leaves
the handle untouched; sixteen threads using it for the first time at once share
one driver client; and every method of `MongoClient` is passed through, checked
by reflection against a recording driver client, so a method added to the
driver's interface cannot be left out silently.
- `ConnectsWhenStartedSpec`: GORM registered through
`MongoDbDataStoreSpringInitializer`, as a Grails application registers it,
against a real server, with a driver listener on every client and a lifecycle
bean at `Integer.MIN_VALUE`, which is where the checkpoint is taken. When that
bean starts, no client has been created and no command sent; after the refresh
the datastore is running, has issued `createIndexes`, and the domain class's
index exists. It fails without the `MongoDatastore` change.
- `MongoDbDataStoreSpringInitializerSpec`: the `mongo` bean reaches MongoDB
after the context is stopped and started again, as Spring does around a
checkpoint, and is still the client GORM uses. It fails with `state should be:
open` without the `RestartableMongoClient` change.
- `MongoDbGormAutoConfigurationCloseSpec`: the client the Boot
auto-configuration builds from Spring Boot's settings does not exist when the
first lifecycle bean starts, is created when the datastore starts, and stays
the same client across a stop and start.
- `MongoDatastoreLifecycleSpec`: a datastore is not running and none of its
clients, on any connection, is connected until it is started; the first session
starts a datastore nothing has started; a stopped datastore is not started by a
session opened on it; clients are restarted in place on every connection,
including one added at runtime and a named connection beside an
application-supplied default client; and a client a custom factory builds
itself is still closed and replaced.
- `EmbeddedMongoStartedWithTheContextSpec`: with either property set, the
URL is published and nothing is listening until the context starts; a lifecycle
bean at `Integer.MIN_VALUE` finds nothing listening; port `0` is published as a
real port; a port held by something else is reported when the server starts;
and a server reused by a reloaded context is not started before that context
starts it. Every feature fails without the initializer change. The properties
are set as an application sets them, after `DefaultLifecycleProcessor` has been
loaded, so the contexts the spec refreshes are not checkpointed or halted.
- The index-build specs that created a datastore and expected its indexes
from the constructor now start it first, as the application context does.
`BuildIndexesAsyncSpec` checks that the synchronous build runs on the thread
that starts the datastore.
- All tests pass in `grails-data-mongodb-core`,
`grails-data-mongodb-embedded`, `grails-data-mongodb-spring-boot`,
`grails-data-mongodb-spring-data` and `grails-data-mongodb`, and in the
`mongodb/base`, `mongodb/database-per-tenant` and `mongodb/springboot` test
examples. Checkstyle and CodeNarc are clean for the changed modules. I have not
run the whole `./gradlew build`.
Verified end to end with a Grails 8.0.0-RC2 application carrying these
commits, on Azul Zulu CRaC 25, against MongoDB in Docker. Before,
`-Dspring.context.checkpoint=onRefresh` failed with the exception above. With
the change the checkpoint is taken before any client exists, the restore
succeeds, the datastore connects as Spring restarts the lifecycle beans,
`BootStrap` reads and updates a user, and logging in as that user works.
## Docs
- The guide's CRaC section covers a checkpoint taken as the context
refreshes as well as one taken while the application runs, says that the client
GORM hands out survives a restore (it used to say to obtain it again), and
lists what still connects early.
- Index Creation on Startup and Building Indexes in the Background say when
the datastore starts, and the Embedded MongoDB section says when its server is
started under the two properties.
- What's New in the Grails guide has an entry for CRaC support in GORM for
MongoDB.
--
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]