matrei commented on PR #16369:
URL: https://github.com/apache/grails-core/pull/16369#issuecomment-5781700512
## Review Findings, round 3
Head `c82dd69b25`, base `8.0.x` at `1fadd5c812`; the branch still merges
clean against the current `8.0.x`. This round covers the five commits since
`190ac9c388`: `077f33c2c9` (configuration metadata), `12497cfb51`
(`recreateOnConflict` for a taken index name), `fdd67c9244` (the client the
Boot auto-configuration builds), `07f68726a4` (`withNewSession(tenantId)` binds
the tenant) and `c82dd69b25` (a class a block does not name keeps its own
connection). They were pushed in the twenty minutes after my round-2 comment,
so that round's two P3s and two nits are unaddressed and still stand; I have
not repeated them here beyond one line each at the end.
What I ran on the head, all green (build cache off, `cleanTest` first,
result XML timestamps from this run):
- `:grails-datamapping-core-test:test` and `:grails-datamapping-core:test`,
both whole modules: 624 + 1 082 tests, 0 failures.
- `:grails-data-hibernate5-core:test` and
`:grails-data-hibernate7-core:test` for `MultipleDataSourceConnectionsSpec` and
the refresh-lock specs: 110 + 109 tests, 0 failures.
- `:grails-data-mongodb-core:test` for `MultipleConnectionsSpec`, every
`*Index*` spec, every `*Tenan*` spec, `MongoConfigurationMetadataSpec`,
`CheckpointRestoreConnectionsSpec`, `MongoDatastoreLifecycleSpec` and
`MongoDatastoreExternalClientSpec`: 39 specs, 120 tests, 0 failures, against a
real container.
- `:grails-data-mongodb-spring-boot:test`, the whole module: 4 tests, 0
failures.
- `codeStyle` on `grails-datamapping-core`, `grails-data-mongodb-core` and
`grails-data-mongodb-spring-boot`: no violations.
- A throwaway Hibernate 7 spec, deleted afterwards, run on both `c82dd69b25`
and the base, for the bystander case under Confirmed.
And one thing that is not green, which CI found first.
### [P1] `@Rollback("moreBooks")` and `@Transactional("moreBooks")` now
switch every multi-tenant class to that tenant, and the schema-per-tenant
examples fail
References:
- CI: `Hibernate7 Functional Tests`, shards 1 and 2 on Java 21 and 25,
`:grails-test-examples-hibernate7-grails-schema-per-tenant:test` FAILED; the
Hibernate 5 shards are still pending and will fail the same way
-
`grails-test-examples/hibernate7/grails-schema-per-tenant/src/test/groovy/schemapertenant/SchemaPerTenantSpec.groovy:58-66`
and the identical Hibernate 5 spec
-
`grails-datamapping-core/src/main/groovy/grails/gorm/transactions/GrailsTransactionTemplate.groovy:96-100`
-
`grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormRegistry.groovy:583-590`
```
SchemaPerTenantSpec > Test should rollback changes in a previous test FAILED
Expected exception of type
'org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException',
but no exception was thrown
at
schemapertenant.SchemaPerTenantSpec.$tt__$spock_feature_1_0(SchemaPerTenantSpec.groovy:66)
...
at
grails.gorm.transactions.GrailsTransactionTemplate.doExecuteAndRollback(GrailsTransactionTemplate.groovy:105)
at
org.grails.datastore.gorm.GormRegistry.runInScope(GormRegistry.groovy:546)
at
org.grails.datastore.gorm.GormRegistry.withConnectionScope(GormRegistry.groovy:531)
at
grails.gorm.transactions.GrailsTransactionTemplate.inConnectionScope(GrailsTransactionTemplate.groovy:96)
at
grails.gorm.transactions.GrailsTransactionTemplate.executeAndRollback(GrailsTransactionTemplate.groovy:100)
```
The cause is `190ac9c388`, not this push: both example modules pass on
`9e0312eb15` and fail from `190ac9c388` on, 1 of 2 in each, reproduced locally
on Hibernate 5 and 7. The test annotates a method `@Rollback("moreBooks")`,
where `moreBooks` is a schema tenant added with `addTenantForSchema`, and
asserts that `Book.count()` inside it, with no tenant set, throws
`TenantNotFoundException`. In SCHEMA and DATABASE mode a tenant is a child
datastore registered as a connection, and `allQualifiers` maps every
multi-tenant entity to every connection, so the transform's connection reaches
`GrailsTransactionTemplate`, `runInScope(null, 'moreBooks')` is pushed,
`isMappedToConnection(Book, 'moreBooks')` is true, and `Book.count()` is routed
to the tenant. Nothing throws, and the test's second half, which then sets the
tenant and expects one book, is never reached.
This is a contract change for every existing multi-tenant application, and
it is not in the commit, the description, or any doc. Until `190ac9c388`, the
`connection` of `@Transactional` and `@Rollback` chose which datastore's
transaction manager opens the transaction, and the tenant resolver still
decided which tenant the calls inside reached, throwing when it found none. Now
the annotation is also a tenant switch, and the safety net inside such a method
is gone: `@Transactional("moreBooks")` around code that was written to run for
the current tenant reads and writes `moreBooks` regardless of who is logged in.
The class-level `inConnectionScope` behaves the same way for
`Book.moreBooks.withTransaction { }`, and the guide documents that ("the
block's connection takes precedence over the current tenant"), but that is a
block that names the tenant on the class; the annotation was never documented
as one.
Two ways out:
- Keep tenants out of the connection-wide scope: in `isMappedToConnection`,
a multi-tenant entity whose datastore is in DATABASE or SCHEMA mode is not
"mapped" to a qualifier that is one of its tenants, so the annotation keeps
choosing the transaction manager only, as before, and the tenant resolver keeps
deciding. The example tests then pass unchanged, and nothing in existing
applications moves. This is the one I would take: it is what the annotation has
always meant, and the `@Transactional(connection)` change was described as
covering the classes mapped to a *datasource*.
- Or keep the new behaviour, in which case the two example specs need
changing to whatever the new contract is, the upgrade guide's section 72 and
the multi-tenancy pages need a paragraph saying that `@Transactional` and
`@Rollback` naming a SCHEMA or DATABASE tenant now route every multi-tenant
class to it and no longer throw `TenantNotFoundException` inside, and
`ConnectionScopeMultiTenancySpec` needs the case. I would not do this on a
maintenance branch.
Either way, the description's "7,490 tests on the seven multi-datasource
example apps" did not include the two schema-per-tenant examples;
`grails-test-examples/hibernate5` and `hibernate7` have more multi-tenant apps
than those seven, and the Hibernate functional CI jobs are the run that covers
them. For what it is worth, round 2 saw this mechanism ("a tenant child
datastore's `TransactionService` pushes the tenant's connection ... so it
routes", under Confirmed) and should have called the contract change out then;
I did not, and the CI run did.
### Nit: `withNewSession(tenantId)` now opens two sessions in DATABASE mode
References:
-
`grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApi.groovy:1048-1061`
-
`grails-datamapping-core/src/main/groovy/grails/gorm/multitenancy/Tenants.groovy:302-372`
The fix binds the tenant through `Tenants.withId(datastore, tenantId,
inNewSession)`. In a shared-connection mode that only binds and calls the
closure, which is what is wanted. In DATABASE mode, with no session bound for
the tenant, `Tenants.withId` runs `datastore.withNewSession(tenantId)`, which
opens a session on the tenant's child datastore, and *then* `inNewSession`
opens the second one through `executeWithNewSession(tenantDatastore)`, which is
the session the closure receives; the outer one is never used.
`Tenants.withTenant(persistentClass, tenantId) { }` (or
`CurrentTenantHolder.withTenant(defaultDatastore, tenantId) { }`, which is what
it wraps) is the bind-only entry point that exists for exactly this, and gives
the commit what its message says, "with the tenant bound", without the extra
session. The new test runs on the in-memory datastore, where the extra session
costs nothing, so it does not show.
### Nit: two docs still say the default host is `localhost`
References:
-
`grails-data-mongodb/docs/src/docs/asciidoc/introduction/upgradeNotes.adoc:111`
("`localhost:27017` unless those are set")
- `grails-data-mongodb/docs/src/docs/asciidoc/testing.adoc:25` ("by default,
`localhost` and port `27017`")
`077f33c2c9` corrects the metadata to `127.0.0.1`, which is what
`ServerAddress.defaultHost()` returns and what `MongoConfigurationMetadataSpec`
now pins. These two sentences state the old value; the first was written in
this PR's own upgrade note.
### Confirmed
- `077f33c2c9`: `AbstractMongoConnectionSourceSettings` has `host =
ServerAddress.defaultHost()`, `port = ServerAddress.defaultPort()` and
`decimalType = true`, so the three metadata corrections are right, and the spec
compares the published defaults with a `new MongoConnectionSourceSettings()`
for all six settings and the url, so the next drift fails.
- `12497cfb51`: error code 86 is `IndexKeySpecsConflict`;
`findIndexByKeyPattern` returns nothing for it, so the name lookup finds the
index that holds the name, the TTL shortcut is skipped for it (it is a
different index), `recreateOnConflict` drops it by name and creates the
declared one, and without the flag the new message names the taken name and the
build counts one failure. Both cases are tested against a real server with an
index pre-created on other keys.
- `fdd67c9244`: the new constructor builds the first client from the
supplier with `closeable = true`, keeps the supplier, and
`createReplacementDefaultClient` prefers it over the configuration and the
options builder; the auto-configuration passes `{
MongoClients.create(mongoOptions) }` where it used to create the client itself,
and the `DisposableBean` that closed that client is gone because
`MongoDatastore.close()` now owns it. The rewritten `CloseSpec` builds a real
context with a 50 ms server-selection timeout and no server, and distinguishes
closed from open by the driver's `IllegalStateException` versus
`MongoTimeoutException`, so the four boot-plugin tests need no MongoDB. The
limitation the description listed for this case is now removed from the code
but is still listed under "Limitations" in the PR description; that line should
go.
- `07f68726a4`: the new test in `ConnectionScopeMultiTenancySpec` reads
tenant `bar` through `withNewSession('bar')` while `foo` is current, which
failed before. `withId` two methods up has used
`Tenants.withId(defaultDatastore, ...)` in the same way for years, so a
datastore in mode NONE handles the call as it always has (Hibernate falls back
to a plain new session, MongoDB to itself).
- `c82dd69b25`: the guard skips the bound-session selector only while a
scope is running, and only that selector; the qualified and default selectors
are untouched. It does not change DATABASE- or SCHEMA-mode tenant routing,
because `shouldSkipActiveDatastore` already ignores a bound tenant session
unless it matches the resolved tenant. On Hibernate it changes nothing either:
with `Author` mapped `ALL`, `Book.books.withTransaction { new Author(name:
n).save(flush: true) }` gives `No Session found for current thread` standalone
and `TransactionRequiredException` under an outer default session on both
`c82dd69b25` and the base, so the change is MongoDB's and Neo4j's, where a
class declared for the block's connection followed the block's session; the new
`BystanderCompany` test covers it. One consequence worth knowing: a session
opened on a datastore *itself* inside a block,
`datastore.getDatastoreForConnection('test2').withSession { Bystander.count()
}` nested in `CompanyA.withConne
ction('test2') { }`, no longer routes `Bystander` to `test2` either.
- All five modules' `codeStyle` and the docs build were not run here beyond
the three `codeStyle` tasks above; the RxGORM note is right,
`grails-datamapping-rx` is commented out of `settings.gradle`.
### Still open from round 2
- [P3] `TransactionService` on the default datastore undoes an enclosing
block while `@Transactional` without a connection does not
(`DefaultTransactionService.newTemplate` passing `DEFAULT`).
- [P3] "mapped to the connection" means "declared in the mapping", which the
MongoDB and Neo4j pages should say, and no MongoDB or Neo4j test covers the
annotation.
- Nit: the `default`-scope assertion in `ConnectionScopeMultiTenancySpec:66`
still never pushes a scope.
- Nit: the "left alone" branch of `inConnectionScope`
(`GormStaticApi:741-742`) still has no test.
## Verification
- `./gradlew --no-build-cache :grails-datamapping-core-test:cleanTest
:grails-datamapping-core-test:test :grails-datamapping-core:cleanTest
:grails-datamapping-core:test :grails-data-hibernate5-core:cleanTest
:grails-data-hibernate5-core:test --tests MultipleDataSourceConnectionsSpec
--tests Hibernate5RefreshLockSpec :grails-data-hibernate7-core:cleanTest
:grails-data-hibernate7-core:test --tests MultipleDataSourceConnectionsSpec
--tests Hibernate7RefreshLockSpec :grails-data-mongodb-core:cleanTest
:grails-data-mongodb-core:test --tests MultipleConnectionsSpec --tests
'*Index*' --tests MongoConfigurationMetadataSpec --tests
CheckpointRestoreConnectionsSpec --tests MongoDatastoreLifecycleSpec --tests
MongoDatastoreExternalClientSpec --tests SchemaBasedMultiTenancySpec --tests
'*Tenan*' :grails-data-mongodb-spring-boot:cleanTest
:grails-data-mongodb-spring-boot:test :grails-datamapping-core:codeStyle
:grails-data-mongodb-core:codeStyle :grails-data-mongodb-spring-boot:codeStyle`
on
`c82dd69b25`: BUILD SUCCESSFUL; 624 + 1 082 + 110 + 109 + 120 + 4 tests, 0
failures, 0 errors; result XML written 20:04-20:08 today.
- `./gradlew --no-build-cache --continue
:grails-test-examples-hibernate7-grails-schema-per-tenant:cleanTest
:grails-test-examples-hibernate7-grails-schema-per-tenant:test
:grails-test-examples-hibernate5-grails-schema-per-tenant:cleanTest
:grails-test-examples-hibernate5-grails-schema-per-tenant:test`: on
`c82dd69b25`, BUILD FAILED, `SchemaPerTenantSpec` 2 tests, 1 failure in each
module; on `9e0312eb15`, BUILD SUCCESSFUL, 2 tests, 0 failures in each.
- CI job `106862262476` log (`Hibernate7 Functional Tests (Java 21,
indy=false, shard 1)`): `> Task
:grails-test-examples-hibernate7-grails-schema-per-tenant:test FAILED`, the
stack trace quoted above, `2 tests completed, 1 failed`; the other three failed
shards carry the same annotation. TestLens's "All tests passed but jobs failed"
on this head is wrong about that job.
- Throwaway `BystanderProbeSpec` in `grails-data-hibernate7/core` (its own
`HibernateDatastore` over `Book` and `Author` from
`MultipleDataSourceConnectionsSpec`, H2), run on `c82dd69b25` and `1fadd5c812`,
printed identical results on both; deleted afterwards, and the worktree is back
on the original branch with no tracked changes.
--
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]