Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8f1500d
Implement O(M+N) GORM scaling optimization (clean rebuild)
borinquenkid May 21, 2026
04cb81f
Add grails-datastore-core optimization changes
borinquenkid May 21, 2026
cfc7588
Fix child datastore initialization order in H5 and H7
borinquenkid May 21, 2026
74810b3
Stabilize multi-tenant datastore resolution and fix test isolation
borinquenkid May 23, 2026
e649c93
Exclude ISSUES.md files from Apache RAT audit
borinquenkid May 23, 2026
f7b1739
fix: resolve GormEnhancer signature mismatches and multi-tenant tr…
borinquenkid May 24, 2026
611e6bd
Remove deprecated calls.
borinquenkid May 24, 2026
d90f7a2
Fix preferred datastore resolution in GormApiResolverSpec
borinquenkid May 25, 2026
5de812a
fix: resolve Data Service multi-datasource routing and clean up debug…
borinquenkid May 27, 2026
a484545
1. GrailsDataHibernate5TckManager.groovy — Added GormRegistry.reset…
borinquenkid May 28, 2026
68af192
fix: suppress spurious MongoDB updates when only auto-timestamps change
borinquenkid May 28, 2026
4358b70
fix: remove extra blank line and add space after typecast in Hibernat…
borinquenkid May 28, 2026
bd1f997
fix: restore GORM API contract, SimpleMap re-save, and tenant entity …
borinquenkid May 29, 2026
39eadad
fix: resolve functional-test regressions from the O(M+N) GORM rewrite
borinquenkid May 29, 2026
da303af
style: clear pre-existing Code Style violations
borinquenkid May 30, 2026
70c27f5
Fix ActiveSessionDatastoreSelector multi-tenant routing and Spock log…
borinquenkid May 30, 2026
032095e
Fix multi-tenant resolution bypass in GormApiResolver and transactions
borinquenkid May 30, 2026
f06f97e
test: fix graphql functional tests query capturing noise
borinquenkid May 30, 2026
bb3e117
ci: selectively disable parallel execution for GORM-dependent modules
borinquenkid May 30, 2026
ed6b993
Clean up Datastore registrations in GormRegistry on destruction and …
borinquenkid May 30, 2026
4c158f4
Avoid opening new GORM sessions in Tenants.withId when current sessio…
borinquenkid May 31, 2026
96ee901
Fix connection routing and transaction manager resolution in GORM sca…
borinquenkid May 31, 2026
6250320
docs: document proposed AST unit tests in ISSUES.md
borinquenkid May 31, 2026
875ab1a
docs: document next steps for unit tests and routing verification in …
borinquenkid May 31, 2026
f59d660
test: add unit tests for multi-datasource and multi-tenant transactio…
borinquenkid May 31, 2026
6760ef3
refactor: implement targeted datastore registry cleanup and resolve m…
borinquenkid Jun 1, 2026
e929ebf
chore: ignore and remove IDE and agent files from git to pass RAT audit
borinquenkid Jun 1, 2026
e189ae8
Resolve GORM multi-tenancy state leaks in test specifications
borinquenkid Jun 1, 2026
9bdc0e6
Restore GORM saveOrUpdate fallback in HibernateGormInstanceApi for no…
borinquenkid Jun 1, 2026
c5ee501
Enforce tenant resolution check in ActiveSessionDatastoreSelector and…
borinquenkid Jun 1, 2026
fe8b0d9
Add GormRegistry fallback in MongoDatastore TenantResolver and delega…
borinquenkid Jun 1, 2026
0dc06b3
Update ISSUES.md to record local verification and resolution of multi…
borinquenkid Jun 1, 2026
343dd58
Refactor multi-tenancy tenant resolution fallback logic in MongoDatas…
borinquenkid Jun 1, 2026
9031c38
flaky login and PR comment
borinquenkid Jun 2, 2026
013e9aa
unexplained rollaback
borinquenkid Jun 2, 2026
6293e80
style: fix CodeNarc consecutive blank lines violation in GrailsDataTc…
borinquenkid Jun 2, 2026
21fb71b
Refactor GormRegistry to use CompileStatic and clean up unused import…
borinquenkid Jun 3, 2026
7b08ae1
Refactor GormRegistry to use standard Groovy properties, implement dy…
borinquenkid Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,8 @@ etc/bin/results
/local.properties
local-tasks.gradle
local-init.gradle
.junie/
.antigravitycli/
.clinerules
.cursorrules
.windsurfrules
818 changes: 818 additions & 0 deletions ISSUES.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ subprojects {
cacheChangingModulesFor(cacheHours, 'hours')
}
}

tasks.withType(Test).configureEach {
systemProperty 'logging.level.org.testcontainers', 'WARN'
systemProperty 'logging.level.com.github.dockerjava', 'WARN'
systemProperty 'logging.level.tc', 'WARN'
systemProperty 'logging.level.asset.pipeline', 'WARN'
systemProperty 'logging.level.org.springframework', 'WARN'
systemProperty 'logging.level.org.hibernate', 'WARN'
systemProperty 'logging.level.com.zaxxer.hikari', 'WARN'
systemProperty 'logging.level.grails.config.external', 'WARN'
systemProperty 'logging.level.org.apache.grails', 'WARN'
}
}

interface ExecSupport {
Expand Down
1 change: 1 addition & 0 deletions gradle/hibernate5-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ tasks.withType(Test).configureEach {
outputs.cacheIf { !doNotCacheTests }
outputs.upToDateWhen { !doNotCacheTests }


onlyIf {
![
'onlyFunctionalTests',
Expand Down
1 change: 1 addition & 0 deletions gradle/hibernate7-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ tasks.withType(Test).configureEach {
outputs.cacheIf { !doNotCacheTests }
outputs.upToDateWhen { !doNotCacheTests }


onlyIf {
![
'onlyFunctionalTests',
Expand Down
1 change: 1 addition & 0 deletions gradle/mongodb-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ tasks.withType(Test).configureEach {

useJUnitPlatform()
maxParallelForks = 1

jvmArgs = ['-Xmx1028M']
afterSuite {
System.out.print('.')
Expand Down
1 change: 1 addition & 0 deletions gradle/rat-root-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ tasks.named('rat') {
'node_modules/**', // exclude node_modules
'**/*.log', // exclude log files
'local-tasks.gradle', // exclude local helper scripts
'**/ISSUES.md', // exclude local issue tracking files (no license header)
] + rootProject.subprojects.collect{"${rootProject.projectDir.relativePath(it.layout.buildDirectory.get().asFile).toString()}/**/*" }
// logger.lifecycle("Excludes for RAT task: ${allExcludes.join(', \n')}")
excludes = allExcludes
Expand Down
38 changes: 37 additions & 1 deletion gradle/test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,38 @@ dependencies {
add('testRuntimeOnly', 'org.junit.platform:junit-platform-launcher')
}

/**
* Recursively checks if a project has a direct or transitive dependency on a target project.
* Used to detect modules that rely on GORM (grails-datamapping-core), which are prone to
* cross-test registry pollution when executed in parallel (maxParallelForks > 1).
*
* Uses Gradle 9 compatible path lookup (proj.project(pd.getPath())) because
* ProjectDependency.getDependencyProject() was removed in Gradle 9.
*
* @param proj the project to inspect
* @param targetProjectName the name of the target dependency project (e.g. 'grails-datamapping-core')
* @param visited set of already visited projects to prevent infinite recursion in cyclic dependency configurations
* @return true if the project depends on the target project
*/
@groovy.transform.CompileStatic
boolean dependsOnProject(Project proj, String targetProjectName, Set<String> visited = new HashSet<String>()) {
if (proj.name == targetProjectName) return true
if (visited.contains(proj.name)) return false
visited.add(proj.name)
for (org.gradle.api.artifacts.Configuration config : proj.configurations) {
for (org.gradle.api.artifacts.Dependency dep : config.dependencies) {
if (dep instanceof org.gradle.api.artifacts.ProjectDependency) {
org.gradle.api.artifacts.ProjectDependency pd = (org.gradle.api.artifacts.ProjectDependency) dep
Project depProj = proj.project(pd.getPath())
if (dependsOnProject(depProj, targetProjectName, visited)) {
return true
}
}
}
}
return false
}

// Disable build cache for Groovy compilation in CI to ensure AST transformations are always applied.
// AST transformers are applied at compile time, and Gradle's incremental compilation might not detect
// when a transformer itself changes, leading to stale bytecode.
Expand Down Expand Up @@ -79,7 +111,11 @@ tasks.withType(Test).configureEach {
showStackTraces = true
}
excludes = ['**/*TestCase.class', '**/*$*.class']
maxParallelForks = configuredTestParallel

// Selectively isolate GORM (grails-datamapping-core) dependent tests to prevent GormRegistry conflicts
def isGormProject = dependsOnProject(project, 'grails-datamapping-core')
maxParallelForks = isGormProject ? 1 : configuredTestParallel

maxHeapSize = isCiBuild ? '768m' : '1024m'
forkEvery = hasProperty('forkEveryUnitTest') ? getProperty('forkEveryUnitTest') as long : (isCiBuild ? 50 : 100)
if (System.getProperty('debug.tests')) {
Expand Down
186 changes: 93 additions & 93 deletions gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

package org.grails.web.converters.marshaller;

import org.grails.datastore.gorm.GormEnhancer;
import org.grails.datastore.gorm.GormRegistry;
import org.grails.datastore.mapping.core.Datastore;
import org.grails.datastore.mapping.model.MappingContext;
import org.grails.datastore.mapping.model.PersistentEntity;
Expand All @@ -29,7 +29,7 @@ public class ByDatasourceDomainClassFetcher implements DomainClassFetcher {
@Override
public PersistentEntity findDomainClass(Object instance) {
Class clazz = instance.getClass();
Datastore datastore = GormEnhancer.findDatastore(clazz);
Datastore datastore = GormRegistry.getInstance().getApiResolver().findDatastore(clazz);
if (datastore != null) {
MappingContext mappingContext = datastore.getMappingContext();
if (mappingContext != null) {
Expand Down
17 changes: 17 additions & 0 deletions grails-data-graphql/ISSUES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# GraphQL O(M+N) Scaling and Performance

## Context
GraphQL GORM integration maps GORM entities to a GraphQL schema. In multi-tenant environments, the schema resolution and data fetching layers must handle tenant context switches efficiently to avoid the O(M+N) performance trap.

## Identified Issues
- **Fetcher Overhead**: GORM Data Fetchers may perform redundant tenant resolution for each field in a deeply nested GraphQL query.
- **Schema Duplication**: If schemas are being re-generated or re-validated per-tenant without caching, it leads to significant CPU and memory pressure.

## Fix Strategy
1. **Context-Aware Fetchers**: Ensure `DataFetcher` implementations capture the tenant ID from the initial execution context and propagate it to GORM static API calls (e.g., using `withTenant(id)` or passing the ID directly to refactored static methods).
2. **Profile Execution**: Use `GraphqlTenantContextProfilingSpec` to measure the cost of fetching data across multiple tenants.

## Targets for B.2 Refactoring
- `org.grails.gorm.graphql.fetcher.PogoDataFetcher`
- `org.grails.gorm.graphql.fetcher.GormEntityDataFetcher`
- `org.grails.gorm.graphql.interceptor.GraphQLInterceptor`
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import graphql.language.SelectionSet;
import graphql.schema.DataFetchingEnvironment;

import org.grails.datastore.gorm.GormEnhancer;
import org.grails.datastore.gorm.GormRegistry;
import org.grails.datastore.mapping.model.PersistentEntity;
import org.grails.datastore.mapping.model.types.Association;
import org.grails.datastore.mapping.model.types.ToMany;
Expand Down Expand Up @@ -60,7 +60,7 @@ public EntityFetchOptions(Class<?> entityClass) {
}

public EntityFetchOptions(Class<?> entityClass, String projectionName) {
this(GormEnhancer.findStaticApi(entityClass).getGormPersistentEntity(), projectionName);
this(GormRegistry.getInstance().findStaticApi(entityClass).getGormPersistentEntity(), projectionName);
}

public EntityFetchOptions(PersistentEntity entity) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@

package org.grails.gorm.graphql.fetcher

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment

import grails.gorm.DetachedCriteria
import grails.gorm.multitenancy.Tenants
import grails.gorm.transactions.TransactionService
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.grails.datastore.gorm.GormEnhancer
import org.grails.datastore.gorm.GormEntity
import org.grails.datastore.gorm.GormRegistry
import org.grails.datastore.gorm.GormStaticApi
import org.grails.datastore.mapping.core.Datastore
import org.grails.datastore.mapping.model.PersistentEntity
Expand Down Expand Up @@ -79,7 +81,7 @@ abstract class DefaultGormDataFetcher<T> implements DataFetcher<T> {
}

protected Object loadEntity(PersistentEntity entity, Object argument) {
GormEnhancer.findStaticApi(entity.javaClass).load((Serializable)argument)
GormRegistry.instance.findStaticApi(entity.javaClass).load((Serializable)argument)
}

protected Map<String, Object> getIdentifierValues(DataFetchingEnvironment environment) {
Expand Down Expand Up @@ -141,7 +143,7 @@ abstract class DefaultGormDataFetcher<T> implements DataFetcher<T> {
}

protected GormStaticApi getStaticApi() {
GormEnhancer.findStaticApi(entity.javaClass)
GormRegistry.instance.findStaticApi(entity.javaClass)
}

abstract T get(DataFetchingEnvironment environment)
Expand Down
Loading
Loading