Async Tasks and Ownership
Background work in Pulse is bounded, cancellable, and owned by core services.
In this section
Related section pages:
Rules
Pulse core owns background work. Plugins may request long operations, but they should not start goroutines that directly mutate player, world, entity, inventory, session, storage, or runtime state.
- Fast handle methods may look synchronous, but mutation still routes through the owning service.
- Long work runs through bounded core task runners with context cancellation and queue backpressure.
- Background jobs prepare data; server-owned services apply the final state change.
- Rejected work returns typed errors such as
task.ErrQueueFullortask.ErrClosed. - Plugin scheduler tasks are for tick-timed gameplay callbacks, not uncontrolled background IO.
Core Task Runner
core/task.Runner is the low-level bounded worker pool. Submit(ctx, name, fn) accepts named work or rejects it when the queue is full or closed.
- Task handles expose
ID,Name,Done,Cancel, andAwait. Resultcarries value, error, start time, and finish time.Runner.Stats()reports queue depth/capacity, running tasks, completions, failures, cancellations, and rejects.plugin.ManagerConfigexposesTaskWorkersandTaskQueue; hosts can inspectplugin.Manager.TaskStats().
Managed Timers
core/task.TimerScheduler backs plugin scheduler callbacks with one owner-managed loop instead of one goroutine per delay.
Later(ctx, delay, fn)runs once unless canceled.Repeat(ctx, period, fn)runs until canceled.Close()cancels pending timers and stops the loop.ctx.Scheduler().Later/Repeatremains scoped to plugin unload.
Migrated Services
ctx.WorldTemplates().Copy(...) now runs through the core task runner. Completion callbacks are scheduled back through the plugin scheduler.
world-template-task.go
taskHandle, err := ctx.WorldTemplates().CopyTask(ctx, plugin.WorldTemplateCopyRequest{ TemplatePath: "worlds/templates/duel", TargetPath: "worlds/arenas/duel-1", WorldID: "duel-1",})if err != nil { return err}result := taskHandle.Await(ctx)if result.Err != nil { return result.Err}world-lifecycle-task.go
preload, err := worldHandle.PreloadChunksTask(ctx, world.Pos{X: 0, Y: 64, Z: 0}, 8)if err != nil { return err}if result := preload.Await(ctx); result.Err != nil { return result.Err}save, err := worldHandle.SaveTask(ctx)if err != nil { return err}_ = savePlayerDataProviderexposesOfflineTask,ByNameTask,ByXUIDTask, andByUUIDTaskfor async persisted-data reads.ctx.ResourcePacks()exposesRegisterFileTask,ChunkTask, andPacks()for task-backed resource-pack IO.- World generation queue saturation returns
world.ErrGenerationQueueFulland increments saturation stats instead of spawning wait goroutines.
resource-pack-task.go
packTask, err := ctx.ResourcePacks().RegisterFileTask(ctx, "packs/hub.mcpack", plugin.ResourcePackOptions{})if err != nil { return err}packResult := packTask.Await(ctx)if packResult.Err != nil { return packResult.Err}