Skip to content

Commit 848c778

Browse files
committed
H7: Refactor InstanceApiHelper to a per-datastore singleton and update scalability analysis
1 parent 751a7d8 commit 848c778

4 files changed

Lines changed: 30 additions & 6 deletions

File tree

grails-data-hibernate7/ISSUES.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
4. **Bootstrapping Stability:** Used lazy initialization for the shared template to ensure `SessionFactory` services are fully available before the template is created.
2727

2828
### Verification Results
29-
Full TCK suite run (2,923 tests) now passes successfully with **0 failures** and stable memory usage. Distinct `GrailsHibernateTemplate` instances were reduced from **12** to **4** in a 4-tenant test case.
29+
1. **Flyweight Template:** Full TCK suite (2,923 tests) now passes. Distinct `GrailsHibernateTemplate` instances reduced from **12** to **4** in a 4-tenant test case.
30+
- **Absolute Saving:** ~149 GB projected for 1,000 tenants / 100 classes.
31+
2. **InstanceApiHelper Singleton:** Reduced `InstanceApiHelper` count from one-per-class to one-per-datastore.
32+
- **Absolute Saving:** ~99,000 objects (~3.1 MB) removed from heap tracking per 100,000 Class-Tenant pairs.
33+
3. **Registry Stability:** `GormEnhancer` now correctly purges datastores and entities, resulting in a stable memory floor across long-running test suites.
3034

3135
---
3236

@@ -116,8 +120,9 @@ Grails provides excellent **Tenant Resolution** (finding who the tenant is) but
116120
The use of `TransactionSynchronizationManager` to bind sessions in `HibernateCriteriaBuilder` creates "Memory Anchors." If a Criteria query is initialized but never executed or closed (common in complex DSL failures), the session — along with its heavy Hibernate 7 state — remains pinned to the **ThreadLocal** map. In thread-pooled environments (Tomcat/Jetty), this memory is never released and pollutes subsequent requests.
117121

118122
### Proposed Fixes
119-
- **DI Managed Bean:** Register the `GrailsHibernateTemplate` as a Spring/Micronaut bean in `HibernateDatastoreSpringInitializer` so it can be shared across all components and datastores using the same `SessionFactory`.
120-
- **API Bridge Refactoring:** Update `HibernateGormStaticApi`, `HibernateGormInstanceApi`, and `HibernateGormValidationApi` to receive the shared template via constructor injection (or from the datastore) rather than instantiating their own.
123+
- **[COMPLETED] Flyweight Template:** Refactor `HibernateDatastore` to hold a single, shared instance of `GrailsHibernateTemplate`.
124+
- **[COMPLETED] API Bridge Refactoring:** Updated `HibernateGormStaticApi`, `HibernateGormInstanceApi`, and `HibernateGormValidationApi` to receive the shared template.
125+
- **[COMPLETED] Fix `close()` Bug:** Corrected the `DATASTORES.get(q)?.remove(datastore)` logic in `GormEnhancer`.
121126
- **Refactor `GormEnhancer`:** Move away from static maps to instance-based maps managed by the `Datastore` instance.
122-
- **Fix `close()` Bug:** Correct the `DATASTORES.get(q)?.remove(datastore)` logic in `GormEnhancer` to use the entity name as the key.
123-
- **LRU/Weak Cache:** Implement a `WeakHashMap` or a LRU cache for tenant-specific API objects to allow eviction under memory pressure.
127+
- **LRU/Weak Cache:** Implement a `WeakHashMap` or a LRU cache for tenant-specific API objects.
128+
- **[COMPLETED] InstanceApiHelper Singleton:** Refactored `InstanceApiHelper` to be a singleton per datastore instead of per-class.

grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ public class HibernateDatastore extends AbstractDatastore
165165
/** The hibernate template. */
166166
protected IHibernateTemplate hibernateTemplate;
167167

168+
/** The instance API helper. */
169+
protected InstanceApiHelper instanceApiHelper;
170+
168171
/** The connection sources. */
169172
protected final ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> connectionSources;
170173

@@ -929,6 +932,13 @@ public IHibernateTemplate getHibernateTemplate() {
929932
return this.hibernateTemplate;
930933
}
931934

935+
public InstanceApiHelper getInstanceApiHelper() {
936+
if (this.instanceApiHelper == null) {
937+
this.instanceApiHelper = new InstanceApiHelper((GrailsHibernateTemplate) getHibernateTemplate());
938+
}
939+
return this.instanceApiHelper;
940+
}
941+
932942
@Override
933943
public <T> T withSession(final Closure<T> callable) {
934944
Closure<T> multiTenantCallable = prepareMultiTenantClosure(callable);

grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
110110
this.autoFlush = datastore.autoFlush
111111
this.failOnError = datastore.failOnError
112112
this.markDirty = datastore.markDirty
113-
this.instanceApiHelper = new InstanceApiHelper((GrailsHibernateTemplate) this.hibernateTemplate)
113+
this.instanceApiHelper = datastore.getInstanceApiHelper()
114114
}
115115

116116
@Override

grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ class HibernateGormInstanceApiSpec extends HibernateGormDatastoreSpec {
4141
api.hibernateTemplate.is(manager.hibernateDatastore.getHibernateTemplate())
4242
}
4343

44+
void "Test that HibernateGormInstanceApi uses the shared InstanceApiHelper from the datastore"() {
45+
given:
46+
def enhancer = manager.hibernateDatastore.gormEnhancer
47+
def api = enhancer.getInstanceApi(PersonInstanceApi)
48+
49+
expect:
50+
api.instanceApiHelper.is(manager.hibernateDatastore.getInstanceApiHelper())
51+
}
52+
4453
@Rollback
4554
def "test save and get"() {
4655
given:

0 commit comments

Comments
 (0)