| name | boxlang-core-dev-async-tasks |
| description | Use this skill when working with BoxLang asynchronous programming: BoxFuture (CompletableFuture extension), AsyncService, executor types (VIRTUAL/FIXED/CACHED/SCHEDULED), BaseScheduler, ScheduledTask fluent API, scheduling with cron constraints, task lifecycle callbacks, and registerring schedulers via ModuleConfig.bx. |
BoxLang Async & Scheduled Tasks
Overview
BoxLang provides first-class async support built on JRE 21 virtual threads and
CompletableFuture. Key classes:
| Class | Package | Purpose |
|---|
BoxFuture<T> | ortus.boxlang.runtime.async | Extended CompletableFuture |
AsyncService | ortus.boxlang.runtime.services | Executor and future factory |
BaseScheduler | ortus.boxlang.runtime.async.tasks | Extend to create a scheduler |
ScheduledTask | ortus.boxlang.runtime.async.tasks | Fluent scheduled task builder |
BoxExecutor | ortus.boxlang.runtime.async.executors | Thead pool wrapper |
AsyncService — Executor Factory
import ortus.boxlang.runtime.services.AsyncService;
import ortus.boxlang.runtime.async.executors.BoxExecutor;
import ortus.boxlang.runtime.async.BoxFuture;
AsyncService asyncService = BoxRuntime.getInstance().getAsyncService();
BoxExecutor virtualExec = asyncService.newExecutor( "myPool", ExecutorType.VIRTUAL );
BoxExecutor fixedExec = asyncService.newExecutor( "myFixed", ExecutorType.FIXED, 4 );
BoxExecutor cachedExec = asyncService.newExecutor( "myCached", ExecutorType.CACHED );
BoxExecutor scheduledExec = asyncService.newScheduledExecutor( "myScheduled" );
BoxExecutor exec = asyncService.getExecutor( "myPool" );
boolean exists = asyncService.hasExecutor( "myPool" );
asyncService.shutdownExecutor( "myPool" );
Executor Types
| Type | Backed By | Best For |
|---|
VIRTUAL | Executors.newVirtualThreadPerTaskExecutor() | I/O-bound work (default) |
FIXED | Executors.newFixedThreadPool(n) | CPU-bound work with known concurrency |
CACHED | Executors.newCachedThreadPool() | Short-lived tasks with variable load |
SCHEDULED | ScheduledThreadPoolExecutor | Scheduled and recurring tasks |
BoxFuture — Async Work
BoxFuture<T> extends CompletableFuture<T> with fluent BoxLang helpers.
import ortus.boxlang.runtime.async.BoxFuture;
BoxFuture<String> future = asyncService.newFuture( () -> fetchFromRemote( url ) );
future
.thenApply( result -> result.toUpperCase() )
.thenAccept( result -> System.out.println( "Got: " + result ) )
.exceptionally( err -> {
logger.error( "Failed: {}", err.getMessage() );
return null;
});
String result = future.get( 10, TimeUnit.SECONDS );
BoxFuture<Void> all = asyncService.allOf(
asyncService.newFuture( () -> taskA() ),
asyncService.newFuture( () -> taskB() )
);
all.join();
BoxFuture<Object> any = asyncService.anyOf(
asyncService.newFuture( () -> serviceA() ),
asyncService.newFuture( () -> serviceB() )
);
Object first = any.join();
BoxFuture in BoxLang Script
// Using runAsync BIF
var future = runAsync( () -> {
return fetchData()
})
// Chain
var result = future
.then( (v) -> v.toUpperCase() )
.get()
// Parallel execution
var futures = [
runAsync( () -> fetchUsers() ),
runAsync( () -> fetchOrders() )
]
// Wait for all
var all = BoxFuture::allOf( futures )
all.join()
BaseScheduler — Creating a Scheduler
Extend BaseScheduler to define a scheduler with one or more tasks.
Override configure() to register tasks.
Java Scheduler
import ortus.boxlang.runtime.async.tasks.BaseScheduler;
import ortus.boxlang.runtime.async.tasks.ScheduledTask;
public class MyModuleScheduler extends BaseScheduler {
public MyModuleScheduler() {
super( "my-module-scheduler" );
}
@Override
public void configure() {
task( "cleanupExpiredSessions" )
.call( () -> sessionService.cleanExpired() )
.every( 15, TimeUnit.MINUTES );
task( "generateDailyReport" )
.call( () -> reportService.generate() )
.dailyAt( "02:00" );
task( "healthCheck" )
.call( () -> monitor.ping() )
.every( 30, TimeUnit.SECONDS )
.withNoOverlaps();
}
@Override
public void onStartup() {
this.logger.info( "MyModuleScheduler started" );
}
@Override
public void onShutdown() {
this.logger.info( "MyModuleScheduler shutting down" );
}
@Override
public void onError( ScheduledTask task, Exception e ) {
this.logger.error( "Task [{}] failed: {}", task.getName(), e.getMessage(), e );
}
@Override
public void onSuccess( ScheduledTask task, Object result ) {
this.logger.debug( "Task [{}] succeeded", task.getName() );
}
}
BoxLang Scheduler
// schedulers/MyScheduler.bx
class extends="ortus.boxlang.runtime.async.tasks.BaseScheduler" {
function configure() {
// Tasks registered via the Java task() API called through DynamicObject
task( "cleanup" )
.call( () -> cleanupExpiredData() )
.everyHour()
task( "report" )
.call( () -> generateReport() )
.dailyAt( "06:00" )
}
}
ScheduledTask — Fluent API
ScheduledTask has a rich fluent API for defining frequency and constraints.
Frequency Methods
ScheduledTask t = task( "myTask" ).call( callable );
t.every( 5, TimeUnit.MINUTES );
t.everyMinute();
t.everyHour();
t.everyDay();
t.everyWeek();
t.everyMonth();
t.startOn( LocalDateTime.now().plusHours( 1 ) );
t.withDelay( 30, TimeUnit.SECONDS );
t.dailyAt( "08:00" );
t.weeklyOn( 2, "08:00" );
t.monthlyOn( 1, "00:00" );
Constraint Methods
t.onWeekends();
t.onWeekdays();
t.onMonday();
t.onTuesday();
t.onFirstBusinessDayOfMonth();
t.onLastBusinessDayOfMonth();
t.dayOfMonth( 15 );
t.when( (task) -> systemIsReady() );
t.withNoOverlaps();
t.disable();
Callbacks
t.before( (task) -> logger.info("Starting {}", task.getName()) )
.after( (task, result) -> logger.info("Done {}", task.getName()) )
.onFailure( (task, err) -> alertTeam(err) )
.onSuccess( (task, result) -> metrics.increment("task.success") );
Registering a Scheduler in ModuleConfig.bx
// ModuleConfig.bx
class {
function onLoad() {
// Register and start a Java scheduler
var scheduler = createObject( "java", "com.example.schedulers.MyModuleScheduler" ).init()
boxRuntime.getSchedulerService().registerScheduler( scheduler )
scheduler.startup()
log.info( "MyModuleScheduler registered and started" )
}
function onUnload() {
// Schedulers are auto-shutdown by SchedulerService on runtime shutdown.
// Explicit shutdown only needed for early cleanup:
boxRuntime.getSchedulerService().removeScheduler( "my-module-scheduler" )
log.info( "MyModuleScheduler removed" )
}
}
SchedulerService API
import ortus.boxlang.runtime.services.SchedulerService;
SchedulerService schedulerSvc = BoxRuntime.getInstance().getSchedulerService();
boolean has = schedulerSvc.hasScheduler( "my-module-scheduler" );
BaseScheduler sched = schedulerSvc.getScheduler( "my-module-scheduler" );
schedulerSvc.removeScheduler( "my-module-scheduler" );
Set<String> names = schedulerSvc.getSchedulerNames();
Task Lifecycle Events (Interceptors)
These BoxEvent keys fire for every task in every scheduler:
| Event | When |
|---|
schedulerBeforeAnyTask | Before a task runs |
schedulerAfterAnyTask | After a task runs |
schedulerOnAnyTaskSuccess | Task completed without error |
schedulerOnAnyTaskError | Task threw an exception |
onSchedulerStartup | Scheduler started |
onSchedulerShutdown | Scheduler stopped |
Testing Schedulers
MyModuleScheduler scheduler = new MyModuleScheduler();
scheduler.configure();
ScheduledTask task = scheduler.getTask( "cleanupExpiredSessions" );
assertNotNull( task );
task.run();
// tests/specs/SchedulerTest.bx
class extends="testbox.system.BaseSpec" {
function run() {
describe( "MyScheduler tasks", () -> {
it( "should have the cleanup task registered", () -> {
var scheduler = new com.example.schedulers.MyModuleScheduler()
scheduler.configure()
expect( scheduler.hasTask("cleanupExpiredSessions") ).toBeTrue()
})
})
}
}
References