Skip to content

Background tasks

Blocking work inside a command handler or tick callback pauses the entire simulation. Use Golem’s background task facility for database calls, file access, expensive calculations, and other work that must not delay movement or replication. The work runs on a bounded worker pool, then its completion returns to the tick goroutine where it can safely update game state.

Take an immutable snapshot of the inputs while you are on the tick goroutine. The worker owns that snapshot and writes its result into a captured variable. The completion reads the result on a later tick:

// Runs on the tick goroutine, for example inside a command handler.
battle.Phase = Settling
settlement := snapshotSettlement(battle)
var result SettlementResult
err := srv.SubmitTask(
battle.Context,
func(ctx context.Context) error {
var err error
result, err = settleTransaction(ctx, settlement)
return err
},
func(s *golem.Server, err error) {
// Back on the tick goroutine. Server and entity mutation is safe here.
if err != nil {
markSettlementForRetry(battle)
return
}
applyCommittedResult(result)
finishBattle(battle)
},
)
if err != nil {
// No completion will run. Keep the battle pending for application-owned retry.
battle.Phase = SettlementPending
if !errors.Is(err, golem.ErrTaskQueueFull) {
log.Printf("submit settlement: %v", err)
}
}

The worker must not read or mutate entities, the registry, visibility state, sessions, world data, or other state owned by the tick goroutine. Pass immutable values or caller-owned data into the closure instead. The worker-to-completion handoff makes captured result memory visible before the completion runs.

SubmitTask never waits for worker or queue capacity. It returns ErrTaskQueueFull immediately when the pending-work queue is full. A rejected task has no completion, so keep the operation pending and retry later rather than blocking the tick.

Use Post when another goroutine already produced the result and only needs to schedule game-state work:

func publishMatchResult(srv *golem.Server, result MatchResult) {
err := srv.Post(func(s *golem.Server) {
applyMatchResult(result)
})
if err != nil {
// Retain result; queue-full retries and shutdown handling are application policy.
retainMatchResult(result, err)
}
}

Post is safe from any goroutine, returns immediately, and never invokes the callback inline. Accepted callbacks run at most once during a later async drain. A full Post queue returns ErrPostQueueFull.

The defaults are finite and suit moderate blocking workloads:

srv := golem.NewServer(golem.ServerConfig{
TaskWorkers: 4,
TaskQueueCapacity: 256,
TaskCompletionQueueCapacity: 256,
PostQueueCapacity: 1024,
AsyncCallbacksPerTick: 256,
})

TaskWorkers limits concurrent work. TaskQueueCapacity limits work waiting for a worker. TaskCompletionQueueCapacity limits finished work waiting for the tick. PostQueueCapacity limits external callbacks. AsyncCallbacksPerTick is the combined completion and Post budget for one tick.

Zero selects each documented default. Negative task or callback limits panic during NewServer. Completions and Posts use separate queues, then share a round-robin tick budget so sustained traffic from one queue does not starve the other. When the completion queue fills, finished workers wait for capacity or shutdown; the tick goroutine does not wait.

Work receives a context that ends when either its caller context or the server Run context ends. Work functions should pass that context into blocking APIs and return promptly when it is cancelled.

A non-nil work error reaches the completion unchanged. If work returns nil after its effective context ends, the completion receives the context error. Worker panics are recovered and logged; while the server remains running, the completion receives an error that matches golem.ErrTaskPanicked with errors.Is.

Post and SubmitTask return ErrServerNotRunning before Run, after shutdown begins, and after Run returns. Run is single-use; another call returns ErrServerAlreadyRun.

Shutdown stops accepting work, cancels running tasks, skips queued work, waits for cooperative workers, and discards queued completions and Posts. No callback runs after the tick loop exits. A work function that ignores cancellation can delay Run indefinitely. Completion and Post callbacks keep normal tick-callback panic behavior, so their panics are not recovered by this facility.

Each tick runs in this order:

  1. OnTickStart
  2. Background task completions and Posts, up to AsyncCallbacksPerTick
  3. Session events and command dispatch
  4. Entity ticks
  5. OnTick
  6. Collision
  7. Replication flush
  8. OnTickEnd

This order makes a completed durable operation visible before the next batch of player commands. Excess callbacks stay queued for later ticks, preventing a completion storm from consuming the whole simulation tick. See Game loop for the complete loop lifecycle.

srv.TaskStats() returns cumulative counters plus approximate live gauges. Use TasksAccepted, TasksRejectedFull, TasksFinished, TaskCompletionsExecuted, TasksCancelled, and TaskPanics to track work. PostsAccepted, PostsExecuted, and PostsRejectedFull track direct Posts. QueuedTasks, RunningTasks, QueuedCompletions, and QueuedPosts show current pressure.

The fields are sampled independently, so do not treat one result as a transactionally consistent snapshot. In particular, finished work can exceed executed completions while callbacks wait for a tick or are discarded during shutdown.

Retries, idempotency, transaction boundaries, and ordering between related operations remain application policy.