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.ErrQueueFull or task.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, and Await.
  • Result carries value, error, start time, and finish time.
  • Runner.Stats() reports queue depth/capacity, running tasks, completions, failures, cancellations, and rejects.
  • plugin.ManagerConfig exposes TaskWorkers and TaskQueue; hosts can inspect plugin.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/Repeat remains 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}_ = save
  • PlayerDataProvider exposes OfflineTask, ByNameTask, ByXUIDTask, and ByUUIDTask for async persisted-data reads.
  • ctx.ResourcePacks() exposes RegisterFileTask, ChunkTask, and Packs() for task-backed resource-pack IO.
  • World generation queue saturation returns world.ErrGenerationQueueFull and 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}