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

   ## Review Findings
   
   Head `8056ea18db`, base `8.0.x` at `1fadd5c812`; the branch sits directly on 
the base and merges clean. All 77 completed CI checks pass (macOS build still 
pending); the one TestLens failure is `UserControllerSpec > User list` on an 
outdated commit, the known 9%-flaky scaffolding Geb test (#16030), unrelated to 
this change.
   
   What I ran on the head, all green:
   
   - `:grails-datamapping-core-test:test --tests MultipleDataSourceSpec`: 4 
tests, 0 failures.
   - `:grails-data-neo4j-core:test --tests NamedConnectionsSpec --tests 
Neo4jConnectionSourceSettingsSpec`: 4 tests, 0 failures.
   - `:grails-data-mongodb-core:test` for the seven touched specs 
(`MultipleConnectionsSpec`, `CheckpointRestoreConnectionsSpec`, 
`SchemaBasedMultiTenancySpec`, `MongoDatastoreLifecycleSpec`, 
`MongoDatastoreExternalClientSpec`, `MongoConnectionSourceFactorySpec`, 
`MongoConnectionSourceSpec`): 34 tests, 0 failures, against a real container.
   - `:grails-data-mongodb-spring-data:test`, the whole module: 17 tests, 0 
failures.
   - `codeStyle` on `grails-datamapping-core`, `grails-data-mongodb-core`, 
`grails-data-mongodb-spring-data` and `grails-data-neo4j-core`: no violations.
   
   The CRaC part, the Neo4j startup fix and the Neo4j qualifier fix are all 
correct as far as I can verify, and each of the claims in the description that 
I could check holds (details under Confirmed). The findings are about the 
connection scope in `GormRegistry`, which is the one piece that changes 
behaviour for every GORM implementation rather than just MongoDB.
   
   ### [P2] A scope named `default` does not mean what `Book.'default'` means, 
and the two halves of one `withConnection` block can disagree
   
   References:
   
   - 
`grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy:550-557`,
 `603-609`, `659-665`
   - 
`grails-data-mongodb/core/src/main/groovy/grails/mongodb/MongoEntity.groovy:243-251`
   
   `resolveStaticApi` (and the instance and validation twins) short-circuit to 
`staticApiRegistry.getDirect(normalizedClassName, scoped)` before the 
`MultiTenant` handling. For a non-tenant entity, and for a tenant entity with a 
non-default scope, that is exactly what naming the connection would do: 
Priority 1 of the multi-tenant branch is the same `getDirect` call. It differs 
only when the scope is `default` on a `MultiTenant` entity in DATABASE or 
SCHEMA mode: an explicit `Book.'default'` skips Priority 1 and resolves the 
*current tenant's* API at Priority 2, while the scope returns the literal 
default connection's API.
   
   I checked this with a throwaway spec on a `SimpleMapDatastore` in DATABASE 
mode with connections `default`, `foo` and `bar`, tenant `foo` bound through 
`SystemPropertyTenantResolver`, two books in `foo`, one in `bar`:
   
   ```
   ProbeBook.'default'.count()                                                  
        == 2   // tenant foo
   GormRegistry.withConnectionScope(ProbeBook, ConnectionSource.DEFAULT) { 
ProbeBook.count() } == 0   // the default store
   ```
   
   Inside `Book.withConnection(ConnectionSource.DEFAULT) { }` on such an entity 
the closure's delegate is `GormEnhancer.findStaticApi(this, 'default')`, which 
resolves the tenant's API, so `list()` reads the tenant and `Book.list()` reads 
the default store in the same block. The test in `MultipleDataSourceSpec` 
(`withConnectionScope(Player, ConnectionSource.DEFAULT)`) does not see this 
because `Player` is not multi-tenant.
   
   The fix is also a simplification. Rather than a separate lookup, feed the 
scope in as the qualifier and let the existing resolution run:
   
   ```groovy
   GormStaticApi resolveStaticApi(Class entityClass, String qualifier) {
       String normalizedClassName = normalizeEntityKey(entityClass)
       // A lookup that names no connection follows the scope, exactly as if it 
had named it.
       if (qualifier == null) {
           qualifier = scopedConnection(normalizedClassName)
       }
       String normalizedQualifier = normalizeQualifier(qualifier)
       ...
   ```
   
   Same three lines in `resolveInstanceApi` and `resolveValidationApi`, and the 
three `getDirect` blocks go. Then "the scope is what naming the connection 
would do" is true by construction, the documentation sentence "An operation 
that names its own connection keeps it" needs no caveat, and most of the 
partial branches Codecov flags in this file go with them. For every case the 
PR's tests cover the result is identical.
   
   ### [P3] A scope silently overrides a `Tenants.withId` nested inside it
   
   References:
   
   - 
`grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy:553`
   
   Because the scope is consulted before `CurrentTenantHolder`, this reads 
`bar`, not `foo` (same probe as above):
   
   ```groovy
   GormRegistry.withConnectionScope(ProbeBook, 'bar') { Tenants.withId('foo') { 
ProbeBook.count() } }  // 1, bar's count
   ```
   
   That is consistent with the explicit form, `Tenants.withId('foo') { 
ProbeBook.bar.count() }`, where the named connection also wins, so I would not 
change the precedence. But it is a new way for a tenant switch to be ignored 
without any error, and DATABASE-mode tenants *are* connections, so 
`withConnection` on a multi-tenant entity is not an outlandish thing to write. 
One sentence in the `withConnectionScope` javadoc and in the two 
`withConnection` guide pages ("the block's connection takes precedence over the 
current tenant for that entity") would keep someone from debugging it. The P2 
change does not alter this behaviour, since Priority 1 runs before the tenant 
lookup either way.
   
   ### Nit: the `withConnection` pages could say the routing is per thread
   
   References:
   
   - 
`grails-data-mongodb/docs/src/docs/asciidoc/multipleDataSources/mongoClientSwitching.adoc:29`
   - 
`grails-data-neo4j/docs/src/docs/asciidoc/multipleDataSources/boltDriverSwitching.adoc:29`
   
   The scope is a `ThreadLocal`, so `Book.withConnection('moreBooks') { 
executor.submit { Book.list() } }` runs `Book.list()` on the default connection 
in the pool thread. The old delegate-based behaviour had the same limit for 
`list()`, but the new sentence "Every operation on `Book` in the closure uses 
the alternate connection" is broader, and worth a "on the calling thread" so it 
stays accurate.
   
   ### Nit: comment wording
   
   References:
   
   - 
`grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:2001`
   
   "// One GORM owns can be replaced after a restore; see start()." is missing 
a word; "One that GORM owns can be replaced ..." reads as intended.
   
   ### Nit: three fixes, one issue
   
   The PR fixes #16366 and, along the way, a Neo4j datastore that could not 
start with any named connection and a Neo4j static API that wrote through the 
wrong connection. Both are real bugs that stand on their own and neither has an 
issue; they are only findable from this PR's commit messages. Worth a tracking 
issue each, or at least the numbers in the release notes, so someone bitten by 
"named Neo4j connection fails at startup with `The event loop thread may not be 
smaller than 1`" can find the fix. Not a reason to split the PR: the commits 
are already separated cleanly.
   
   ### Confirmed
   
   - `stop()` closes every owned client (default, configured, added at runtime) 
and `start()` replaces exactly those flagged `clientStopped`; a connection 
added while stopped is left alone (`clientStopped` false), which matches 
`resumeIndexBuild`'s existing handling of the same case. With a supplied 
default client and named connections, the supplied one is neither closed nor 
replaced and the datastore still goes non-running so Spring calls `start()`: 
the `0 * supplied.close()` test covers it.
   - `MongoConnectionSourceFactory.create(name, settings)` copies the builder 
(`MongoClientSettings.builder(builder.build())`) and re-applies the URL and 
every `MongoClientSettingsBuilderCustomizer`, so reusing a child's `settings` 
for its replacement is safe and loses nothing. The default connection cannot be 
rebuilt from its settings on the supplied-client and `Builder` constructor 
paths because `createDefaultConnectionSources` nulls the URL, which is why 
`defaultClientOptions` has to be kept; the replacement is built through the 
same static `createMongoClient` as the original, so the two paths agree.
   - `SingletonConnectionSources` wraps the parent's `ConnectionSource` 
instance, so `replaceSource` on a child's default connection source is visible 
through the parent's `connectionSources`; the lifecycle spec asserts 
`getConnectionSource(name).source.is(client)` for each.
   - `close()` after a restore: `MongoConnectionSource.close()` closes the 
replacement, then the `inUse` loop closes it again. `MongoClientImpl.close()` 
is guarded by an `AtomicBoolean` in driver 5.x, so the double close is a no-op. 
With a factory that returns a plain `DefaultConnectionSource`, the loop is what 
closes the replacements, and the spec for that case passes.
   - `DatastoreMongoClientDatabaseFactory`: in Spring Data MongoDB 5.1.1, 
`SimpleMongoClientDatabaseFactory.doGetMongoDatabase`, `getSession` and 
`destroy` all call `getMongoCluster()`, and `MongoDatabaseFactorySupport` reads 
its own field only inside `getMongoCluster()`, so the override really does 
cover every use. `mongoInstanceCreated` is `false` on the constructor used, so 
`destroy()` will not close GORM's client.
   - `resolveTenantIds()` now reads through `getMongoClient()`, which is the 
volatile field `start()` replaces. With the factory-created default connection 
source now a `MongoConnectionSource`, the old 
`defaultConnectionSource.getSource()` would also have worked, but the datastore 
field is right for the custom-factory case too.
   - `Neo4jDriverConfigBuilder`: `Config.defaultConfig()` is the static 
`EMPTY`, `ConfigBuilder.eventLoopThreads` starts at `0`, and 
`withEventLoopThreads(0)` throws "The event loop thread may not be smaller than 
1". `ConfigurationBuilder` skips a `null` fallback, so returning `null` for a 
value equal to the driver default is the right signal. A value the default 
connection sets *to* the driver default is not inherited, but the named 
connection gets the same default from the builder, so nothing observable 
changes. `withMaxConnectionPoolSize(0)` would be the other sentinel and is 
never hit (default 100).
   - `Neo4jGormApiFactory` now builds `Neo4jGormStaticApi` with the same 
six-argument constructor `MongoGormApiFactory` uses for `MongoStaticApi`; the 
new constructor only delegates to `GormStaticApi`, and nothing in 
`Neo4jGormStaticApi` cached the datastore the old constructor received 
(`datastore` is read through the base resolver at each use).
   - `withConnectionScope` nests, restores the previous entry, removes the 
thread-local when the map empties, and is undone on exception; 
`MultipleDataSourceSpec` covers nesting and the throw. `GormEntity`'s 
`currentGormStaticApi()`/`currentGormInstanceApi()` and 
`DetachedCriteria.withPopulatedQuery` all go through `resolveStaticApi(cls, 
null)` / `resolveInstanceApi(cls, null)`, so `Book.list()`, dynamic finders, 
`where` and `book.save()` follow the scope, as tested against MongoDB with 
`ScopedCompany` and Neo4j with `RoutedCompany` on two embedded servers.
   - Docs: the CRaC section's claim that an embedded MongoDB is stopped after 
the clients and started before them holds (`MongoDatastore.LIFECYCLE_PHASE = 
-1000` above `EmbeddedMongoLifecycle.PHASE`). The Neo4j upgrading note for the 
`withConnection` change is the right place given the old guide documented the 
opposite; MongoDB's guide already promised the new behaviour, so a 
release-notes line is enough there.
   - No user-facing doc references `MongoConnectionSource`, 
`DatastoreMongoClientDatabaseFactory` or `withConnectionScope`.
   
   ## Verification
   
   - `./gradlew :grails-datamapping-core-test:test --tests 
grails.gorm.tests.MultipleDataSourceSpec :grails-data-neo4j-core:test --tests 
grails.gorm.tests.NamedConnectionsSpec --tests 
org.grails.datastore.gorm.neo4j.config.Neo4jConnectionSourceSettingsSpec`: 
BUILD SUCCESSFUL; 4 + 2 + 2 tests, 0 failures.
   - `./gradlew :grails-data-mongodb-core:test --tests <the seven specs above> 
:grails-data-mongodb-spring-data:test`: BUILD SUCCESSFUL; 34 + 17 tests, 0 
failures, Docker available.
   - `./gradlew :grails-datamapping-core:codeStyle 
:grails-data-mongodb-core:codeStyle :grails-data-mongodb-spring-data:codeStyle 
:grails-data-neo4j-core:codeStyle`: no violations.
   - A throwaway `ScopeVsTenantProbeSpec` in `grails-datamapping-core-test` 
(DATABASE-mode `SimpleMapDatastore`, three connections, 
`SystemPropertyTenantResolver`) produced the numbers quoted in the P2 and P3, 
then was deleted; the worktree is clean.
   - Spring Data MongoDB 5.1.1 and Neo4j driver 4.4.13 sources from the Gradle 
cache for the factory and `Config` claims.
   


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