From 02a319d9424736dae2f541b9dbd33af6344c0265 Mon Sep 17 00:00:00 2001 From: David Levin Date: Mon, 17 Aug 2026 14:56:41 -0700 Subject: [PATCH] fix: scheduled tasks could not load module components on ACF COLDBOX-1419 Scheduled tasks run on background threads that have no web request, and on Adobe ColdFusion behind IIS those threads look for components in the wrong web root. ColdBox only registered an engine mapping for a module when it set this.classMapping, so a task that was the first code to build a module component failed with "component not found". ModuleService now stores an engine mapping for every module search folder and every explicitly registered module path, and ColdBoxScheduledTask registers those mappings on its own thread before each run. Adds four specs; verified on BoxLang, Lucee 5, and Adobe 2023+. --- changelog.md | 4 ++ system/web/services/ModuleService.cfc | 14 +++++- system/web/tasks/ColdBoxScheduledTask.cfc | 21 +++++++++ .../async/tasks/ColdBoxScheduledTaskSpec.cfc | 44 +++++++++++++++++++ .../specs/web/services/ModuleServiceTest.cfc | 31 +++++++++++++ 5 files changed, 113 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index dbb169dd4..e8eb15250 100644 --- a/changelog.md +++ b/changelog.md @@ -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 - diff --git a/system/web/services/ModuleService.cfc b/system/web/services/ModuleService.cfc index 88aa0b51a..519638d34 100755 --- a/system/web/services/ModuleService.cfc +++ b/system/web/services/ModuleService.cfc @@ -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 @@ -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( "." ) ) { diff --git a/system/web/tasks/ColdBoxScheduledTask.cfc b/system/web/tasks/ColdBoxScheduledTask.cfc index 97955baa1..3d9103252 100644 --- a/system/web/tasks/ColdBoxScheduledTask.cfc +++ b/system/web/tasks/ColdBoxScheduledTask.cfc @@ -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: * diff --git a/tests/specs/async/tasks/ColdBoxScheduledTaskSpec.cfc b/tests/specs/async/tasks/ColdBoxScheduledTaskSpec.cfc index f84f0d723..901d455b0 100644 --- a/tests/specs/async/tasks/ColdBoxScheduledTaskSpec.cfc +++ b/tests/specs/async/tasks/ColdBoxScheduledTaskSpec.cfc @@ -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 diff --git a/tests/specs/web/services/ModuleServiceTest.cfc b/tests/specs/web/services/ModuleServiceTest.cfc index 8ed1d1345..5342974e4 100644 --- a/tests/specs/web/services/ModuleServiceTest.cfc +++ b/tests/specs/web/services/ModuleServiceTest.cfc @@ -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" );