call list produced by the Call Builder into aggregated outputs. It is the Executor-phase implementation of the Worker and corresponds to 4.2.4 Call execution subsystem in the Architecture overview. For the Python Executor process itself, see Python Executor; this page treats it only as the UDS peer.
Responsibilities and boundaries
Responsible for:- Scheduling calls from multiple tasks with one Worker-global
DispatcherCore, admitting work fairly per task / instance. - Selecting an Executor for top-level calls via
Filter -> Score; subfunction calls are always bound back to the Executor running the parent call. - Call result caching and singleflight deduplication within a task.
- Handling call suspension (IO / subfunction / source fetch) and resumption, and forwarding IO requests to the IO access subsystem.
- Managing the Python Executor process pool (spawn / restart / sandbox) and maintaining a long-lived UDS connection to each Executor.
- Aggregating the outputs of successful root calls into
outputs []anyand handing them back toWorkerCorefor the Plugin phase.
- Generating tasks, allocating slots, or placing Workers (owned by the Coordinator and the Worker).
- Function source snapshots and auditing themselves (owned by Function Code View); this page only consumes
TaskFunctionView.ResolveForDispatch. - Real BlockDB / RPC access (owned by the IO subsystem); Executors never connect to external dependencies directly.
DispatcherCoreis a Sans-IO, pure in-memory state machine with a single writer: only the dispatch actor goroutine (Orchestrator.RunDispatchLoop) may mutate it; other goroutines can only post adispatchEvent. The lock invariant is enforced byinternal/worker/adapters/actor_invariants_test.go.- A call maps to exactly one
DispatchCallContextfor its whole lifetime; retries reuse the samecallIDand only incrementAttemptCount. DONEis the only terminal state;setCallTerminalis the single accounting point and is idempotent when called repeatedly.- Subfunction calls have their own
DispatchCallContextwithMaxAttempts = 1; they never enterTotalCount/outputsand only return their result to the parent call throughResumeCall. - Subcall IDs are generated from a process-level monotonic
subcallSeq({parent}/sub-{n}) and are globally unique within the worker process. - The Worker side has no per-call timer: the execution budget
CallBudgetMsis enforced locally by the Executor, and the outer wall-clock protection is the task deadline plus the wedge backstop. - The adapter reservation is released as soon as the Executor’s terminal frame arrives (transport truth), and this is idempotent with the dispatcher’s
UnbindCall.
Code location
Core types and interfaces
core.DispatcherCore(internal/worker/core/dispatcher.go): the global scheduler. Its input is theHandleXxxevent methods; its output is[]DispatchCommand.core.DispatchCallContext(dispatcher_types.go): the scheduling carrier for a single call, holdingState,ExecutorID,AttemptCount,BudgetMs,IsSubcall,BatchIdx,ArgsJSON.core.TaskDispatchState(dispatcher_types.go): per-task state, holdingReadyQueue,InflightCalls,CallCache,Singleflight,PendingBindSubcalls,ChildSubcalls, plus the streaming-production fields (ProductionOpen,BatchRemaining,PendingSettle,CollectedOutputs).core.ExecutorSnapshot(dispatcher_types.go): the Executor capacity view provided by the adapter, holdingHealthy,RunnableCount,UsableContexts,HeartbeatSeq,RunningCallID,LastSchedulerActiveAtMs.core.DispatchCommand(dispatcher_types.go): a sealed interface implemented byBindCall,UnbindCall,CancelCallCmd,DispatchCallCmd,ArmCallRetry,ConvergeTaskCalls,CompleteCallFromCache,BatchSettled,StopCallProduction.core.DispatcherCore.TryDispatchNext / tryDispatchNextFair: one admission decision;selectExecutorperformsFilter -> Score.core.DispatcherCore.PrepareCallPhaseWithDigests + CommitCallPhase,CommitCallStream + PrepareCallBatchWithDigests + CommitCallBatch: the one-shot and streaming call registration entry points;Prepare*has no side effects and can run off-actor.core.DispatcherCore.FlushDirtyTasks(dispatcher_internal.go): the convergence epilogue; grouped by task, it emitsStopCallProduction -> drain -> BatchSettled -> ConvergeTaskCalls.adapters.ExecutorAdapter(exec_adapter.go): the UDS adapter interface the orchestrator depends on.
adapters.Orchestrator.executeDispatchCommands(orchestrator_dispatch.go): the command executor.Bind/Unbindrun synchronously on the actor;SendExecuteCall/SendResumeCall/SendCancelCallalways run in an off-actor goroutine.executor.IOHandler,executor.SubcallHandler,executor.FunctionCodeResolver(adapters/executor/adapter.go): the three orchestrator entry points the adapter calls back into after receivingCallWaiting.executor.Adapter(adapter.go): oneexecutorConnper Executor, holdingreservations,draining,dispatchedAttempts,usableContexts;Snapshots()is derived from these.executor.PoolManager,PoolConfig,SlotState(pool.go): the process slot state machineRUNNING / WAITING / BACKOFF / STOPPED;executorSpawnerabstracts over bare process and sandbox.uds.MessageEnvelopeand the payload structs (api/uds/types.go):ExecuteCallPayload,ResumeCallPayload,CancelCallPayload,CallWaitingPayload,CallCompletedPayload,CallFailedPayload,HeartbeatPayload.
Data flow / execution flow
Interaction model: Executor reader goroutines / timers / gRPC post facts asdispatchEvents (evCallCompleted, evCallWaiting, evSubcallRequest, evExecutorSnapshot, etc.); the actor calls DispatcherCore.HandleXxx to obtain commands; executeDispatchCommands executes them; every event ends with flushDispatchEpilogue.
Key steps:
1
Register calls
runCallPhase (orchestrator_phases.go) resolves each call’s functionCodeDigest by taskCodeEpoch on the task activation goroutine, calls PrepareCallPhaseWithDigests, then posts evCallPhaseStarted so the actor runs CommitCallPhase. Streaming builders go through CommitCallStream plus one CommitCallBatch per batch; the producer is backpressured by the StreamBuildConfig.MaxOutstandingBatches credit window, and BatchSettled returns one credit.2
Pick a call
TryDispatchNext takes the call at the head of ReadyQueue: it first checks CallCache (a hit goes straight to CompleteCallFromCache), then Singleflight (consecutive calls with the same key are registered as waiters in one pass); otherwise selectExecutor picks an Executor, the call is registered as the singleflight leader, set to RUNNING, AttemptCount++, BudgetMs = CallDeadlineMs, and BindCall is emitted.3
Pick an Executor (Filter -> Score)
selectExecutor filters out: !Healthy; scheduler stalled for longer than SchedulerStallTimeoutMs; headroom = UsableContexts - localInflight <= 0; compensated runnable >= ExecutorMaxRunnable; localInflight >= ExecutorMaxInflight; MemoryBytes >= ExecutorMemoryHighWatermark. Scoring: lower runnable first, then lower localInflight, then higher headroom, and finally ExecutorID in lexicographic order. When ExecutorSelectionSampleSize > 0, only the first N eligible candidates are scanned (a power-of-d approximation).4
Bind and dispatch
BindCall calls Adapter.Bind synchronously on the actor (subcalls go through BindSubcall, which passes as long as the parent holds a live reservation); on success, HandleExecutorBound emits DispatchCallCmd. The adapter uses resolveFunctionCodeForDispatch to validate (functionID, digest) against the task’s pinned view and fetch the EntrySelector, assembles ExecuteCallPayload (digest only, no source), then in a new goroutine passes the audit gate, performs alive / draining checks, and calls SendExecuteCall. A bind failure emits ArmCallRetry(now) for a root call without consuming attempt budget; a subcall is parked back into PendingBindSubcalls.5
Suspend and resume
The Executor sends
CallWaiting. waitKind=function_code does not change dispatcher state; the adapter returns the source directly via FunctionCodeResolver. io / subcall trigger HandleCallWaiting, and the adapter calls IOHandler.HandleIO (locating the runtime by the TaskID echoed in the payload, without consulting the dispatcher) or SubcallHandler.HandleSubcall in a processWaitRequest goroutine. The result goes through SendResumeCall, which blocks until the frame is actually flushed before HandleCallResumed fires; if the send fails, a resume_send_failed failure event is synthesized for the parent call (retryable, carrying the attempt sequence number).6
Terminal state and convergence
When
CallCompleted / CallFailed reaches the adapter, it first releases the reservation via releaseOnTerminal, then calls back into applyCallCompleted / applyCallFailed. The core writes CallCache, resolves singleflight waiters, cancels the call’s subcall subtree, and calls markTaskDirty. flushDispatchEpilogue loops over FlushDirtyTasks until quiescent: when TerminalRootCount == TotalCount (and, for streaming, ProductionDone), it emits ConvergeTaskCalls, which the adapter translates into WorkerCore.HandleTaskPhaseFinished(PhaseCalls).Subfunction call path
- The SDK inside the Executor sends
CallWaiting(waitKind=subcall, functionId, args, ancestry, grantedBudgetMs). HandleSubcallpostsevSubcallRequest;applySubcallRequestvalidates on the actor: depthlen(ancestry) < MaxSubcallDepth, cycle detection (comparingCacheKeyForJSONagainst ancestor keys), and the requesting executor must equal the executor the parent call is currently bound to (rejecting late requests from an old attempt).resolveFunctionCodeForDispatchfetches the subfunction digest;AddSubcallJSONcreates the childDispatchCallContext(inheriting the parent’sBatchIdx, withBudgetMs = grantedBudgetMs), recordspendingSubcalls[childID], andTryBindSubcallToParentbinds it back to the parent Executor. If the parent is unbound or the parent Executor is unhealthy, the subcall is parked inPendingBindSubcallsand retried onHandleExecutorBound(parent)orHandleExecutorSnapshotUpdated.- After the child call reaches a terminal state,
sendSubcallResumereturns the result or asubcallError(includingretryable) to the parent Executor viaResumeCall;BudgetUsedMslets the parent deduct budget. Subcalls that hit the cache / singleflight go throughCompleteCallFromCacheand resume the parent directly. - When the parent call reaches a terminal state (including a failed attempt that is retried),
finalizeSubcallSubtreerecursively cancels the whole subtree and emitsCancelCallCmd; the adapter marks the reservation as draining and callsSendCancelCallbest-effort.
Executor Pool and connections
PoolManager.Startspawns onepython -m blockx_executor --executor-id exec-N --socket-path ... --executor-max-inflight ... --executor-max-runnable ...per slot. After the process exits,waitLooprestarts it afterRestartDelayMs; more thanMaxRestartsrestarts withinRestartWindowMsputs the slot intoBACKOFFforBackoffMs.KillExecutoris used by the wedge backstop for hard kills.- With
ExecutorSpawnMode=sandbox,sandboxSpawnerstarts a containerd container throughgithub.com/Chaintable/emulator/sandbox: no network, only the UDS socket directory mounted, only thechildEnvKeysallowlist of environment variables passed through, and optional CPU / memory limits. Seedocs/specs/executor-sandbox-isolation.mdin the blockx repo for the design. - The Executor actively connects to
Adapter.SocketPath()(<SocketDir>/blockx-worker-<pid>.sock); the first validHeartbeatcompletes the attach, and there is no explicit registration message. Each connection has one reader goroutine and one writer goroutine; the writer takes frames from theoutbox(depth 256), coalescing up to 64 frames per sharedFlush. ExecuteCallenqueues without blocking and returns a retryable error when the outbox is full;ResumeCall/CancelCallblock until the flush completes, because dispatcher state progression depends on the frame actually arriving.- 500ms tick:
SyncExecutorSnapshotsfeedsAdapter.Snapshots()intoHandleExecutorSnapshotUpdated;CheckWedgedExecutorshard-kills Executors whoseRunningCallID != ""and whoseLastSchedulerActiveAtMshas stalled for longer thanSchedulerStallTimeoutMs;CheckHeartbeatTimeoutsdeclares connections with no heartbeat for more thanHeartbeatTimeoutMsas transport lost, triggeringonExecutorLost→applyExecutorLost, which first refreshes the snapshot and then emits a retryable failure for every inflight call on that Executor.
UDS protocol
Frame format: a 4-byte big-endian length prefix followed by a JSONMessageEnvelope; when a binary sidecar is present, the body starts with the BXB1 magic plus a 4-byte JSON length, with the raw bytes immediately following the JSON (BlockDB protobuf takes this path to avoid base64). A single frame is capped at 16 MiB. Encoding/decoding uses sonic, and json.RawMessage fields are spliced in verbatim.
State and lifecycle
DispatchCallContext.State has only four states (CallReady / CallRunning / CallWaiting / CallDone). The Executor’s internal fine-grained runnable / greenlet states are not surfaced to the Worker.
- Retry and backoff:
handleAttemptFailuresetsREADYwhenretryable && AttemptCount < MaxAttempts && !StopDispatching;computeBackoffemitsArmCallRetryusingbase * 2^(attempt-1)plus up to 5% jitter; the adapter usestime.AfterFuncto postevRetryReadywhen it fires. The base is taken from the task’sCallRetryPolicy.BaseBackoffMswhen set, otherwise fromDispatcherConfig.BaseBackoffMs. - Stale events:
CallFailureStaledrops failure events when the call isREADY && AttemptCount > 0(backing off) or theattemptSeqdoes not match; a call that is alreadyDONEignores every late terminal event. - Failure policy: under
CallFailureFastFail, the final failure of any root call triggersmarkStopDispatching, after which no further calls are dispatched, anddrainCancelledCallscancels and converges all remaining calls once inflight drops to zero. - Budget: each root-call attempt gets
BudgetMs = CallDeadlineMs(default 5000ms, 0 means unlimited), counting only CPU + IO backend + subcalls + sleep, not queueing time; subcalls receive thegrantedBudgetMscarved out by the parent. - Reservation lifecycle (adapter):
Bindadds toreservations; a normal terminal state callsUnbind; the cancel path callsMarkDrainingand waits for the Executor’s terminal frame (releaseOnTerminal) before releasing; a reconnect era clears all reservations. - Output retention: when
MaxCollectedOutputBytes > 0and the accumulated output exceeds the limit, the task stops production withOUTPUT_BYTES_EXCEEDEDand discards its outputs.
Configuration
Frominternal/worker/app/config.go (WorkerFullConfig) and internal/worker/core/config.go (DispatcherConfig). See internal/worker/app/app.go for environment variable overrides.
Extension points
- Changing the scheduling policy: edit Filter / Score in
selectExecutor/betterExecutorCandidate(dispatcher_internal.go); edit the fairness model intryDispatchNextFair(dispatcher.go). Rundispatcher_select_bench_test.goanddispatcher_fairness_bench_test.goto check for regressions. - Adding a core event or command: add a
DispatchCommandimplementation indispatcher_types.goand a branch inexecuteDispatchCommandsinorchestrator_dispatch.go; for a new event, add anevXxxtype andapplyXxxinorchestrator_actor.go, and make suredispCoreis only mutated on the actor (actor_invariants_test.gowill catch violations). Any entry point that can affect convergence must callmarkTaskDirty; convergence only happens insideFlushDirtyTasks. - Adding a UDS message type or payload field: add the constant and struct in
api/uds/types.go, add routing inhandleMessageinadapters/executor/adapter.go, and add a handler inhandlers.go; update the Python-side protocol andapi/uds/codec_test.goin step. - Adding a
waitKind: add a branch inhandleCallWaiting/processWaitRequestinhandlers.go; decide whether it enters the dispatcher’sWAITINGstate (function_codedoes not). - Adding an Executor spawn mode: implement
executorSpawnerandexecutorProcess(spawner.go), register it innewExecutorSpawner, and add validation insandbox_config.go. - Adjusting Executor heartbeat fields:
uds.HeartbeatPayload→handleHeartbeat/heartbeatUpdateExistinginhandlers.go→core.ExecutorSnapshot.
Testing
- Core tests are split by topic:
dispatcher_test.go(basic scheduling),dispatcher_subcall_test.go,dispatcher_stream_test.go(streaming production / batch settle / output cap),dispatcher_instance_fairness_test.goanddispatcher_fairness_quantum_test.go,retry_backoff_test.go,dispatcher_stale_failure_test.go,dispatcher_wedge_test.go. - Adapter tests inject send failures and similar paths through an
ExecutorAdapterstub:orchestrator_dispatch_test.go,orchestrator_subcall_test.go,orchestrator_send_failure_test.go,orchestrator_capacity_failure_test.go. adapters/executor/tests start a fake executor over a real UDS socket:adapter_test.go,pool_test.go,resume_pool_test.go,write_coalesce_test.go,payload_buffer_test.go.api/uds/codec_test.goandbench_test.gocover frame encoding/decoding and the binary sidecar.- E2E tests live in
cmd/worker/worker_process_*_test.go; see Testing anddocs/specs/worker-test-organization.mdin the blockx repo for the test organization conventions.
Related docs
Specs in the blockx repo:docs/specs/call-execution-subsystem.md: the design of this subsystem (primary), covering Filter/Score, retries, subcalls, and streaming production.docs/specs/worker-executor-connection-and-python-sdk-hook.md: connection attach, failure detection, the UDS message contract, and the SDK hook.docs/specs/architecture.md§4.2.4: where the subsystem sits in the overall architecture.docs/specs/2026-07-23-execute-call-function-code-reference.md:ExecuteCallcarries only the digest;waitKind=function_codefetches source on demand.docs/specs/2026-07-30-python-executor-cpu-optimization.md: the lasting impact is that BlockDB requests / responses use theBXB1binary sidecar, and the Worker uses the raw bytes as the IO cache key.docs/specs/executor-sandbox-isolation.md: the containerd sandbox spawn mode.