matrei commented on PR #16214:
URL: https://github.com/apache/grails-core/pull/16214#issuecomment-5703927219

   # Review
   
   **Head:** `f25990cdbc31fa975c6fa4181e04a3567729713e` (4 commits, merge of 
`8.0.x` at `c1b532aef9`), **base:** `8.0.x`, mergeable/clean per GitHub.
   **CI:** all 30 check runs green on the head SHA; codecov patch 82.35 % (3 
uncovered lines).
   **Open review thread:** jdaugherty asked for a What's New entry and 
documentation of the behaviour change. No docs are in the PR yet.
   
   ## What was run locally
   
   | What | Result |
   |------|--------|
   | `:grails-datastore-core:test` (`--no-build-cache`, `cleanTest`) | 152 
tests, 0 failures |
   | `:grails-data-simple:test` | 4 tests, 0 failures |
   | `:grails-data-mongodb-core:test` | 673 tests, 0 failures, 45 skipped 
(embedded replica set, no Docker) |
   | `:grails-data-mongodb-spring-data:test` | 15 tests, 0 failures, incl. both 
new `UnifiedMongoTransactionSpec` cases |
   | `:grails-data-neo4j-core:compileJava/compileGroovy` | compiles against the 
new `AbstractSession` |
   | New tests against the merge-base sources (five main files swapped to 
`c1b532aef9`) | all three new read-only cases fail (2 in 
`grails-data-mongodb-core`, 1 in `grails-data-mongodb-spring-data`); the 
read-write cases pass either way, as the PR body claims |
   | Style checks on `grails-datastore-core`, `grails-data-mongodb-core`, 
`grails-data-mongodb-spring-data` | `checkstyleMain/Test`, `codenarcMain/Test`, 
`codeStyle` all pass on the three projects (no PMD/SpotBugs tasks exist on 
them; the root `aggregateViolations` was not run) |
   | Scratch probe (not committed): `withNewSession { save(); read-only tx; 
count() }` then `count()` in a fresh session | flush mode `AUTO` → `COMMIT` 
after the read-only commit; same-session `count()` = 0; after session close = 0 
|
   
   ## Verdict
   
   The code change is correct and small, and it does what the title says. What 
blocks merging is documentation: this is a user-visible behaviour change on 
every `DatastoreTransactionManager` datastore that overrides the new hook 
(Mongo today), jdaugherty's request for docs is still open, and the repo's rule 
is that user-facing changes do not merge without `grails-doc` coverage. Two 
wording problems in the PR narrative also need fixing so the docs that get 
written are accurate.
   
   ## Findings
   
   ### 1. Documentation is missing for a behaviour change (blocking)
   
   The PR removes a write that applications on Mongo currently get. There is 
nothing in `grails-doc/src/en/guide/introduction/whatsNew.adoc`, nothing in 
`grails-doc/src/en/guide/upgrading/upgrading80x.adoc` (section *20. Other 
Default Behavior Changes* is the natural home for a bullet), and nothing in 
`grails-data-mongodb/docs/src/docs/asciidoc/transactions.adoc`, which still 
opens with "MongoDB doesn't support transactions directly" and never mentions 
read-only semantics.
   
   The docs need to say precisely what happens to a write queued before a 
read-only transaction, because it is not "left where it was" (see finding 2). 
Verified with the scratch probe above and by reading the code:
   
   - `DatastoreTransactionManager.doBegin` 
(`grails-datastore-core/.../transactions/DatastoreTransactionManager.java:121-126`)
 sets the session to `FlushModeType.COMMIT` for a read-only definition and 
nothing ever restores it. `AbstractSession`'s default is `AUTO` 
(`AbstractSession.java:109`), so after the read-only commit the same session no 
longer auto-flushes before queries (`Query.java:760`), and 
`OpenSessionInViewInterceptor.postHandle` 
(`grails-datastore-web/.../OpenSessionInViewInterceptor.java:82`) skips its 
end-of-request flush for the same reason.
   - When the session closes, `AbstractAttributeStoringSession.disconnect()` 
(`:112-116`) calls `clear()`, so the queued insert is discarded.
   
   So the honest description is: *a write saved without `flush: true` before a 
read-only transaction in the same session is no longer persisted by that 
transaction, is invisible to later queries in that session, and is dropped when 
the session ends. Use `save(flush: true)` or a read-write transaction.* This 
matches Hibernate, where `GrailsHibernateTransactionManager.doBegin` sets 
`MANUAL` and `GrailsOpenSessionInViewInterceptor.postHandle` skips 
`MANUAL`/`COMMIT` sessions, so "the same meaning as on Hibernate" holds. It 
just has to be written down, with the migration advice, because on Mongo it 
used to work by accident.
   
   The Spring Data limitation from the new `UnifiedMongoTransactionSpec` case 
also belongs in the Mongo docs: on `GormSharedSessionMongoTransactionManager`, 
`readOnly` only governs the GORM flush. A `MongoTemplate` write inside a 
read-only transaction still commits, because it goes straight into the shared 
`ClientSession`. The test enshrines that; users should not have to discover it 
from a test name.
   
   ### 2. The PR narrative misstates the flush-mode default and the fate of the 
queued write
   
   - Commit `05fb17e722` and the PR body say `FlushModeType.COMMIT` is "already 
the `Session` default". It is not; `AbstractSession.flushMode` initialises to 
`AUTO` and `ConnectionSourceSettings.flushMode` is `AUTO` too. The new comment 
inside `doBegin` is accurate ("keeps an AUTO session from flushing ahead of 
queries"), so only the commit message and PR body need correcting. Worth doing 
before squash, since the message becomes the permanent record.
   - The `then:` labels in `MongoTransactionDisabledSpec.groovy:78` and 
`MongoTransactionSpec.groovy:191` read "the queued write was left where it 
was". It was left in a session that is now in `COMMIT` mode and will `clear()` 
on close, so "left queued and discarded when the session closes" is what the 
test actually demonstrates. Same for the `UnifiedMongoTransactionSpec` name, 
which says "drops": that one is right, the other two should agree with it.
   
   ### 3. 
`AbstractMongoSession.beginTransactionInternal(TransactionDefinition)` has no 
null guard (nit)
   
   `grails-data-mongodb/core/.../AbstractMongoSession.java:277` calls 
`definition.isReadOnly()` unconditionally. 
`Session.beginTransaction(TransactionDefinition)` carries no nullability 
contract, the base `AbstractSession` default tolerates `null`, and 
`HibernateSessionSpec` already exercises the interface with `null`. Both 
in-tree callers (`DatastoreTransactionManager`, 
`Neo4jDatastoreTransactionManager`) always pass a real definition, so this is 
not reachable today, but `definition != null && definition.isReadOnly()` costs 
nothing and keeps the override as forgiving as the default it replaces.
   
   ### 4. `SessionOnlyTransaction` gains a branch with no test in its own 
module (nit)
   
   The read-only branch in 
`grails-datastore-core/.../SessionOnlyTransaction.java:70-72` is only covered 
from `grails-data-mongodb-core` via `MongoTransactionDisabledSpec`. A 
`SessionOnlyTransactionSpec` in `grails-datastore-core` with a `Mock(Session)` 
asserting `0 * session.flush()` for `readOnly = true`, `1 * session.flush()` 
otherwise, and `1 * session.clear()` on rollback would cover the class where it 
lives, lock the contract the deprecation Javadoc promises to out-of-tree 
datastores, and probably recover the codecov patch gap. The deprecated two-arg 
constructor could be asserted to behave as read-write in the same spec.
   
   ### 5. Non-blocking observations
   
   - `MongoTransaction.commit()` failure path (`MongoTransaction.java:92-113`) 
still calls `session.clear()` when a read-only commit fails, which discards the 
surrounding session's queued writes that the transaction never owned. 
`DatastoreTransactionManager.doRollback` does the same on any rollback, and 
Spring's Hibernate manager clears pre-bound sessions on rollback too, so this 
is consistent and pre-existing. Mentioning it only because the new Javadoc says 
a read-only transaction "has no pending operations of its own".
   - `@Deprecated(since = "8.0", forRemoval = true)` matches the convention 
used elsewhere in the tree. Removing `MongoTransaction(AbstractMongoSession, 
ClientSession)` outright is fine: it was introduced in an 8.0.0 milestone 
(#15744) and `AbstractMongoSession` is its only caller.
   - Codecov's three uncovered lines are almost certainly the deprecated 
`SessionOnlyTransaction` constructor and the no-arg 
`AbstractMongoSession.beginTransactionInternal()`, which is now only reachable 
via `Session.beginTransaction()` with no argument and has no in-tree caller for 
Mongo. Finding 4 covers the first; the second is dead-ish but harmless.
   
   ## Verified as correct
   
   - `AbstractSession.beginTransaction(TransactionDefinition)` now forwards the 
definition, and the default `beginTransactionInternal(TransactionDefinition)` 
delegates to the no-arg method, so `SimpleMapSession` is unchanged (its 
`MockTransaction.commit()` never flushed; the read-write flush still comes from 
`doCommit`). `Neo4jSession` overrides `beginTransaction(TransactionDefinition)` 
directly and never enters this path. Both Hibernate sessions throw 
`UnsupportedOperationException` for it regardless.
   - No remaining callers of the removed `MongoTransaction` constructor or the 
deprecated `SessionOnlyTransaction` constructor anywhere in the tree, including 
`grails-data-neo4j`.
   - Read-write commits are unchanged: `DatastoreTransactionManager.doCommit` 
still flushes before `transaction.commit()`, so the flush inside 
`MongoTransaction.commit()` / `SessionOnlyTransaction.commit()` remains a no-op 
on that path and still covers callers that commit the `Transaction` directly. 
The read-write tests in `MongoTransactionDisabledSpec` and 
`UnifiedMongoTransactionSpec` assert this.
   - The new tests genuinely test the change: with the five main files swapped 
back to the merge-base, `MongoTransactionSpec`, `MongoTransactionDisabledSpec` 
and `UnifiedMongoTransactionSpec` each fail exactly their read-only case and 
nothing else.
   - Read-only server-side transactions still open and commit a `ClientSession` 
transaction; with nothing flushed into it the commit is empty, and 
`commitWithRetry` / the failure cleanup are untouched.
   - The Spring Data interop manager inherits the behaviour with no code 
change, which the two new `UnifiedMongoTransactionSpec` cases pin down.
   


-- 
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]

Reply via email to