Add opt-in MongoDB multi-document transactions to GORM for MongoDB#15744
Add opt-in MongoDB multi-document transactions to GORM for MongoDB#15744codeconsole wants to merge 7 commits into
Conversation
GORM for MongoDB previously treated a transaction as a client-side flush boundary: pending writes were batched and flushed on commit, but each write auto-committed individually and nothing rolled back when a later operation failed. This adds real server-side transactions backed by a com.mongodb.client.ClientSession. When grails.mongodb.transactional is enabled (default false), a GORM transaction starts a ClientSession and MongoDB transaction and every read and write for the session runs within it, committing or aborting atomically. A new MongoTransaction drives the commit (retrying on an UnknownTransactionCommitResult) and the abort, and closes the session afterwards. The feature is opt-in and degrades gracefully: a standalone topology is detected at runtime and falls back to the legacy flush-only behavior with a one-time warning. Identifier generation for native Long ids is intentionally left non-transactional, mirroring the semantics of database sequences.
borinquenkid
left a comment
There was a problem hiding this comment.
Hi @codeconsole,
Please keep an eye on #15678 (GORM: Shared Mapping Registry O(M+N) Scaling), which introduces significant internal structural refactoring to how GormRegistry and MongoDatastore handle tenant routing and fallback resolution.
Since that optimization is targeting 8.0.x-hibernate7, your transaction changes here will be downstream from those modifications. It might be worth checking your diff against those updates to prevent initialization order regressions or multi-tenant signature mismatches when merging into the 8.0 release line.
…ve Long id tests On a failed commit (or a flush failure during commit), MongoTransaction now explicitly aborts the server transaction rather than relying on ClientSession.close() to abort it implicitly. Adds tests covering native Long identifier generation inside a transaction: ids are generated and persisted on commit, and the document is rolled back on failure even though the id counter is intentionally not enrolled in the transaction.
…ined The clientSession field is accessed only on the owning session's thread (per the AbstractSession single-thread-confinement contract), so it needs no synchronization. Adds a clarifying comment; no behavior change.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15744 +/- ##
================================================
+ Coverage 0 49.4804% +49.4804%
- Complexity 0 16717 +16717
================================================
Files 0 1948 +1948
Lines 0 92564 +92564
Branches 0 16158 +16158
================================================
+ Hits 0 45801 +45801
- Misses 0 39643 +39643
- Partials 0 7120 +7120
🚀 New features to boost your workflow:
|
jdaugherty
left a comment
There was a problem hiding this comment.
Reading about the MongoDB transactions and asking AI made me think there is a gap here. Was whole-transaction retry on TransientTransactionError consciously deferred?
| // ignored. | ||
| if (!warnedTimeoutIgnored) { | ||
| warnedTimeoutIgnored = true; | ||
| LOG.warn("A per-transaction timeout was requested but GORM for MongoDB does not apply it to the " + |
There was a problem hiding this comment.
We should throw an error here, not silently fail.
There was a problem hiding this comment.
Done — throws TransactionUsageException on a non-default timeout now. doBegin rolls back the started session and rethrows as CannotCreateTransactionException, so nothing leaks.
|
The implementation of grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoTransaction.java |
|
|
||
| @SuppressWarnings({"rawtypes", "unchecked"}) | ||
| public DeleteResult deleteMany(com.mongodb.client.MongoCollection collection, Bson filter) { | ||
| return clientSession != null ? collection.deleteMany(clientSession, filter) : collection.deleteMany(filter); |
There was a problem hiding this comment.
Shouldn't all of these not null checks use the helper hasActiveTransaction() instead? Isn't the session only required for the transaction case?
There was a problem hiding this comment.
Yes — all nine use hasActiveTransaction() now. Only the transactional case needs the session, and it falls back to the session-less overload if one ever outlives its transaction.
- setTimeout now throws TransactionUsageException for a non-default timeout instead of silently ignoring it. The server transaction is started before the manager applies a timeout, so it cannot be honored at this layer; DatastoreTransactionManager.doBegin catches it, rolls back the just-started ClientSession, and rethrows as CannotCreateTransactionException, so nothing leaks. - All nine AbstractMongoSession driver helpers branch on hasActiveTransaction() rather than a raw clientSession null check, so a session that lingers after its transaction commits falls back to the session-less overload. - Document on the MongoTransaction Javadoc that whole-transaction retry on TransientTransactionError is intentionally deferred: it requires re-executing the transaction body, which the Spring PlatformTransactionManager SPI cannot do. - Add a spec asserting a per-transaction timeout is rejected and the datastore stays usable afterwards, and document the timeout behavior in advancedConfig.adoc.
|
Yes — deliberately, and it's a boundary rather than missing work. Commit-retry is implemented: on an Retrying the whole transaction on a |
Reword the MongoTransaction Javadoc so it no longer reads as deferred work: whole-transaction retry on a TransientTransactionError is left to the application (as Spring Data MongoDB's own transaction manager does), since re-running the transaction body would repeat its side effects.
Bring MongoTransaction and AbstractMongoSession in line with the reviewed Layer 1 code: MongoTransaction.commit() now explicitly aborts the server-side transaction on a failed commit rather than relying on close() to do so implicitly, and the AbstractMongoSession clientSession field carries its threading/nullability contract note. This makes the interop branch's copy identical to apache#15744.
✅ All tests passed ✅🏷️ Commit: 90e5103 Learn more about TestLens at testlens.app. |
What
Adds opt-in MongoDB multi-document transactions to GORM for MongoDB.
Previously GORM for MongoDB never used a
com.mongodb.client.ClientSession: every driver write was issued session-less and auto-committed, so a GORM transaction was only a client-side flush boundary — writes already flushed within it were not rolled back on failure (no server-side atomicity).With
grails.mongodb.transactional = true, a GORM transaction now starts and drives a realClientSessiontransaction, so all reads and writes commit or roll back atomically:How
MongoTransaction(replaces the flush-onlySessionOnlyTransactionwhen enabled):commit()flushes thencommitTransaction()(with bounded retry onUnknownTransactionCommitResult),rollback()aborts; both close the session. On commit failure the GORM session cache is cleared.AbstractMongoSessionholds the activeClientSession, starts it inbeginTransactionInternal(), and routes every read/write through small helpers that pass the session when a transaction is active and stay session-less otherwise.MongoQuery, and theMongoStaticApi/MongoEntitysurface.DatastoreTransactionManageris unchanged — it already orchestrates flush/commit/rollback; this just supplies aTransactionthat drives a server transaction.Opt-in and fallback
grails.mongodb.transactionaldefaults tofalse) — no behavior change for existing apps.Boundaries
Longcounter) is intentionally left non-transactional, mirroring database sequence semantics.PROPAGATION_REQUIRED) is supported;REQUIRES_NEW/NESTEDare not.Tests
MongoTransactionSpec— commit persists multiple docs; rollback discards them on the server; read-your-writes within a transaction; cross-collection atomic rollback;findOneAndDeleteparticipates in the transaction; nested GORMREQUIRES_NEW.MongoTransactionDisabledSpec— default-off keeps the legacy flush behavior.Targets
8.0.x. Independent of #15743; this is also the prerequisite for a follow-up Spring Data MongoDB interop module.