Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- [COLDBOX-1419] Scheduled tasks could not load module components on engines that lose path mappings between requests, including Adobe ColdFusion behind IIS. ColdBox now stores mappings for module folders and registered module paths. Scheduled tasks register these paths with the CFML engine before the task starts.

## [8.1.0] - 2026-04-14

- <https://coldbox.ortusbooks.com/readme/release-history/whats-new-with-8.1.0>
Expand Down
14 changes: 13 additions & 1 deletion system/web/services/ModuleService.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" {
physicalPath : expandPath( "/" & replace( arguments.invocationPath, ".", "/", "all" ) ),
invocationPath : arguments.invocationPath
}
// Register the module path with the CFML engine. This lets scheduled tasks find
// module components when no web request is active. (COLDBOX-1419)
var explicitLocation = variables.moduleRegistry[ arguments.moduleName ]
variables.mappingRegistry[ explicitLocation.locationPath ] = explicitLocation.physicalPath
variables.util.addMapping( name: explicitLocation.locationPath, path: explicitLocation.physicalPath )
}

// Check if passed module name is not loaded into the registry
Expand Down Expand Up @@ -1506,7 +1511,14 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" {
*/
private function scanModulesDirectory( required dirPath ){
var expandedPath = expandPath( arguments.dirpath )
var dirEntries = directoryList( expandedPath, false, "query", "", "asc" )

// Register this module folder with the CFML engine. This lets scheduled tasks find
// module components when no web request is active. (COLDBOX-1419)
var mappingName = arguments.dirPath.startsWith( "/" ) ? arguments.dirPath : "/" & arguments.dirPath
variables.mappingRegistry[ mappingName ] = expandedPath
variables.util.addMapping( name: mappingName, path: expandedPath )

var dirEntries = directoryList( expandedPath, false, "query", "", "asc" )
for ( var item in dirEntries ) {
// Only directories and no . folders
if ( item.type == "Dir" && !item.name.startsWith( "." ) ) {
Expand Down
21 changes: 21 additions & 0 deletions system/web/tasks/ColdBoxScheduledTask.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,27 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" {
return this
}

/**
* Runs this task on the scheduler's background thread.
* Registers module paths with the CFML engine before the task starts. Adobe ColdFusion
* can lose mappings that code adds while the application runs. Scheduled tasks also
* skip the mapping setup used by web requests. Without this setup, a task may not find
* its module components. (COLDBOX-1419)
*
* @force Run the task even if it is disabled or blocked by a constraint
*/
function run( boolean force = false ){
try {
if ( !isNull( variables.controller ) ) {
variables.controller.getModuleService().loadMappings();
}
} catch ( any e ) {
// Log the mapping error and let the task continue.
err( "Error loading module mappings for task (#getName()#) : #e.message & e.detail#" );
}
super.run( argumentCollection = arguments );
}

/**
* This method verifies if the running task is constrained to run on specific valid constraints:
*
Expand Down
44 changes: 44 additions & 0 deletions tests/specs/async/tasks/ColdBoxScheduledTaskSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,50 @@ component extends="tests.resources.BaseIntegrationTest" {
expect( t.getCache().getKeys() ).toInclude( t.getFixationCacheKey() );
} );

describe( "module mappings for scheduled tasks (COLDBOX-1419)", function(){
it( "loads module mappings before the task runs", function(){
var t = prepareMock(
scheduler
.task( "cbTask-mapping-replay" )
.call( function(){
return "ran";
} )
);

var mockModuleService = createEmptyMock( "coldbox.system.web.services.ModuleService" ).$( "loadMappings" );
var mockController = createStub().$( "getModuleService", mockModuleService );
t.$property( propertyName: "controller", mock: mockController );

t.run( force = true );

expect( mockModuleService.$once( "loadMappings" ) ).toBeTrue();
expect( t.getStats().totalSuccess ).toBe( 1 );
} );

it( "runs the task when loading module mappings fails", function(){
var t = prepareMock(
scheduler
.task( "cbTask-mapping-replay-fail" )
.call( function(){
return "ran";
} )
);

var mockModuleService = createEmptyMock( "coldbox.system.web.services.ModuleService" ).$(
method : "loadMappings",
throwException: true,
throwType : "MockMappingException"
);
var mockController = createStub().$( "getModuleService", mockModuleService );
t.$property( propertyName: "controller", mock: mockController );

t.run( force = true );

expect( mockModuleService.$once( "loadMappings" ) ).toBeTrue();
expect( t.getStats().totalSuccess ).toBe( 1 );
} );
} );

describe( "schedule synchronization", function(){
it( "stores schedule metadata in cache lock", function(){
var t = scheduler
Expand Down
31 changes: 31 additions & 0 deletions tests/specs/web/services/ModuleServiceTest.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,37 @@ component extends="tests.resources.BaseIntegrationTest" {
.toBeInstanceOf( "mserv.models.MyModel" );
} );

it( "Adds every module search folder to the mapping registry (COLDBOX-1419)", function(){
var mappingRegistry = variables.moduleService.getMappingRegistry();
var scanLocations = [
"/coldbox/system/modules",
getController().getSetting( "ModulesLocation" )
];
scanLocations.append( getController().getSetting( "ModulesExternalLocation" ), true );

scanLocations
.filter( function( location ){
return arguments.location.trim().len();
} )
.each( function( location ){
var mappingName = arguments.location.startsWith( "/" ) ? arguments.location : "/" & arguments.location;
expect( mappingRegistry ).toHaveKey( mappingName );
expect( mappingRegistry[ mappingName ] ).toBe( expandPath( mappingName ) );
} );
} );

it( "Adds a registered module path to the mapping registry (COLDBOX-1419)", function(){
if ( !variables.moduleService.isModuleRegistered( "test-module" ) ) {
variables.moduleService.registerAndActivateModule( "test-module", "tests.resources" );
}
var mappingRegistry = variables.moduleService.getMappingRegistry();
// The CFML component loader must be able to find the registered module path.
expect( mappingRegistry ).toHaveKey( "/tests/resources" );
expect( mappingRegistry[ "/tests/resources" ] ).toBe( expandPath( "/tests/resources" ) );
// Keep the "mserv" mapping declared by the test module.
expect( mappingRegistry ).toHaveKey( "/mserv" );
} );

it( "Can reload a convention registered module", function(){
variables.moduleService.reload( "api" );
expect( variables.moduleService.getModuleRegistry() ).toHaveKey( "api" );
Expand Down
Loading