GORM: Shared Mapping Registry O(M+N) Scaling (clean rebuild)#15678
GORM: Shared Mapping Registry O(M+N) Scaling (clean rebuild)#15678borinquenkid wants to merge 43 commits into
Conversation
Introduces shared-registry architecture to eliminate per-tenant API wrapper duplication in multi-tenant environments with high entity/tenant cardinality. Core changes: - GormRegistry: normalization caches (entity keys, qualifiers), O(1) lookup paths - GormApiResolver: simplified fallback chains, qualified API caching - AbstractGormApiRegistry/sub-registries: normalized key/qualifier registration - GormEnhancer: delegates API resolution through GormRegistry Datastore integrations: - Hibernate 7 and Hibernate 5: aligned to shared registry model - MongoDB, Neo4j, SimpleMap, GraphQL: registry-pattern integration Adjacent migrations: - AsyncEntity: GormEnhancer.findStaticApi -> GormRegistry.instance.findStaticApi - ByDatasourceDomainClassFetcher: GormEnhancer.findDatastore -> GormRegistry apiResolver - TCK: added transaction-capable datastore support in GrailsDataTckManager This commit excludes all collateral CodeNarc reformat changes (2,835 files from commit 4add87e) and agent experiments, containing only the optimization-specific module changes. Agent collaboration note: Claude Sonnet 4.6 assisted with branch archaeology and rebuild strategy; borinquenkid is the primary author and remains responsible for the final changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Includes SessionResolver and ThreadLocalSessionResolver (new interfaces/classes introduced by the O(M+N) scaling refactor), plus updates to AbstractDatastore, AbstractMappingContext, and related core classes that the datastore modules (SimpleMap, Hibernate 5/7) depend on at compile time. Missed from initial clean rebuild commit. Agent collaboration note: Claude Sonnet 4.6 assisted; borinquenkid is the primary author and remains responsible for the final changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
During child datastore construction, GormRegistry.registerEntityDatastores calls getDatastoreForConnection() before the parent's datastoresByConnectionSource map is populated, throwing ConfigurationException for multi-datasource setups. H5 anonymous child: add self-reference check so the child returns itself when asked for its own connection name, rather than delegating to the parent map. H7 ChildHibernateDatastore: use PARENT_HOLDER ThreadLocal to pass the parent reference through the super() call before the parent field is assigned; also pass the parent's datastoresByConnectionSource map to HibernateGormEnhancer so it can resolve sibling datastores during initialize(). Fixes DataSource not found for name [secondary/schemaA] ConfigurationException in multi-datasource and schema-per-tenant multi-tenancy test suites. Agent collaboration note: Claude Sonnet 4.6 assisted; borinquenkid is the primary author and remains responsible for the final changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The O(M+N) GormRegistry refactor exposed two classes of regression in multi-tenancy and multi-datasource scenarios. This commit addresses both. Production fixes: GormApiResolver: Move the DISCRIMINATOR mode check before the MultipleConnectionSourceCapableDatastore delegation so that tenant IDs are never mistaken for datasource connection names. For the DEFAULT qualifier, return the preferred (active-transaction) datastore directly rather than re-routing through getDatastoreForConnection, which would return the parent and mismatch the session factory already bound to the transaction. GormRegistry.registerEntityDatastores: Stop overwriting child datastores with the parent for non-DEFAULT qualifiers that resolve back to the parent. In SCHEMA and DISCRIMINATOR mode the qualifier is a runtime tenant ID, not a datasource name; routing it back to the parent is correct and must not clobber the child entries added by addTenantForSchemaInternal. GormRegistry.findTransactionManager: Fall back through the full apiResolver when getDatastore returns null so that DISCRIMINATOR/SCHEMA tenant IDs still resolve to a transaction manager. HibernateDatastore (H5) / ChildHibernateDatastore (H7): Return null instead of throwing ConfigurationException when getDatastoreForConnection is called for a sibling that is not yet registered during initialization. GormRegistry will re-register all entities with the correct datastores once initialization completes. Child datastores also delegate to the parent for unrecognized connection names so the lookup chain stays consistent. HibernateGormInstanceApi (H7): Always resolve the template via the datastore registry rather than caching a DEFAULT-qualifier instance, so that preferred-datastore switching in multi-datasource transactions picks up the correct session factory. GrailsHibernateTransactionManager (H7): Remove debug System.err.println statements left over from investigation. Test infrastructure fixes: gradle/hibernate5-test-config.gradle, gradle/hibernate7-test-config.gradle: Set forkEvery = 1 so each test class runs in its own JVM. The root test-config.gradle uses forkEvery = 50 (CI) / 100 (local) for speed; with a shared GormRegistry singleton that per-test setup/teardown mutates, TCK specs running before PartitionedMultiTenancySpec in the same JVM were clearing datastoresByQualifier["default"], causing a NullPointerException in count() when PartitionedMultiTenancySpec later resolved a GormPersistentEntity. forkEvery = 1 eliminates cross-class singleton contamination at the cost of extra JVM startup overhead, which is acceptable given the test isolation requirement. GrailsDataHibernate5TckManager: Add grailsConfig field and populate a local ConfigObject from it in createSession(), fixing MissingPropertyException when test specs assign grailsConfig before calling setup(). Verified: H5 669 tests / 0 failures, H7 2960 tests / 0 failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Local issue-tracking files have no Apache license header and are not source artifacts. Add **/ISSUES.md to the RAT exclusion list alongside the existing local-tasks.gradle exclusion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…saction routing
- GormEnhancer: Remove generic type parameters (`<D>`) from deprecated API lookup methods (`findStaticApi`, `findInstanceApi`, `findValidationApi`). This fixes a `MissingMethodException` encountered
when older compiled code or dynamic Groovy proxies call these methods without exact signature matches.
- TransactionalTransform: Fix `IllegalStateException: No GORM implementations configured` in multi-tenant environments. Revert logic that incorrectly passed `tenantId` as a connection source
qualifier to `GormRegistry`, and instead fetch the tenant-specific datastore using `getTargetDatastore().getDatastoreForTenantId(tenantId)`.
- ServiceTransformation: Ensure the stateful `$datastore` field is properly generated for generic data services to resolve injection failures in tests.
afa32a1 to
611e6bd
Compare
… artifacts
Fixes TCK DataServiceConnectionRoutingSpec failures introduced during the O(M+N)
scaling refactor. The decentralized API resolver changes caused getTargetDatastore(String)
to ignore the explicitly-injected $targetDatastore and route through the API resolver
instead, which could return a child datastore that has no knowledge of sibling connections.
- AbstractDatastoreMethodDecoratingTransformation: getTargetDatastore(String) now checks
$targetDatastore before falling back to the API resolver
- ServiceTransformation: generateConnectionAwareTransactionManager uses getTargetDatastore()
instead of getDatastore() for correct multi-datasource transaction manager resolution
- GrailsDataHibernate7TckManager: fix setTargetDatastore array overload to use
MultipleConnectionSourceCapableDatastore[] instead of Datastore[]
…) + entity re-registration in setup(), wired setTargetDatastore(multiDataSourceDatastore) in getServiceForConnection(), and wired setTargetDatastore(multiTenantMultiDataSourceDatastore) in getServiceForMultiTenantConnection(). This fixes 26 H5 failures. 2. GrailsDataHibernate7TckManager.groovy — Wired setTargetDatastore(multiTenantMultiDataSourceDatastore) in getServiceForMultiTenantConnection(). This fixes 5 H7 DataServiceMultiTenantConnectionRoutingSpec failures. 3. GormApiResolver.groovy — Removed 6 residual System.out.println debug statements from ActiveSessionDatastoreSelector. 4. ServiceTransformSpec.groovy — Added GormRegistry.reset() in setup()/cleanup() to prevent cross-spec registry pollution causing 3 flaky test failures.
Root cause: AutoTimestampEventListener.beforeUpdate() unconditionally sets lastUpdated/dateCreated on every PreUpdate event, even when the entity has no user-intent changes. This caused PendingUpdate.run() to see a changed property, add it to $set, and increment the optimistic-locking version — breaking the MarkDirtyFalseSpec contract that a no-op save() must not increment version. Fix in MongoCodecEntityPersister.persistEntity(): - Before cancelUpdate fires, capture a snapshot of all property values and whether the entity already had user-intent dirty state (hasPreExistingDirty). - After cancelUpdate, compare the snapshot to detect what changed. If the only changes are lastUpdated/dateCreated (auto-timestamp properties) with no pre-existing user-intent dirty state, restore those timestamps, call trackChanges(), and veto the update. - The veto condition requires onlyAutoTimestampChanged to be non-empty — entities without auto-timestamp properties (including those with embedded-only associations) are never incorrectly vetoed, even when the snapshot comparison misses embedded-object mutations (same object reference). - Also refactored persistEntity() to remove the unnecessary isUpdate && !session.isDirty(obj) early-return guard, which was incompatible with the snapshot logic. Other changes: - GrailsDataMongoTckManager: added GormRegistry.reset() in setup() for per-test datastore isolation (prevents stale GORM state polluting successive test features). - DirtyCheckingSupport: propagate dirty-checking into PersistentCollection items so collection-element mutations are visible to the parent entity's dirty check traversal. Tests verified: MarkDirtyFalseSpec, EmbeddedAssociationSpec, EmbeddedCollectionAndInheritanceSpec, EmbeddedCollectionWithOneToOneSpec, EmbeddedUnsetSpec, LastUpdatedSpec, BeforeUpdatePropertyPersistenceSpec, DirtyCheckUpdateSpec — plus full testSelected suite: 4588 tests, 0 failures.
…eDatastore Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
c5f7c32 to
4358b70
Compare
…resolution
- currentGorm{Instance,Static,Validation}Api() throw IllegalStateException
when GORM is uninitialized, instead of returning null. Callers such as
DefaultLinkGenerator.getResourceId already catch IllegalStateException and
fall back; this restores the pre-O(M+N) contract and fixes NPEs in
VndErrorRenderingSpec, HalJsonRendererSpec, TableSpec and others.
- GormStaticApi.getGormPersistentEntity() falls back to the mapping context
captured at construction when registry resolution returns null. Entity
metadata is identical across tenants, so this stable reference fixes the
withTenant(tenantId).count() NPE for DISCRIMINATOR multi-tenancy under
cross-spec registry state (PartitionedMultiTenancySpec in the full suite).
- SimpleMapSession.isDirty() treats an identified instance absent from the
backing map as dirty so save() re-inserts it. Fixes the unit-test pattern of
saving @shared instances in setup() across feature iterations after
clearData() empties the in-memory datastore (DefaultInputRenderingSpec).
- DefaultHalViewHelper.renderEntityProperties excludes the version property from
embedded output, consistent with top-level GORM rendering. KeyValue entities
gained an auto-mapped version under the GORM mapping strategy (HalEmbeddedSpec).
- Remove stale @NotYetImplemented on UniqueConstraintOnHasOneSpec; unique-on-hasOne
now works, so the spec passes and the assertion is restored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes five of the six failing Functional Tests CI tasks. Each was a core GORM contract that regressed in the shared-GormRegistry rewrite but surfaced far downstream; root-cause fixes are guarded by core unit tests. - SimpleMapSession: a rolled-back transaction set a session-level rollbackOnly flag that was never cleared, permanently turning flush() into a no-op on the (long-lived, thread-bound) test session. Subsequent save(flush:true) calls assigned an id and populated the first-level cache but never reached the backing map. Clear the marker on commit/rollback and reset it when a transaction begins. Adds SimpleMapSessionSpec coverage. Fixes the app1 BookControllerSpec save/delete count()==0 failures. - DataTest harness: the single shared GormRegistry resolves a domain mapped to a non-default datasource to a dedicated per-connection child datastore, but the unit-test interceptors only bound a session for the default datastore. Entities on non-default datasources ran in throwaway per-call sessions, so save() without an explicit flush was lost before an auto-flushing query. Bind (and symmetrically unbind) a session for every connection source; no-op for single-datasource specs. Adds NonDefaultDatasourceFlushSpec. Fixes demo33 CarSpec. - GormStaticApi.withTransaction(Map)/withNewTransaction(Map): replaced the broken definition.setProperty(key, value) call (no such method on the Java bean DefaultTransactionDefinition) with the property-set idiom definition[key]=value, plus CharSequence coercion and a clear error for unknown properties. Fixes CrossDatasourceTransactionSpec read-only transactions. - DefaultHalViewHelper: reverted the embedded-version exclusion. Embedded HAL output renders the version property for versioned entities (functional TeamSpec depends on it); the unit-level HalEmbeddedSpec only saw an extra version:0 because KeyValue entities now auto-map a version under the GORM mapping strategy, so its expectation is updated instead. - demo33 UniqueConstraintOnHasOneSpec: removed a stale @NotYetImplemented (a second copy of an already-fixed spec) that fails as "passes unexpectedly". - Bar/FooIntegrationSpec: assert datastore persistence through public, observable behavior (Mongo ObjectId / Hibernate sequential id) instead of the removed internal org.grails.datastore.gorm.GormEnhancer.findStaticApi probe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removed unused imports and redundant code from ServiceTransformation and HibernateDatastore to fix linting violations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ging capture - Update ActiveSessionDatastoreSelector in GormApiResolver to correctly bypass the parent DEFAULT datastore when routing multi-tenant entity operations in DATABASE/SCHEMA modes, without inadvertently skipping explicitly opened child datastores. This resolves transaction auto-commit issues in DatabasePerTenantIntegrationSpec. - Adjust GraphQL CommentIntegrationSpec StringMessagePrintStream output capture to filter out SLF4J deprecation warnings (HHH90000022: Hibernate's legacy org.hibernate.Criteria API is deprecated), preventing false-positive query over-counting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- GormApiResolver: Ensure PreferredDatastoreSelector performs tenant validation for DEFAULT qualifier when the preferred datastore is multi-tenant capable, preventing static queries like count() from incorrectly querying the parent datastore in an active transaction. - GrailsHibernateTransactionManager (H5): Reapply the isExistingTransaction override from H7 to ensure transactions bound to the parent SessionFactory are not mistakenly reused for tenant-specific DataSources.
The integration tests were capturing and counting all System.out logs as queries, causing Hibernate deprecation warnings to fail the assertions. Filtered capture to explicitly require 'Hibernate:'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In Gradle 9, ProjectDependency.getDependencyProject() has been removed. Updated gradle/test-config.gradle to recursively identify any subprojects depending on ':grails-datamapping-core' (GORM) using path resolution, and force their test tasks to run with maxParallelForks = 1. This prevents singleton GormRegistry and local resource conflicts in CI while preserving parallel execution for all other modules. Co-Authored-By: Gemini <noreply@google.com>
…fix Hibernate 5 schema-per-tenant resource unbinding
- Remove Hibernate 5/7, MongoDB, and Neo4j datastores from GormRegistry upon destroy/close to prevent scalability/leak issues.
- Unbind the tenant-specific DataSource from TransactionSynchronizationManager in Hibernate 5 schema-per-tenant setup to prevent thread connection bound state conflicts.
- Restore previous connection holder correctly in GrailsHibernateTemplate regardless of session holder existence.
Co-Authored-By: Gemini <noreply@google.com>
…n exists Update Tenants.withId to check if the child datastore for the tenant already has a current session active on the thread. If so, execute the closure directly rather than forcing a new session, which preserves transaction propagation for multi-tenant integrations. Also, clean up debug print statements and use protected logger.debug statements under the log.isDebugEnabled() check.
…ling - Restore connection routing to use getDatastoreForConnection instead of getDatastoreForTenantId in AbstractDatastoreMethodDecoratingTransformation. - Align SimpleMapDatastore getDatastoreForTenantId with relational/document tenancy invariants by returning this for non-DATABASE modes. - Avoid StackOverflowError in transactional method decoration by using AttributeExpression (direct field access) to access the transactionManager field. - Prevent duplicate transactionManager field/method weaving in subclasses when already inherited or declared on the superclass. - Update GormRegistry lookup methods to return null instead of throwing IllegalStateException on missing qualifiers if the defaultDatastore is initialized. - Expose defaultDatastore property in GormRegistry. - Update ISSUES.md.
… isolate Hibernate 5 multi-tenant tests
…te getDatastoreForTenantId to getDatastoreForConnection
…-tenancy and proxy failures (MBG-6)
…namic finder delegation, and improve input validation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15678 +/- ##
===================================================
- Coverage 49.2583% 19.0184% -30.2399%
+ Complexity 16506 288 -16218
===================================================
Files 1941 57 -1884
Lines 92023 3423 -88600
Branches 16062 597 -15465
===================================================
- Hits 45329 651 -44678
+ Misses 39649 2642 -37007
+ Partials 7045 130 -6915 🚀 New features to boost your workflow:
|
f50cb5c to
7b08ae1
Compare
Resolves 15 merge conflicts introduced when the PR base was automatically changed from 8.0.x-hibernate7 to 8.0.x. Resolution strategy: prefer 8.0.x structure; preserve O(M+N) scaling changes from HEAD. Files resolved by taking 8.0.x (pure infra/test changes): - gradlew.bat — improved Windows setlocal/exit handling - GrailsDataHibernate5TckManager.groovy — simplified cast - HibernateDatastoreSpringInitializerSpec.groovy — simplified withNewSession calls - DistinctProjectionSpec.groovy — correct Person domain import - DirtyCheckingAfterListenerSpec.groovy — remove duplicate imports - GormEnhancerSpec.groovy — new getAll test coverage from 8.0.x - NullValueEqualSpec.groovy — add H7 @IgnoreIf condition - OneToManySpec.groovy — improved test naming - OptimisticLockingSpec.groovy — improved @IgnoreIf style Files resolved by manual merge (preserve O(M+N) content): - HibernateEntity.groovy — keep findAllWithNativeSql/findWithNativeSql (new public API from HEAD); adopt 8.0.x currentHibernateStaticApi() helper; keep findAllWithSql/findWithSql non-deprecated per 8.0.x - HibernateGormInstanceApi.groovy — take 8.0.x's isAttached/lock/attach with HibernateAttachSupport; remove duplicate methods left by HEAD reorganization - HibernateGormStaticApi.groovy — merge-both: DatastoreResolver constructors, withNewSession(Serializable), eachTenant, withTenant from HEAD (O(M+N) multi-tenancy routing); doListInternal/doSingleInternal, findAllWithNativeSql, findWithNativeSql, event-firing query helpers from 8.0.x - GrailsDataTckManager.groovy — keep HEAD's TransactionCapableDatastore and PlatformTransactionManager (O(M+N) test infra); prefer 8.0.x domain class list - HibernateGormEnhancerSpec.groovy — correct package for HibernateGormDatastoreSpec (grails.gorm.tests); keep GormRegistry import (O(M+N) assertions) - HibernateGormStaticApiSpec.groovy — correct Club package (grails.gorm.tests); keep GormRegistry + AvailableHints imports (both actively used); take 8.0.x improved test bodies with GString injection-safety test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
taking a look now, I use multi-tenant in a few apps |
|
@borinquenkid I am seeing a bit more than expected in the PR including routing fixes, transaction changes, test isolation changes, Mongo/Neo4j/simple datastore edits, TCK rewrites, debug specs, and deleted cross-layer tests. Can you catch me up on how these relate to Multi-tenancy resolution regressions and Child datastore initialization order ? |
|
Thanks for the detailed read — happy to connect the dots. The single root cause is commit 8f1500d, which replaced GormEnhancer's per-entity static maps (one (entityClass, qualifier) → GormApi entry per entity × per connection — O(M×N) memory) with a single process-wide GormRegistry that resolves APIs on demand using a selector chain. Every other change in this PR is a downstream ripple from that one architectural swap. The two threads you named map to the two hardest ripples: Thread 1 — Multi-tenancy resolution regressions The old static maps gave each entity a direct pointer to its API for a given connection at enhancement time. The registry replaces that with a runtime selector chain (ActiveSession → Preferred → Qualified → Default). Several call sites assumed the old direct-lookup contract and started resolving to the wrong (usually default) datastore for non-default connections or tenants:
Thread 2 — Child datastore initialization order ChildHibernateDatastore.initialize() returned null in the 8.0.x-hibernate7 base — the child was intentionally not enhanced. Under the registry, each child must register its own APIs (so multi-datasource entities resolve to the right child). The new initialize() calls new HibernateGormEnhancer(this, txManager, settings, datastoresByConnectionSource). The critical ordering constraint: datastoresByConnectionSource in the parent must be populated before the child is constructed (the parent builds its connection map during its own initialize(), then calls new ChildHibernateDatastore(parent, ...)). The bindParent() / PARENT_HOLDER thread-local in ChildHibernateDatastore handles the case where getPrimaryDatastore() is called during the super-constructor chain before this.parent is assigned.
Two items that need explicit acknowledgment:
tl;dr: The PR is wider than a typical bug-fix because the root change was architectural — every datastore (H5, H7, Mongo, Neo4j, SimpleMap), the test harness, and the TCK had local copies of the enhancement pattern that all needed updating. The scope tracks the scope of the original GormRegistry rewrite. |
…d debug spec cleanup - HibernateGormStaticApi: replace bare `persistentEntity` references (from the now-removed AbstractGormApi field) with `gormPersistentEntity` property calls in buildWhereHql, validateWherePropertyName, getAllInternal, prepareHqlQuery, firePostQueryEvent, and firePreQueryEvent; fixes 17 @CompileStatic errors that appeared after the GormRegistry migration removed the field - HibernateGormStaticApi: remove duplicate `import...Query` (was shadowing the aliased `import...Query as GormQuery`); update two bare Query usages to GormQuery - HibernateEntity: replace removed GormEnhancer.findStaticApi() call with GormRegistry.findStaticApi() so currentHibernateStaticApi() compiles under @CompileStatic - grails-data-mongodb: remove 7 debug/duplicate geo test files (DebugGeoJSONSpec, DebugGeoJSONDecodeSpec, DebugGeoJSONQuerySpec, DebugGetSpec, GeoPlaceTest, GeoRetrieveTest, SimplePlaceTest) that imported a non-existent MongoDatastoreSpec and whose coverage is fully provided by GeoJSONTypePersistenceSpec - grails-datamapping-tck: add Javadoc to four replacement specs explaining why CrossLayerMultiDataSourceSpec and CrossLayerMultiTenantMultiDataSourceSpec were removed and where their coverage now lives Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… merge The cosmetics cleanup commit (efc250a) removed the 'as Class[]' coercions from domainClasses when passing it to DefaultGrailsApplication and HibernateDatastore constructors. Those constructors expect Class<?>[] but getDomainClasses() returns Collections.unmodifiableList() (an UnmodifiableRandomAccessList), so Groovy's MOP cannot find a matching constructor at runtime, failing every TCK spec with: GroovyRuntimeException: Could not find matching constructor for: DefaultGrailsApplication(UnmodifiableRandomAccessList, GroovyClassLoader) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@borinquenkid I think something laser targeted to |
…nation, and test class conflicts - Revert version property initialization from constX(0L) to null in GormEntityTransformation; our O(M+N) scaling commit mistakenly changed the initial value, causing DirtyCheckBindingSpec to assert version=null on a transient entity that actually had version=0L. - Fix HibernateGormInstanceApi.isAttached/lock/attach to use getHibernateTemplate() lazy getter instead of direct hibernateTemplate field access; HibernateGormApiFactory constructs the API without setting that field, so direct access was an NPE for any request touching these methods. - Fix HibernateGormStaticApi.executeQuery(query, Map) to pass the same map as both namedParams and querySettings (matching H5 behavior); pagination keys (max, offset) were being sent only to namedParams where INTERNAL_SETTINGS skips them, so no setMaxResults was applied. - Add coerceId() helper in H7 HibernateSession.retrieveAll() to convert String IDs to the entity's declared identifier type before HQL lookup and result map key comparison. - Rename local Person class to DirtyCheckPerson in HibernateDirtyCheckingSpec and HibernateUpdateFromListenerSpec (H5+H7) to eliminate DuplicateMappingException collision with the TCK's org.apache.grails.data.testing.tck.domains.Person. - Replace reflection on removed DATASTORES/STATIC_APIS fields in H5 GormEnhancerCleanupSpec and HibernateGormEnhancerSpec with GormRegistry public API (matching H7 versions). - Remove @NotYetImplemented from demo33 UniqueConstraintOnHasOneSpec — the test now passes. - Fix MongoDB PlacePartialTest/PlaceWithExceptionTest/PlaceWithoutSphereTest compilation by extending GrailsDataTckSpec<GrailsDataMongoTckManager> instead of non-existent MongoDatastoreSpec. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🚨 TestLens detected 1128 failed tests 🚨Here is what you can do:
Test SummaryCI - Groovy Joint Validation Build / build_grails > :grails-data-hibernate5-core:test
CI - Groovy Joint Validation Build / build_grails > :grails-data-hibernate5-core:test
CI - Groovy Joint Validation Build / build_grails > :grails-data-hibernate7-core:test (first 40 of 46)
CI - Groovy Joint Validation Build / build_grails > :grails-data-hibernate7-core:test (first 40 of 46)
CI - Groovy Joint Validation Build / build_grails > :grails-data-mongodb-core:test (first 40 of 513)
CI - Groovy Joint Validation Build / build_grails > :grails-data-mongodb-core:test (first 40 of 513)
🏷️ Commit: 2f651c7 Test Failures (first 10 of 1128)AggregateMethodSpec > Test aggregate method (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)AggregateMethodSpec > Test aggregate method (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)AssignedIdentifierSpec > Test that assigned identifiers work with property setting (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)AssignedIdentifierSpec > Test that assigned identifiers work with property setting (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)AssignedIdentifierSpec > Test that assigned identifiers work with stateless domains (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)AssignedIdentifierSpec > Test that assigned identifiers work with stateless domains (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)AssignedIdentifierSpec > Test that assigned identifiers work with the constructor (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)AssignedIdentifierSpec > Test that assigned identifiers work with the constructor (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)AssignedIdentifierSpec > Test that entities can be saved, retrieved and updated with assigned ids (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)AssignedIdentifierSpec > Test that entities can be saved, retrieved and updated with assigned ids (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / build_grails)Muted Tests (first 20 of 1128)Select tests to mute in this pull request:
Reuse successful test results:
Click the checkbox to trigger a rerun:
Learn more about TestLens at testlens.app. |
|
And here is a targeted stab at Child datastore initialization order: #15776 Having 456 files in this PR makes it challenging for the team to review in a reasonable timeframe. |
|
Superseded by the restructured GormRegistry PR stack (feat/gorm-registry-core-impl, feat/gorm-registry-core-tests, and per-adapter PRs). |
Description
This PR replaces #15656, which was rebuilt on a clean branch to remove a corruption commit from the history.
Problem: In multi-tenant GORM environments with many tenants (M) and domain classes (N), the previous implementation instantiated a full set of static API, instance API, and validation API objects per tenant per entity, producing O(M × N) object allocations. This caused excessive memory consumption and degraded startup performance as tenant counts scaled.
Solution: Refactor
GormRegistryto use a single shared registry keyed by entity class name and qualifier (datasource/tenant), with the GORM API objects created once per entity per qualifier and reused across tenants. This reduces the allocation profile to O(M + N).This rebuild also fixes two regression classes exposed by the new registry:
Multi-tenancy resolution regressions —
GormApiResolverandGormRegistry.registerEntityDatastoreswere routing DISCRIMINATOR/SCHEMA tenant IDs through the datasource connection lookup path, causing child datastores to be overwritten by the parent andPartitionedMultiTenancySpec.count()to NPE. Fixed by detecting multi-tenancy mode before delegating togetDatastoreForConnection, and by skipping non-DEFAULT qualifier registration when the qualifier resolves back to the parent (i.e., it's a runtime tenant ID, not a datasource name).Child datastore initialization order —
HibernateDatastore(H5) andChildHibernateDatastore(H7) were throwingConfigurationExceptionwhengetDatastoreForConnectionwas called for a sibling during initialization before all children were registered. Fixed to returnnullduring the initialization phase soGormRegistryfalls back gracefully and re-registers once initialization completes.Test infrastructure: Added
forkEvery = 1togradle/hibernate5-test-config.gradleandgradle/hibernate7-test-config.gradle. The root config usesforkEvery = 50/100for speed, but with a sharedGormRegistrysingleton, TCK specs running in the same JVM beforePartitionedMultiTenancySpecwere clearingdatastoresByQualifier["default"]and causing the NPE described above. Each test class now gets its own JVM.Verified: H5 — 669 tests / 0 failures. H7 — 2960 tests / 0 failures.
Contributor Checklist
Issue and Scope
8.0.x-hibernate7— major release branch; breaking API changes permitted).Code Quality
./gradlew codeStylehas been run and violations resolved.Licensing and Attribution
Documentation