feat: add SessionResolver infrastructure and extend core datastore APIs#15779
Open
borinquenkid wants to merge 6 commits into
Open
feat: add SessionResolver infrastructure and extend core datastore APIs#15779borinquenkid wants to merge 6 commits into
borinquenkid wants to merge 6 commits into
Conversation
…enantCount)
GormEnhancer.registerEntity eagerly instantiated a static, instance, and
validation API provider for every (entity, qualifier) pair. For a MultiTenant
entity expanded across N tenant connection sources this is
O(entityCount * tenantCount) provider allocations at application startup.
Where the eager fan-out occurs:
DISCRIMINATOR (column) No - single shared connection; entities
expand to [DEFAULT] only
SCHEMA (schema-per-tenant) Yes - allQualifiers() resolves existing schemas
from the live database at startup
(schemaHandler.resolveSchemaNames); with N
pre-registered schemas it allocated N*M
DATABASE (database-per-tenant) Yes - statically-configured per-tenant
datasources expand every entity across N
apiQualifiers() now restricts eager provider creation to the canonical qualifier
set while still registering datastore routing for every qualifier; per-qualifier
providers are created lazily on first access via computeIfAbsent (single-flight),
collapsing startup allocation to O(entityCount + tenantCount).
Adds GormEnhancerAllQualifiersSpec (datamapping-core) and mirrored
GormApiAllocationSpec for hibernate5 and hibernate7.
Assisted-by: claude-code:claude-opus-4.8
Two bugs surfaced when lazy GORM API allocation was introduced for tenant qualifiers: 1. DATABASE per-tenant: SQLErrorCodesFactory eagerly acquires a connection via DataSourceUtils during GrailsHibernateTemplate construction while a parent-transaction synchronisation is already active, binding the child DataSource to TSM. The original executeWithNewSession code only unbound the DataSource when the SessionFactory was also bound, so the stale ConnectionHolder was still present when HibernateTransactionManager called doBegin, causing "Already value bound" for the child connection. Fix: decouple the DataSource unbind/rebind from the SessionFactory null-check so both are restored independently (hibernate5 + hibernate7). 2. SCHEMA per-tenant: addTenantForSchema re-creates a child datastore (new SessionFactory) on every test setup. registerAllEntitiesWithEnhancer updated DATASTORES routing but not STATIC_APIS/INSTANCE_APIS/VALIDATION_APIS for non-eager schema qualifiers, leaving a stale API referencing the old SessionFactory. The next withNewSession bound the new SF to TSM, but findStaticApi returned the cached API for the old SF, so getCurrentSession() found nothing. Fix: in GormEnhancer.registerEntity, evict the stale lazy-cached API entries for any qualifier that is not in apiQualifiers, forcing re-creation against the current datastore on next access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This was referenced Jun 27, 2026
…ng fixes
Cover the two bugs fixed in the previous commit:
1. GrailsHibernateTemplate.executeWithNewSession TSM bug (H5 + H7):
- H5: standalone spec; manually pre-binds a ConnectionHolder for the
DataSource without a matching SessionFactory holder in TSM, then
verifies executeWithNewSession completes without "Already value bound".
- H7: uses the outer transaction's active synchronisation; a secondary
local datastore triggers SQLErrorCodesFactory to auto-bind its DS to
TSM, replicating the production trigger without manual bindResource.
2. GormEnhancer.registerEntity stale API eviction (H5 + H7):
- H5: calls addTenantForSchema('tenantA') twice (H5 has no idempotency
guard, so the second call re-creates the child and re-registers
entities); verifies stale lazily-cached APIs for tenantA are evicted.
- H7: creates a second SCHEMA datastore for the same entity class without
closing the first; verifies that SchemaTenantGormEnhancer.registerEntity
evicts stale lazily-cached tenant APIs on the new datastore's
registration pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce SessionResolver and ThreadLocalSessionResolver for thread-safe session lookup without coupling callers to a specific Datastore instance. Extend AbstractDatastore, Datastore, DatastoreUtils, and MappingContext with the hooks GormRegistry needs for O(M+N) API registration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
da5001a to
4b9d006
Compare
…pilation Making getDatastore()/setDatastore() abstract in the Service trait breaks @CompileStatic classes that implement the trait (e.g. DefaultTenantService), because Groovy's static compiler does not properly satisfy trait abstract method contracts when the implementing class declares the same method in its own body. Restore the original backing-field implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MappingContext interface declares initialize(ConnectionSourceSettings) as public; MongoMappingContext.initialize was protected, which Java rejects as assigning weaker access privileges to an interface method implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
✅ All tests passed ✅🏷️ Commit: a870fc8 Learn more about TestLens at testlens.app. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Prerequisite for the GormRegistry O(M+N) scaling stack. Introduces low-level infrastructure in `grails-datastore-core` that the GormRegistry layer depends on:
This PR carries no GORM API changes and has no dependency on any GormRegistry class — it can be reviewed and merged independently.
Relationship to the upstream fix
This stack builds on top of fix/gorm-api-registration-scaling, which solves the same O(M×N) problem through a different strategy. The two approaches are deliberately complementary:
Upstream fix strategy — incremental optimization (4 files, ~815 lines)
The partner's fix adds
apiQualifiers()toGormEnhancerto split tenant qualifiers into a canonical eager set and a lazy-on-demand set, then introduces an internalGormEnhancerRegistrythat allocates per-qualifier API providers viacomputeIfAbsent. This collapses startup allocation from O(entityCount × tenantCount) to O(entityCount + tenantCount) while keepingGormEnhanceras the sole stateholder. Two follow-up commits fix TSM consistency bugs that surfaced from the lazy-allocation: a staleConnectionHolderinGrailsHibernateTemplate.executeWithNewSession(DATABASE mode) and stale cached APIs afteraddTenantForSchemare-creates a child datastore (SCHEMA mode).Footprint: all changes contained within
GormEnhancer.groovyandGrailsHibernateTemplate.java(H5 + H7). No new public types.This stack strategy — registry-first architecture (251 files across 9 PRs)
This stack extracts all per-entity, per-qualifier API state from
GormEnhancerinto a dedicatedGormRegistrysingleton.GormEnhancerbecomes stateless — it delegates entity registration, API lookup, and datastore lifecycle to the registry. APIs are created at entity-registration time by a pluggableGormApiFactory(one implementation per datastore type), then looked up O(1) by(entityClass, qualifier)at call time.Key differences from the upstream fix:
computeIfAbsentGormRegistry,GormApiFactory,SessionResolverregisterEntityGormApiFactoryGormEnhancer+ H5/H7 templateConflict resolution: our
GormRegistrysupersedes the upstreamGormEnhancerRegistry. When rebasing this stack onto the upstream fix, conflicts inGormEnhancer.groovyare always resolved in favour of our version — the registry approach provides a strict superset of the optimization.Test plan
Stack
This is the prereq for:
🤖 Generated with Claude Code