Skip to content

Command Dispatch

The dispatch system routes commands from processes to handlers. Processes yield commands with correlation tags, handlers execute async work, and results flow back via event queues.

sequenceDiagram
participant P as Process
participant W as Worker
participant R as Registry
participant H as Handler
P->>W: yield(command, tag)
W->>R: getHandler(cmdID)
R-->>W: handler
W->>H: Handle(cmd, tag, receiver)
H-->>H: async work
H->>W: CompleteYield(tag, result)
W->>P: queue event, wake
P->>P: resume with result

The registry stores handlers in a hybrid structure:

type Registry struct {
handlers [256]Handler // System commands: O(1) index
extended map[CommandID]Handler // Extended commands: map lookup
frozen atomic.Bool // Lock-free after boot
}

System commands (0-255) use array indexing. Extended commands use map lookup. After Freeze(), all lookups are lock-free.

RangeModuleExamples
1-9processSend, Spawn, Terminate, Cancel, Monitor, Unmonitor, Link, Unlink, Exec
10-29clockSleep, Ticker, Timer
30-39socketConnect, Listen, Accept, Bind, Resolve
50-59streamRead, Write, Close, Seek
60-69httpRequest, RequestBatch
70-79ttyterminal I/O
80-89websocketConnect, Send, Receive
90-99eventSubscribe, Send
100-119sqlQuery, Execute, Prepare, Stmt, Tx ops
120-129storeGet, Set, Delete, Has
130-139securityValidateToken, CreateToken
140-149functionCall, AsyncStart, AsyncCancel
150-159execProcessWait
160-169cloudstorageUpload, Download, List, Presigned URLs
170-179evalCompile, Run
180-189workflowSideEffect, Exec, Version, UpsertAttrs
190-199contractOpen, Call, AsyncCall, AsyncCancel
200-211pg (process group)Join, Leave, GetMembers, GetLocalMembers, WhichGroups, Broadcast, BroadcastLocal, WhichLocalGroups, Monitor, Events, JoinGroups, LeaveGroups
256+customUser-defined services

Registration happens during boot via MustRegisterCommands(). Collisions panic at startup.

Commands are data structures with a unique CommandID:

const MyCommand dispatcher.CommandID = 256
type MyCmd struct {
Input string
Option int
}
var myCmdPool = sync.Pool{New: func() any { return &MyCmd{} }}
func (c *MyCmd) CmdID() dispatcher.CommandID { return MyCommand }
func (c *MyCmd) Release() {
c.Input = ""
c.Option = 0
myCmdPool.Put(c)
}

Pool reuse eliminates allocation in hot paths. Register at package init:

func init() {
dispatcher.MustRegisterCommands("myservice", MyCommand)
}

A dispatcher groups related handlers. It implements RegisterAll to register handlers and lifecycle methods for setup/teardown:

type Handler interface {
Handle(ctx context.Context, cmd Command, tag uint64, receiver ResultReceiver) error
}
type ResultReceiver interface {
CompleteYield(tag uint64, data any, err error)
}
type Dispatcher struct {
// service state
}
func (d *Dispatcher) RegisterAll(register func(id dispatcher.CommandID, h dispatcher.Handler)) {
register(myapi.MyCommand, dispatcher.HandlerFunc(d.handleMyCommand))
}
func (d *Dispatcher) handleMyCommand(ctx context.Context, cmd Command, tag uint64, receiver ResultReceiver) error {
c := cmd.(*myapi.MyCmd)
go func() {
result := doWork(c)
if ctx.Err() == nil {
receiver.CompleteYield(tag, result, nil)
}
}()
return nil
}

Register as a boot component:

func MyDispatcher() boot.Component {
return boot.New(boot.P{
Name: "dispatcher.myservice",
DependsOn: []boot.Name{DispatcherName},
Load: func(ctx context.Context) (context.Context, error) {
reg := dispatcher.GetRegistrar(ctx)
svc := myservice.NewDispatcher()
svc.RegisterAll(reg.Register)
return ctx, nil
},
})
}

When a process needs async work, it yields a command with a correlation tag:

type Yield struct {
Cmd Command
Tag uint64 // Process-local counter for correlation
}

The worker extracts yields from StepOutput after each step and dispatches them to handlers. Each tag uniquely identifies the request so results can be matched back.