matrei commented on PR #16208:
URL: https://github.com/apache/grails-core/pull/16208#issuecomment-5680524291
## AI Review
**Head:** `ee362fa1f8`
(`codeconsole:feature/mongodb-skip-index-build-8.0.x`) · **Base:** `8.0.x` at
`3067d0a855` · merge-base is clean, the branch is `8.0.x` plus the feature
commits and merges from `8.0.x`.
**Run locally (Docker, MongoDB via Testcontainers):**
- `./gradlew :grails-data-mongodb-core:cleanTest
:grails-data-mongodb-core:test --no-build-cache` for the nine new specs plus
`MongoConnectionSourceSettingsSpec`, `MongoStaticApiMultiTenancySpec`,
`TtlIndexSpec`, `TextIndexViaAttributesSpec`, `IndexWithInheritanceSpec`,
`IndexAttributesAndCompoundKeySpec`: 38 tests, 0 failures, result XML freshly
written.
- `./gradlew :grails-data-mongodb-core:cleanTest
:grails-data-mongodb-core:check --no-build-cache --continue` (whole module):
698 tests, 0 failures, 45 skipped, across 214 result files; `checkstyleMain`
and `codenarcMain` clean (the `*Test` variants are SKIPPED by the build config;
PMD and SpotBugs are not wired into this module's `check`). BUILD SUCCESSFUL.
- `./gradlew :grails-data-mongodb-docs:asciidoctor -x aggregateGroovydoc`:
BUILD SUCCESSFUL. Asciidoctor prints `possible invalid reference: queryIndexes`
for the new `<<queryIndexes,…>>` xref, but it prints the same INFO for the
pre-existing `advancedConfig`, `idGeneration` and `textSearch` xrefs; the
anchors are defined in `index.adoc` ahead of the includes, so the link resolves.
Overall this is a solid PR: the three features are cleanly separated, the
reporting is genuinely useful, and the test coverage of the failure and
degradation paths is unusually thorough. The findings below are mostly about
the multi-connection case, which the async design does not quite finish, and
about one behaviour change the description undersells.
### Findings
**1. Child datastores' index-build executors are never shut down (medium)**
`close()` (`MongoDatastore.java:1454`) calls `shutdownNow()` on *this*
datastore's `indexBuildExecutor` only. With more than one connection
configured, every child datastore in `datastoresByConnectionSource` is a full
`MongoDatastore` with its own executor and thread
(`gorm-mongo-index-build-<connection>`), and nothing ever closes the children —
`close()` does not iterate them, and `AbstractDatastore.destroy()` does not
know about them.
Two consequences when `buildIndexesAsync = true` and a non-default
connection is configured:
- If the application shuts down while a child's build is still in flight,
`connectionSources.close()` closes that child's client underneath it. The
child's executor is *not* shut down, so the catch block at
`MongoDatastore.java:614` takes the `else` branch and logs `The background
index build failed: ...` at **ERROR** — exactly the noise
`BuildIndexesBackgroundFailureSpec` guarantees does not happen, but only for
the default connection.
- After a child's build completes, its single-thread executor keeps its core
thread alive forever (see 3), so each non-default connection leaks one idle
daemon thread until the JVM exits.
Suggested fix: in `close()`, before `connectionSources.close()`, shut down
the executor of every value in `datastoresByConnectionSource` other than `this`
(a small `shutDownIndexBuild()` helper the children can share).
`BuildIndexesPerConnectionSpec` only exercises `buildIndexes`; a feature that
enables `buildIndexesAsync` with a second connection and closes the datastore
mid-build would pin this.
**2. The supplied-`MongoClient` fix also switches on multi-tenancy — say so
(medium, docs/release notes)**
`createDefaultConnectionSources` (`MongoDatastore.java:1526`) now binds the
settings from configuration instead of `new MongoConnectionSourceSettings()`.
The description lists `stateless`, `transactional`, `engine`, flush mode and
`decimalType` as the settings that were silently ignored on that path. The
bigger one is `multiTenancy`: with the bare settings, `multiTenancy.mode` was
`NONE`, and that value is what both `MongoDatastore.multiTenancyMode`
(`MongoDatastore.java:213`) and `GormStaticApi.resolveMultiTenancyMode`
(`grails-datamapping-core/.../GormStaticApi.groovy:108`, reading
`defaultConnectionSource.settings.multiTenancy.mode`) are driven by. So an
application on the Spring Boot auto-configuration path with a `MongoClient`
bean and a configured tenancy mode was running with tenant discrimination
effectively off at the datastore and static-API level; after this PR it is on.
That is the right outcome, but it is a security-relevant behaviour change,
not just a "`stateless = true` now takes effect" one. Please:
- name it in the release note bullet and in the PR description, and
- add a feature to `SuppliedMongoClientSettingsSpec` asserting
`datastore.multiTenancyMode` follows the configuration, so the fix cannot
regress silently the way the original bug did.
**3. The async executor's thread lives for the life of the application
(low)**
`Executors.newSingleThreadExecutor` (`MongoDatastore.java:223`) never times
out its core thread, so once the startup build is done
`gorm-mongo-index-build-DEFAULT` sits idle until `close()`. Daemon, so harmless
for JVM exit, but a permanent thread per connection for a task that runs once
at startup. A `ThreadPoolExecutor(0, 1, …)` with `allowCoreThreadTimeOut(true)`
and a short keep-alive, or a plain daemon `Thread` created per `buildIndex()`
call and remembered for interruption in `close()`, would release it. This also
shrinks the surface of finding 1.
**4. The protected `initializeIndices(PersistentEntity)` hook is bypassed by
the startup build (low, API)**
Before this PR `buildIndex()` called the protected
`initializeIndices(entity)`; a subclass overriding it customised index creation
everywhere. Now `buildDeclaredIndexes()` (`MongoDatastore.java:642`) calls the
private `initializeIndices(entity, summary)` directly, so an override is
honoured only on `persistentEntityAdded`. Nothing in-tree overrides it, so this
is a note rather than a blocker: either make the two-argument overload the
protected one (with `IndexBuildSummary` package-visible), or accept the
narrowing and say so in the Javadoc of the one-argument method.
**5. The created / already-present split double-counts a key pattern
declared twice on one entity (low, reporting only)**
`ExistingIndexes` is a snapshot taken before the entity's first
`createIndex` and is never updated. `name index: true` together with
`compoundIndex name: 1` both produce `{name: 1}`; the second call is idempotent
on the server but is counted as *created* (`MongoDatastore.java:1211`). The
same staleness means that after a `recreateOnConflict` drop-and-recreate, a
later declaration on the same keys would look up the old index name. Recording
each key pattern this build created (or recreated) into the snapshot after a
successful `createIndex` fixes both, and costs nothing.
**6. Documentation nits (low)**
- `queryIndexes.adoc`: "on a single daemon thread per connection … so
enabling this does not launch several index builds against the server at once"
is true per connection only. With three connections against one server there
are three concurrent builders. Worth one clause.
- `advancedConfig.adoc`: the NOTE that `grails.mongodb.*` keys are not
relaxed-bound (`database-name` is ignored) is a claim no test in this PR backs.
I believe it is right for the `PropertyResolver` path the builder uses, but it
is unrelated to the feature and would be better either backed by a tiny feature
in `MongoConnectionSourceSettingsSpec` or dropped from this PR.
- The summary's "from N domain class(es)" counts every entity mapped to the
connection, including those declaring no index at all (`summary.entities++`
runs before `initializeIndices`). The doc wording "how many domain classes it
covered" is consistent with that, so fine, but "2 created, 0 already present,
from 40 domain class(es)" may surprise a reader who expects 40 to be the number
with indexes.
### Verified as correct
- **Settings.** `buildIndexes` defaults to `true`, `buildIndexesAsync` to
`false`; both bind through `MongoConnectionSourceSettingsBuilder` and resolve
per connection (`BuildIndexesPerConnectionSpec` covers global-off /
connection-on and inheritance). Constants and Javadoc on `MongoSettings` and
`AbstractMongoConnectionSourceSettings` are consistent with the docs.
- **`buildIndexes = false`.** No `createIndex`/`collMod` is issued:
`buildIndex()` returns early (`MongoDatastore.java:594`) and
`initializeIndices` guards `persistentEntityAdded` too
(`MongoDatastore.java:1111`). Queries and writes are unaffected. Verified
against the container with `BuildIndexesDisabledSpec` and its control
`BuildIndexesEnabledByDefaultSpec`.
- **Async path, default connection.** One single-thread executor named after
the connection, daemon thread, created only when both flags are on. `close()`
calls `shutdownNow()` *before* `super.destroy()` and the client close, so an
interrupted build is classified as shutdown (DEBUG) rather than failure
(ERROR); an uninterrupted failure is logged at ERROR. Both branches are
exercised by `BuildIndexesBackgroundFailureSpec` (the proxy-based
`FailingMongoClient` is a neat way to get there without mocking the driver).
`CommandListener` runs on the calling thread in the sync driver, so
`BuildIndexesAsyncSpec`'s thread assertions are sound.
- **Reporting.** Lazy `listIndexes` per entity, reused by the conflict path
(which used to list on its own); on an unreadable list `classified` flips and
the summary falls back to "N index declaration(s) applied", while the conflict
that cannot be reconciled blind is still logged at ERROR and counted as failed
(`BuildIndexesUnreadableIndexListSpec`). `reconcileIndexConflict` now returns
whether the index ended in the declared state and the counts follow it. Summary
at INFO on success, WARN with a failure count otherwise
(`BuildIndexesFailureSummarySpec`, `BuildIndexesSummaryLogSpec`). Per-index
DEBUG line goes through the `AbstractDatastore` logger, so the documented
`org.grails.datastore.mapping.core: DEBUG` does enable it.
- **Supplied client.** `databaseName` is still overridden from the mapping
context after binding; the `MongoClientSettings.Builder`/credentials the
builder may populate are unused on this path, as the description says.
`SuppliedMongoClientSettingsSpec` proves the bound settings take effect, not
merely report.
- **Test logging setup.** `logback-classic` replaces `slf4j-nop` on the test
classpath only (`compileOnly slf4j-nop` for main is untouched);
`logback-test.xml` pins root to WARN and every log-asserting spec raises and
then restores the level of the one logger it touches and detaches its appender.
No other module consumes `grails-data-mongodb-core`'s test output, so the
config file does not leak. Specs filter captured events by database name, so
sharing one JVM fork with other specs is safe.
- **Docs.** Release notes are under the `8.0` heading; `queryIndexes.adoc`
and `advancedConfig.adoc` cross-reference each other and show both YAML and
Groovy forms.
- **CI.** Codecov reports 96 % patch coverage; the single TestLens failure
is the known-flaky scaffolding `UserControllerSpec > User list` (#16030),
unrelated to this change.
--
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]