Start and manage an embedded MongoDB so generated apps run without one installed - #16095
Open
codeconsole wants to merge 7 commits into
Open
Start and manage an embedded MongoDB so generated apps run without one installed#16095codeconsole wants to merge 7 commits into
codeconsole wants to merge 7 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16095 +/- ##
==================================================
+ Coverage 52.3426% 52.4386% +0.0960%
- Complexity 18293 18371 +78
==================================================
Files 2036 2041 +5
Lines 96346 96572 +226
Branches 16831 16863 +32
==================================================
+ Hits 50430 50641 +211
- Misses 38492 38501 +9
- Partials 7424 7430 +6
🚀 New features to boost your workflow:
|
A Forge application with MongoDB selected fails on startup unless a MongoDB happens to be listening on the configured url, because nothing starts one. Add grails-data-mongodb-embedded, an ApplicationContextInitializer that starts a server before Grails Data connects and publishes its url into the properties the application reads. It runs before the context refreshes because the url has to exist before the datastore bean that reads it is created. Two backends sit behind one interface. mongo-java-server reimplements the wire protocol in Java, starts in milliseconds and downloads nothing, so it is the default and ships with the module. Flapdoodle runs a real mongod, so it is the only backend that supports transactions, change streams and persistence; it is compileOnly here and added by applications that want it, because it pulls in jgrapht under LGPL-2.1 or EPL-2.0 which an Apache release should not require. Forge now wires this into the MongoDB feature by default, with the in-memory backend for development and test and flapdoodle for production, where the database is kept in ./prodDb. The generated application carries no source for this, only configuration, in the same way the H2 database is wired. mongo-java-server declares only "The BSD License", which CycloneDX maps to BSD-4-Clause. Its LICENSE has three numbered clauses and no advertising clause, so map it to BSD-3-Clause alongside the org.jline entries that need the same correction.
codeconsole
force-pushed
the
feature/mongodb-embedded-8.0.x
branch
from
August 5, 2026 00:31
777785a to
fff5f3e
Compare
…document Production no longer enables the embedded MongoDB. The initializer publishes its url ahead of every other property source, so an application deployed with MONGO_HOST and MONGO_PORT set would have ignored them and quietly served an empty local database. Development and test keep it; an application that wants one in production enables it for that environment itself. This also removes flapdoodle from generated applications, since only production used it. Reuse is now limited to a server this JVM started, tracked by port. Probing the socket treated any listener as a reusable MongoDB, so an unrelated service on the port produced a url pointing at it and a misleading protocol error later. Anything else holding the port now fails the start with an error naming the port. This class ships in a jar and so survives a devtools restart in the base classloader, which is what made tracking possible without the probe. Document the module in the MongoDB manual: the two backends and their trade-offs, every configuration property, restart and port behaviour, why production should stay external, and use outside Grails.
Asking a backend whether it is available means holding one, and holding one means
loading its class -- which resolves the types named in its methods. So
constructing the flapdoodle backend without flapdoodle fails before it can answer
that it is unavailable.
Flapdoodle is deliberately not a dependency of this module, so that is the
ordinary case: the initializer could not be created at all, and an application
that wanted the in-memory server got
Unable to instantiate factory class [EmbeddedMongoInitializer]
Caused by: NoClassDefFoundError: de/flapdoodle/reverse/Transition
naming a library it never asked for. Setting the backend to in-memory did not help
either, because the constructor runs before any property is read -- so the
documented default could never start.
The question is now asked of the class loader instead, by name rather than by
type, and only a backend whose library is present is offered. Asking for
flapdoodle without it on the classpath still says that its library is missing
rather than that the name means nothing.
Ahead-of-time processing refreshes a context to read its bean definitions and write them out as code. Nothing in that context is meant to run, so a database no one will query is of no use to it -- and starting one does not merely waste time, it does not finish: the server listens on a non-daemon thread and is stopped only by a JVM shutdown hook, so processAot completed its work and then hung holding port 27017 until it was killed. Found on an application that enabled the embedded server outside development, which an application training an AOT cache has to do: the training run runs in production, and needs a database it can actually reach. (cherry picked from commit e9e8a6444f7cda17df7e15490a9a6fc155160ff8)
borinquenkid
requested changes
Aug 7, 2026
borinquenkid
left a comment
Member
There was a problem hiding this comment.
Once you fix the codecov the plan is sensible and approved
CRaC refuses to checkpoint a process holding an open socket, and an application using this module holds several. The in-memory backend runs the server inside the JVM, so the image would have to contain its listening socket, its event loops and the server side of every connection; the driver adds one socket per pooled connection plus its server monitors. Checkpointing failed with a CheckpointOpenSocketException per socket, so an embedded MongoDB could not be used with CRaC at all. Both ends now take part in Spring's lifecycle, which stops beans before a checkpoint and starts them again after a restore. That needs no dependency on org.crac, and covers an ordinary shutdown too. EmbeddedMongoLifecycle stops the server and binds it again afterwards, below the datastore's phase so it outlives the client that talks to it. MongoDatastore closes its MongoClient, which is what releases the sockets and stops the monitor threads -- draining the pool does not, the monitors stay connected -- and builds a replacement from the same factory and configuration on restore. A client the application supplied is left alone, since its lifecycle belongs to whoever created it. MongoServer.shutdownNow() closes its backend, and closing a backend clears every database, so the in-memory backend now keeps its data across a restart. The data lives on the heap and the checkpoint image preserves it, so a restored process comes back with the collections it had rather than an empty database. Verified against a generated MongoDB application on Zulu CRaC 25: checkpoint and restore both succeed, restore completes in 54ms against 3.4s to start cold, and a document written before the checkpoint is readable after it.
RunningInMemoryMongo keeps the bound port rather than the InetSocketAddress it came from, so java.net.InetSocketAddress is no longer referenced.
The stop and restart that CRaC drives had no test, and it is where the subtle failure lives: a server stopped only to release its sockets must come back with the data it had, or a restored process silently returns an empty database. EmbeddedMongoLifecycleSpec drives that cycle against both backends -- the in-memory server keeping its collections on the heap, and a real mongod keeping a persistent database-dir -- and fails without the backend that declines to clear itself on shutdown. MongoDatastoreLifecycleSpec covers the other end: the client GORM owns is closed on stop, rebuilt on start, and the replacement rather than the client it replaced is what close() has to release. A client the application supplied is left alone throughout. A closed driver is told from an open one by the server selection it refuses, so no MongoDB is needed to observe it. The remaining additions are the configuration and failure paths that were described but never exercised: a backend named without its library, no backend at all, a version flapdoodle does not know, a database directory that cannot be created, blank settings, and the port following server.port. Line coverage of the new module goes from 68% to 100%; the branches left are one defensive null check and a port default that could only be observed by binding 27017.
borinquenkid
approved these changes
Aug 8, 2026
✅ All tests passed ✅🏷️ Commit: 8b8bea0 Learn more about TestLens at testlens.app. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
An application generated by Forge with MongoDB selected does not start:
Nothing starts a MongoDB, so the application only works if one happens to be listening on the configured url. There is currently no embedded option on
8.0.xat all — theembedded-mongodbfeature that existed on7.0.xwas dropped, and it was test-scoped and pinned to MongoDB 3.2.1 in any case.Scope
Providing a database the application did not ask for means owning its lifecycle, so this covers starting it, stopping it, and every point in between where the process expects it to still be there:
These are one change rather than two because they are the same commitment. An embedded MongoDB that a generated application cannot checkpoint is a database that works until the application does something ordinary with it, and the fix belongs in the module that started the server rather than in every application that uses one.
Approach
A new module,
grails-data-mongodb-embedded, contributes anApplicationContextInitializerthat starts a server and publishes its url into whichever properties the application reads. It is an initializer rather than an auto configuration because the url has to be in theEnvironmentbefore the datastore bean that reads it is created.Two backends sit behind one interface:
in-memory(mongo-java-server)flapdoodleapidependency of the modulecompileOnly; applications add it$textdatabase-dirFlapdoodle is deliberately not a dependency of the module: it pulls in
org.jgrapht:jgrapht-core, offered under LGPL-2.1 or EPL-2.0, which an Apache release should not require. An application that wants a real mongod adds it itself, the same way it picks a SQL driver. The published pom contains onlyspring-context,mongo-java-serverandslf4j-api.The target property is configuration (
embedded.mongodb.property-names, defaulting tograils.mongodb.url) rather than hardcoded, so the module is not limited to Grails Data. Spring Data users remain better served by flapdoodle's ownde.flapdoodle.embed.mongo.spring3xauto configuration, which this does not duplicate.Surviving a checkpoint
CRaC refuses to checkpoint a process holding an open socket, and an application using this module holds several. The in-memory backend runs the server inside the JVM, so the image would have to contain its listening socket, its event loops and the server side of every connection; the driver adds one socket per pooled connection plus its server monitors. Every one of them is reported:
Both ends now take part in Spring's lifecycle, which stops beans before a checkpoint and starts them again after a restore. That needs no dependency on
org.crac, and covers an ordinary shutdown too.EmbeddedMongoLifecyclestops the server and binds it again afterwards, at a phase below the datastore's so it outlives the client that talks to it.MongoDatastorecloses itsMongoClient, which is what releases the sockets and stops the monitor threads — draining the pool does not, the monitors stay connected — and builds a replacement from the same factory and configuration on restore. A client the application supplied is left alone, since its lifecycle belongs to whoever created it.One wrinkle is worth calling out for review.
MongoServer.shutdownNow()closes its backend, andAbstractMongoBackend.close()clears every database. That is right for a server that is finished with, and wrong when the server is being stopped only to release its sockets, so the in-memory backend keeps its data across a restart. Without it the checkpoint succeeds and the restored process silently comes back with an empty database, which is a worse failure than not checkpointing.Forge
GrailsDataMongoDBwires this in by default rather than offering it as a separate feature, since the starter not working was the problem. Development and test use the in-memory backend; production uses flapdoodle and keeps its database in./prodDb, mirroring how the H2 database is wired for the Grails website application. No source is generated into the application — only the dependency and theenvironments:configuration.Verifying
A generated MongoDB application, with no MongoDB installed and no Docker:
GET /returns 200, a driver round-trips a document, and nomongodprocess exists — port 27017 is held by the application JVM.The same application on Azul Zulu CRaC 25, in Docker:
54ms to restore against 3.4s to start cold, and a document written before the checkpoint is read back after it. Checked against a run that was never checkpointed, since a restored process that comes back empty otherwise looks like a success.
The in-memory backend is also what makes this path testable without external infrastructure: the run above needs a CRaC JDK and a container to checkpoint in, but no MongoDB beyond the one inside the JVM.
EmbeddedMongoInitializerSpeccovers both backends against real servers: a driver round-trip through each, backend selection and its error messages, publishing into several properties, reusing a server that is already listening (the devtools restart path), and the in-memory backend refusingdatabase-dirrather than silently discarding data.EmbeddedMongoLifecycleSpecdrives the checkpoint cycle itself, since that is where the subtle failure lives: the in-memory server keeping its collections across a stop taken only to release its sockets, and a real mongod keeping a persistentdatabase-diracross the same stop. It fails without the backend that declines to clear itself on shutdown, which is the bug described above.MongoDatastoreLifecycleSpeccovers the other end — the client GORM owns closed on stop, rebuilt on start, and the replacement rather than the client it replaced released byclose(), with an application-supplied client left alone throughout. A closed driver is told from an open one by the server selection it refuses, so none of this needs a MongoDB to observe.Notes for review
SbomPlugin: mongo-java-server declares only"The BSD License", which CycloneDX maps to BSD-4-Clause. Its LICENSE has three numbered clauses and no advertising clause, so it is BSD-3-Clause. Mapped alongside theorg.jlineentries that need the identical correction.publish-root-config.gradle: a module missing frompublishedProjectsfails configuration withExtension of type 'GrailsPublishExtension' does not exist, which does not point at the allowlist.compileOnly 'org.apache.groovy:groovy'in the module: its main sources are Java so that a plain Spring Boot application can use it, but every published module still produces a groovydoc jar, and groovydoc cannot infer its classpath without Groovy present.MongoDatastoreis the one change outside the new module.mongobecomesvolatilerather thanfinalso a restore can replace it; every read other than construction already went throughgetMongoClient().buildInfo.versionas5.0.0, so development and test report 5.0 while production under flapdoodle reports 8.0. Worth documenting.EmbeddedMongoInitializer(List<EmbeddedMongoBackend>)constructor, because "this backend's library is not on the classpath" cannot be produced any other way when both jars are on the test classpath.EmbeddedMongoInitializerare left uncovered: a defensive null check that cannot be reached, and the port default, which could only be observed by binding 27017 and would collide with any locally installed MongoDB.