Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c4fb997
Add optional parallel eager loading
elpete Aug 22, 2026
7c5e3d5
fix: make parallel eager loading cross-engine safe
elpete Aug 22, 2026
eaf332f
fix: preserve engine thread transfer semantics
elpete Aug 22, 2026
dfbcf08
refactor: avoid internal closures
elpete Aug 25, 2026
6e0331a
fix: isolate parallel merge loop variables
elpete Aug 25, 2026
d486fc7
fix: preserve Lucee thread merge scope
elpete Aug 25, 2026
f825002
fix: preserve parallel eager-load results across threads
elpete Aug 25, 2026
2c9d853
fix: transfer eager relationship values directly
elpete Aug 25, 2026
1aa04ad
fix: detect parallel support on BoxLang
elpete Aug 25, 2026
f36712a
fix: retain eager-loaded CFCs in their worker state
elpete Aug 25, 2026
62f29ea
fix: transfer parallel eager loads as entity state
elpete Aug 25, 2026
fc1ce25
fix: tag null thread values before assignment
elpete Aug 25, 2026
d4ca77c
fix: hydrate loaded entities for parallel workers
elpete Aug 25, 2026
017045f
fix: hydrate parallel entities inside workers
elpete Aug 25, 2026
26c7041
fix: preserve parallel eager loading semantics
elpete Aug 27, 2026
e423faa
test: allow concurrent worker failure ordering
elpete Aug 27, 2026
1913614
fix: use portable thread attribute resolution
elpete Aug 27, 2026
66fbbc8
fix: disambiguate serialized attribute state
elpete Aug 27, 2026
6bec058
fix: avoid BoxLang thread scope collision
elpete Aug 27, 2026
782ae5e
fix parallel eager loading isolation
elpete Aug 27, 2026
d7e53b5
split eager loading test registrations
elpete Aug 27, 2026
9453049
avoid Lucee test bytecode verifier failure
elpete Aug 27, 2026
14402f0
Use ColdBox executor for parallel eager loading
elpete Aug 27, 2026
eb3a1b1
Track parallel worker contexts explicitly
elpete Aug 27, 2026
8d1092a
Isolate BoxLang parallel worker requests
elpete Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions ModuleConfig.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ component {

function configure() {
settings = {
"defaultGrammar" : "AutoDiscover@qb",
"defaultQueryOptions" : {},
"preventDuplicateJoins" : true,
"preventLazyLoading" : false,
"refreshOnSaveFallback" : true,
"lazyLoadingViolationCallback" : ( entity, relationName ) => {
"defaultGrammar" : "AutoDiscover@qb",
"defaultQueryOptions" : {},
"parallelEagerLoadingMaxThreads" : 4,
"parallelEagerLoadingTimeout" : 60000,
"preventDuplicateJoins" : true,
"preventLazyLoading" : false,
"refreshOnSaveFallback" : true,
"lazyLoadingViolationCallback" : ( entity, relationName ) => {
throw(
type = "QuickLazyLoadingException",
message = "Attempted to lazy load the [#arguments.relationName#] relationship on the entity [#arguments.entity.mappingName()#] but lazy loading is disabled. This is usually caused by the N+1 problem and is a sign that you are missing an eager load."
Expand Down Expand Up @@ -54,6 +56,15 @@ component {
}

function onLoad() {
wirebox
.getInstance( "AsyncManager@coldbox" )
.newExecutor(
name = "quick-parallel-eager-loading",
type = "fixed",
threads = max( 1, int( settings.parallelEagerLoadingMaxThreads ) ),
loadAppContext = true
);

binder
.map( alias = "QuickQB@quick", force = true )
.to( "#moduleMapping#.models.QuickQB" )
Expand Down Expand Up @@ -86,6 +97,11 @@ component {
}

function onUnload() {
var asyncManager = wirebox.getInstance( "AsyncManager@coldbox" );
if ( asyncManager.hasExecutor( "quick-parallel-eager-loading" ) ) {
asyncManager.deleteExecutor( "quick-parallel-eager-loading" );
}

var cacheBox = wirebox.getCachebox();
if ( cacheBox.cacheExists( settings.metadataCache.name ) ) {
cacheBox.getCache( settings.metadataCache.name ).clearAll();
Expand Down
6 changes: 3 additions & 3 deletions models/BaseEntity.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -949,11 +949,11 @@ component accessors="true" {
overlay = overlay.previous;
}

var attributes = [];
var runtimeAttributes = [];
for ( var i = newestFirst.len(); i >= 1; i-- ) {
attributes.append( newestFirst[ i ] );
runtimeAttributes.append( newestFirst[ i ] );
}
return attributes;
return runtimeAttributes;
}

private void function registerRuntimeAttribute( required struct attribute ) {
Expand Down
56 changes: 56 additions & 0 deletions models/ParallelEagerLoadingCoordinator.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Coordinates parallel eager-loading work on Quick's application-wide executor.
*/
component singleton {

property name="executor" inject="executor:quick-parallel-eager-loading";
property name="controller" inject="coldbox";
property name="requestService" inject="coldbox:requestService";

function init() {
variables.currentWorker = createObject( "java", "java.lang.ThreadLocal" ).init();
return this;
}

public any function submit( required any task ) {
return variables.executor.submit( arguments.task, "run" );
}

public any function getExecutor() {
return variables.executor;
}

public any function createWorkerRequestContext( required string workerName ) {
var sourceContext = variables.requestService.getContext();
var workerContext = createObject( "component", "coldbox.system.web.context.RequestContext" ).init(
properties = variables.controller.getConfigSettings(),
controller = variables.controller
);
workerContext.collectionAppend( structCopy( sourceContext.getCollection() ), true );
workerContext.collectionAppend(
structCopy( sourceContext.getPrivateCollection() ),
true,
true
);
workerContext.setPrivateValue( "__quickParallelWorkerContextId", arguments.workerName );
return workerContext;
}

public void function enterWorker( required string name ) {
variables.currentWorker.set( arguments.name );
}

public void function leaveWorker() {
variables.currentWorker.remove();
}

public boolean function isWorker() {
return !isNull( variables.currentWorker.get() );
}

public string function getWorkerName() {
var workerName = variables.currentWorker.get();
return isNull( workerName ) ? "" : workerName;
}

}
67 changes: 67 additions & 0 deletions models/ParallelEagerLoadingTask.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Retrieves the database rows for one prepared eager-loading relationship.
* Relationship preparation and entity hydration remain on the calling thread.
*/
component {

function init(
required struct plan,
required string name,
required any coordinator,
required any requestContext,
required any completionQueue
) {
variables.plan = arguments.plan;
variables.name = arguments.name;
variables.coordinator = arguments.coordinator;
variables.requestContext = arguments.requestContext;
variables.completionQueue = arguments.completionQueue;
return this;
}

public void function run() {
// ColdBox's BoxLang executor context retains the caller's request scope.
// Enter a fresh application request so concurrent workers cannot overwrite
// each other's ColdBox RequestContext.
if ( server.keyExists( "boxlang" ) ) {
runThreadInContext(
applicationName = getApplicationMetadata().name,
callback = function() {
execute();
}
);
return;
}
execute();
}

private void function execute() {
var requestContextInstalled = false;
var workerEntered = false;
try {
request.cb_requestContext = variables.requestContext;
requestContextInstalled = true;
variables.coordinator.enterWorker( variables.name );
workerEntered = true;
variables.completionQueue.offer( {
"name" : variables.name,
"success" : true,
"rows" : variables.plan.relation.retrieveEagerRows()
} );
} catch ( any e ) {
variables.completionQueue.offer( {
"name" : variables.name,
"success" : false,
"error" : e
} );
} finally {
if ( workerEntered ) {
variables.coordinator.leaveWorker();
}
if ( requestContextInstalled ) {
structDelete( request, "cb_requestContext" );
}
}
}

}
Loading
Loading