This guide covers the normal Workerkit path: create a runtime, register workers, start them, inspect status, expose commands when useful, and shut down cleanly.
For symbol-level details, see api.md.
Most Workerkit programs follow this shape:
- Create one
Runtimefor a service boundary. - Register one or more workers.
- Start the runtime.
- Inspect status or dispatch worker-owned commands.
- Drain and shut down gracefully.
runtime, err := workerkit.New(workerkit.Identity{Name: "search"})
if err != nil {
return err
}
err = runtime.Register(workerkit.WorkerSpec{
Name: "index",
Description: "maintains the search index",
Worker: indexWorker,
})
if err != nil {
return err
}
if err := runtime.StartAll(ctx); err != nil {
return err
}
defer func() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = runtime.Shutdown(shutdownCtx)
}()A runtime represents one service boundary. It is the aggregate operational object for workers that should share lifecycle, status, readiness, command admission, retry policy, concurrency limits, failure policy, and observers.
Use multiple runtimes only when you truly want separate operational boundaries. Most services should start with one runtime.
A Workerkit worker is ordinary Go code behind a small lifecycle interface:
type Worker interface {
Start(context.Context) error
Stop(context.Context) error
}Start should begin the worker's domain work or initialize the resources it
owns. Stop should release those resources and stop the worker's activity.
Workerkit does not own the domain model, storage, queue, broker, scheduler, or business rules. It owns the runtime envelope around the worker.
Workers are registered before startup:
err := runtime.Register(workerkit.WorkerSpec{
Name: "ingest",
Description: "consumes source records",
Worker: ingestWorker,
})Worker names are local inside the runtime. Workerkit qualifies them as
runtime/worker in status, telemetry, and operations surfaces.
Registration order matters. StartAll starts workers in registration order.
StopAll stops workers in reverse registration order.
The common lifecycle methods are:
Startstarts one worker.StartAllstarts registered workers in registration order.Drainmarks one worker unready and not accepting new Workerkit commands.DrainAlldrains running workers in registration order.DrainAllBestEffortattempts to drain all running workers and joins errors.WaitIdlewaits for one worker to have no in-flight commands.WaitAllIdlewaits for runtime-wide command idleness.Stopstops one worker and closes admission for that worker only.StopAllcloses runtime-wide command admission and stops workers in reverse registration order.Shutdowncloses runtime-wide command admission, drains all workers best-effort, waits for runtime idle, then stops.
Lifecycle mutations are serialized per runtime. Concurrent calls wait for the active lifecycle operation, and that wait counts against their context deadline. Command dispatch and status reads remain concurrent with lifecycle operations. Dispatch is gated by any explicit runtime-wide cutoff plus the target worker's lifecycle and accepting-work state.
For the full lifecycle model, read lifecycle.md.
Running does not imply ready.
Workers can start, warm up, and then call WorkerRuntime.SetReady(true) through
the worker-scoped runtime handle:
func (w *worker) Start(ctx context.Context) error {
workerRuntime, ok := workerkit.WorkerRuntimeFromContext(ctx)
if !ok {
return errors.New("worker runtime missing")
}
workerRuntime.SetReady(false)
// warm up local state
workerRuntime.SetReady(true)
return nil
}Aggregate runtime readiness is derived from readiness-contributing workers.
Workers can opt out with WithWorkerReadinessContribution(false).
Workers can register domain commands:
err := runtime.Register(spec,
workerkit.WithCommand("refresh", workerkit.CommandHandlerFunc(refresh)),
)Commands are not lifecycle controls. They are worker-owned domain operations routed by Workerkit.
Use Runtime.Commands for discovery and Runtime.Dispatch for direct
execution. The command payload and result payload are raw bytes, so the worker
owns the contract.
Read commands.md for the full command model.
Use NewLoopWorker when the domain work is a long-running loop:
worker := workerkit.NewLoopWorker(func(ctx context.Context, runtime workerkit.WorkerRuntime) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
// do domain work
}
}
})This gives the loop managed lifecycle and cancellation instead of leaving it as an unmanaged goroutine.
Use Status for aggregate runtime status:
status := runtime.RuntimeStatus()
fmt.Println(status.State, status.Ready, status.InFlight)Use Workers or Worker for worker inspection:
for _, worker := range runtime.Workers() {
fmt.Println(worker.QualifiedName, worker.Status.State, worker.Status.Ready)
}Status snapshots separate lifecycle, readiness, command admission, in-flight work, last lifecycle transition, last worker failure, and last command failure.
Runtime options set defaults:
WithReadinessPolicyWithRuntimeCommandConcurrencyWithDefaultStartTimeoutWithDefaultStopTimeoutWithDefaultCommandTimeoutWithDefaultStartRetryWithDefaultCommandRetryWithDefaultWorkerCommandConcurrencyWithDefaultPanicPolicyWithDefaultFailurePolicyWithDefaultReadyOnStartWithDefaultAcceptingWorkOnStartWithDefaultWorkerReadinessContributionWithObserver
Worker options override runtime defaults for one registered worker:
WithWorkerStartTimeoutWithWorkerStopTimeoutWithWorkerCommandTimeoutWithWorkerStartRetryWithWorkerCommandRetryWithWorkerCommandConcurrencyWithWorkerPanicPolicyWithWorkerFailurePolicyWithWorkerReadyOnStartWithWorkerAcceptingWorkOnStartWithWorkerReadinessContributionWithCommandWithCommandSpec
Start with direct runtime usage and tests. Add worker commands only when the worker has real domain operations worth exposing. Add retry and concurrency policy where repeated work is safe and overload needs backpressure.
When Workerkit is part of an HTTP service, register the runtime with the shared
Opskit registry and pass that registry to Servekit for /readyz and generic
admin inspection. Optionally use servekitservice.New to coordinate Workerkit
startup and shutdown around the application-owned server. Use opshttp only
when you need Workerkit-specific HTTP command dispatch or privileged lifecycle
controls.