Skip to main content
Call 执行子系统位于 Worker 进程内部,负责把 Call Builder 产出的 call list 变成聚合后的 outputs。它是 Worker 的 Executor 阶段实现,在 系统架构总览 中对应 4.2.4 Call 执行子系统。Python Executor 进程本身见 Python Executor,本页只把它当作 UDS 对端。

职责与边界

负责:
  • 用一个 Worker 全局的 DispatcherCore 调度多个 task 的 call,按 task / instance 公平放量。
  • 为顶层 call 做 Filter -> Score 选 Executor;子函数调用固定绑回父 call 所在 Executor。
  • 在 task 内做 call result cache 与 singleflight 去重。
  • 处理 call 的挂起(IO / 子函数 / 源码拉取)与恢复,把 IO 请求转交 IO 访问子系统
  • 管理 Python Executor 进程池(spawn / 重启 / 沙箱),并维护与每个 Executor 的 UDS 长连接。
  • 汇聚成功根 call 的输出为 outputs []any,交回 WorkerCore 进入 Plugin 阶段。
不负责:
  • 生成 task、分配 slot、Worker 选址(归 Coordinator 与 Worker)。
  • 函数源码快照与审计本身(归 Function Code View),本页只消费 TaskFunctionView.ResolveForDispatch
  • 真实 BlockDB / RPC 访问(归 IO 子系统),Executor 不直连外部依赖。
核心不变量:
  • DispatcherCore 是 Sans-IO 纯内存状态机,单写者:只有 dispatch actor goroutine(Orchestrator.RunDispatchLoop)能改它;其他 goroutine 只能投递 dispatchEvent。锁不变量由 internal/worker/adapters/actor_invariants_test.go 强制。
  • 一个 call 整个生命周期只对应一个 DispatchCallContext;retry 复用同一 callID,只递增 AttemptCount
  • DONE 是唯一终态;setCallTerminal 是唯一记账点,重复调用幂等。
  • 子函数调用有独立的 DispatchCallContextMaxAttempts = 1,不进入 TotalCount / outputs,只把结果通过 ResumeCall 回送父 call。
  • 子调用 ID 由进程级单调 subcallSeq 生成({parent}/sub-{n}),worker 进程内全局唯一。
  • Worker 侧没有 per-call 定时器:执行预算 CallBudgetMs 由 Executor 本地强制,墙钟外层保护是 task deadline 加 wedge backstop。
  • Executor 终态帧到达即释放 adapter 预留(transport truth),与 dispatcher 的 UnbindCall 幂等。

代码位置

核心类型与接口

  • core.DispatcherCoreinternal/worker/core/dispatcher.go):全局调度器。输入是 HandleXxx 事件方法,输出是 []DispatchCommand
  • core.DispatchCallContextdispatcher_types.go):单个 call 的调度载体,含 StateExecutorIDAttemptCountBudgetMsIsSubcallBatchIdxArgsJSON
  • core.TaskDispatchStatedispatcher_types.go):per-task 状态,含 ReadyQueueInflightCallsCallCacheSingleflightPendingBindSubcallsChildSubcalls,以及流式生产字段(ProductionOpenBatchRemainingPendingSettleCollectedOutputs)。
  • core.ExecutorSnapshotdispatcher_types.go):adapter 提供的 Executor 容量视图,含 HealthyRunnableCountUsableContextsHeartbeatSeqRunningCallIDLastSchedulerActiveAtMs
  • core.DispatchCommanddispatcher_types.go):sealed 接口,实现有 BindCallUnbindCallCancelCallCmdDispatchCallCmdArmCallRetryConvergeTaskCallsCompleteCallFromCacheBatchSettledStopCallProduction
  • core.DispatcherCore.TryDispatchNext / tryDispatchNextFair:一次放量决策;selectExecutorFilter -> Score
  • core.DispatcherCore.PrepareCallPhaseWithDigests + CommitCallPhaseCommitCallStream + PrepareCallBatchWithDigests + CommitCallBatch:一次性与流式两种 call 注册入口;Prepare* 无副作用可 off-actor 运行。
  • core.DispatcherCore.FlushDirtyTasksdispatcher_internal.go):收敛 epilogue,按 task 分组产出 StopCallProduction -> 排空 -> BatchSettled -> ConvergeTaskCalls
  • adapters.ExecutorAdapterexec_adapter.go):orchestrator 依赖的 UDS adapter 接口。
  • adapters.Orchestrator.executeDispatchCommandsorchestrator_dispatch.go):命令执行器。Bind/Unbind 在 actor 上同步执行;SendExecuteCall/SendResumeCall/SendCancelCall 一律 off-actor goroutine。
  • executor.IOHandlerexecutor.SubcallHandlerexecutor.FunctionCodeResolveradapters/executor/adapter.go):adapter 收到 CallWaiting 后回调 orchestrator 的三个入口。
  • executor.Adapteradapter.go):每个 Executor 一个 executorConn,持有 reservationsdrainingdispatchedAttemptsusableContextsSnapshots() 由此派生。
  • executor.PoolManagerPoolConfigSlotStatepool.go):进程 slot 状态机 RUNNING / WAITING / BACKOFF / STOPPEDexecutorSpawner 抽象 bare process 与 sandbox。
  • uds.MessageEnvelope 与 payload 结构(api/uds/types.go):ExecuteCallPayloadResumeCallPayloadCancelCallPayloadCallWaitingPayloadCallCompletedPayloadCallFailedPayloadHeartbeatPayload

数据流 / 执行流程

交互模式:Executor reader goroutine / 定时器 / gRPC 把事实投递为 dispatchEventevCallCompletedevCallWaitingevSubcallRequestevExecutorSnapshot 等);actor 调 DispatcherCore.HandleXxx 得到命令;executeDispatchCommands 执行命令;每个事件结束都走 flushDispatchEpilogue 关键步骤说明:
1

注册 call

runCallPhaseorchestrator_phases.go)在 task 激活的 goroutine 上按 taskCodeEpoch 解析每个 call 的 functionCodeDigest,调用 PrepareCallPhaseWithDigests,再投递 evCallPhaseStarted 让 actor CommitCallPhase。流式 builder 走 CommitCallStream + 逐批 CommitCallBatch,生产者受 StreamBuildConfig.MaxOutstandingBatches 信用窗口背压,BatchSettled 归还一个信用。
2

选 call

TryDispatchNextReadyQueue 队首取 call:先查 CallCache(命中直接 CompleteCallFromCache),再查 Singleflight(同 key 连续 call 一次性登记为 waiter),否则 selectExecutor 选 Executor、登记 singleflight leader、置 RUNNINGAttemptCount++BudgetMs = CallDeadlineMs,输出 BindCall
3

选 Executor(Filter -> Score)

selectExecutor 过滤:!Healthy;调度器停摆超过 SchedulerStallTimeoutMsheadroom = UsableContexts - localInflight <= 0;补偿后 runnable >= ExecutorMaxRunnablelocalInflight >= ExecutorMaxInflightMemoryBytes >= ExecutorMemoryHighWatermark。打分:runnable 低优先,再 localInflight 低,再 headroom 高,最后 ExecutorID 字典序。ExecutorSelectionSampleSize > 0 时只扫前 N 个 eligible 候选(power-of-d 近似)。
4

Bind 与下发

BindCall 在 actor 上同步调 Adapter.Bind(子调用走 BindSubcall,父有活预留即放行),成功后 HandleExecutorBound 输出 DispatchCallCmd。adapter 用 resolveFunctionCodeForDispatch(functionID, digest) 从 task pinned view 校验并取 EntrySelector,组装 ExecuteCallPayload(只带 digest,不带源码),在新 goroutine 里过 audit gate、做 alive / draining 检查后 SendExecuteCall。Bind 失败对根 call 输出 ArmCallRetry(now) 且不消耗 attempt 预算,子调用则重新 park 到 PendingBindSubcalls
5

挂起与恢复

Executor 发 CallWaitingwaitKind=function_code 不改变 dispatcher 状态,adapter 直接用 FunctionCodeResolver 回源码。io / subcall 触发 HandleCallWaiting,adapter 在 processWaitRequest goroutine 里调 IOHandler.HandleIO(按 payload 回显的 TaskID 找 runtime,不查 dispatcher)或 SubcallHandler.HandleSubcall。结果经 SendResumeCall 阻塞到帧真正 flush 才触发 HandleCallResumed;发送失败则合成父 call 的 resume_send_failed 失败事件(retryable,带 attempt 序号)。
6

终态与收敛

CallCompleted / CallFailed 到达 adapter 后先 releaseOnTerminal 释放预留,再回调 applyCallCompleted / applyCallFailed。core 写 CallCache、解决 singleflight waiter、取消该 call 的子调用子树,markTaskDirtyflushDispatchEpilogue 循环 FlushDirtyTasks 到静止:TerminalRootCount == TotalCount(流式还要 ProductionDone)时输出 ConvergeTaskCalls,adapter 翻译成 WorkerCore.HandleTaskPhaseFinished(PhaseCalls)

子函数调用路径

  1. Executor 内 SDK 发 CallWaiting(waitKind=subcall, functionId, args, ancestry, grantedBudgetMs)
  2. HandleSubcall 投递 evSubcallRequestapplySubcallRequest 在 actor 上校验:深度 len(ancestry) < MaxSubcallDepth、环检测(CacheKeyForJSON 与祖先 key 比对)、请求来源 executor 必须等于父 call 当前绑定的 executor(拒绝旧 attempt 的迟到请求)。
  3. resolveFunctionCodeForDispatch 取子函数 digest,AddSubcallJSON 创建子 DispatchCallContext(继承父 BatchIdxBudgetMs = grantedBudgetMs),记录 pendingSubcalls[childID]TryBindSubcallToParent 绑回父 Executor;父未绑定或父 Executor 不健康时 park 到 PendingBindSubcalls,等 HandleExecutorBound(parent)HandleExecutorSnapshotUpdated 重试。
  4. 子 call 终态后 sendSubcallResume 把结果或 subcallError(含 retryable)经 ResumeCall 回送父 Executor,BudgetUsedMs 让父扣减预算。cache / singleflight 命中的子调用走 CompleteCallFromCache 直接 resume 父。
  5. 父 call 终态(含 attempt 失败重试)时 finalizeSubcallSubtree 递归取消整棵子树,输出 CancelCallCmd;adapter 把预留标为 draining 并 best-effort SendCancelCall

Executor Pool 与连接

  • PoolManager.Start 为每个 slot spawn 一个 python -m blockx_executor --executor-id exec-N --socket-path ... --executor-max-inflight ... --executor-max-runnable ...。进程退出后 waitLoopRestartDelayMs 重启,RestartWindowMs 内超过 MaxRestarts 次进入 BACKOFFBackoffMsKillExecutor 供 wedge backstop 硬杀。
  • ExecutorSpawnMode=sandboxsandboxSpawner 通过 github.com/Chaintable/emulator/sandbox 起 containerd 容器:无网络、只挂载 UDS socket 目录、只透传 childEnvKeys 白名单环境变量、可选 CPU / 内存限额。设计见 blockx 仓库 docs/specs/executor-sandbox-isolation.md
  • Executor 主动连 Adapter.SocketPath()<SocketDir>/blockx-worker-<pid>.sock),首个有效 Heartbeat 完成 attach;没有显式注册消息。每条连接一个 reader goroutine 和一个 writer goroutine;writer 从 outbox(深度 256)取帧,一次最多 coalesce 64 帧后共享一次 Flush
  • ExecuteCall 非阻塞入队,outbox 满返回可重试错误;ResumeCall / CancelCall 阻塞到 flush 完成,因为 dispatcher 状态推进依赖它真正到达。
  • 500ms tick:SyncExecutorSnapshotsAdapter.Snapshots() 喂给 HandleExecutorSnapshotUpdatedCheckWedgedExecutors 硬杀 RunningCallID != ""LastSchedulerActiveAtMs 停摆超过 SchedulerStallTimeoutMs 的 Executor;CheckHeartbeatTimeouts 把超过 HeartbeatTimeoutMs 无心跳的连接判为 transport lost,触发 onExecutorLostapplyExecutorLost 先刷快照再对该 Executor 上全部 inflight call 发 retryable 失败。

UDS 协议

帧格式:4 字节大端长度前缀 + JSON MessageEnvelope;带二进制 sidecar 时 body 以 BXB1 魔数 + 4 字节 JSON 长度开头,JSON 之后紧跟原始字节(BlockDB protobuf 走此路径,避免 base64)。单帧上限 16 MiB。编解码用 sonic,json.RawMessage 字段原样拼接。

状态与生命周期

DispatchCallContext.State 只有四态(CallReady / CallRunning / CallWaiting / CallDone)。Executor 内部的 runnable / greenlet 细状态不上升到 Worker。
  • 重试与退避:handleAttemptFailureretryable && AttemptCount < MaxAttempts && !StopDispatching 时置 READYcomputeBackoffbase * 2^(attempt-1) 加最多 5% 抖动输出 ArmCallRetry;adapter 用 time.AfterFunc 到时投递 evRetryReady。base 优先取 task 的 CallRetryPolicy.BaseBackoffMs,否则 DispatcherConfig.BaseBackoffMs
  • 陈旧事件:CallFailureStale 丢弃 READY && AttemptCount > 0(退避中)或 attemptSeq 不匹配的失败事件;已 DONE 的 call 忽略一切迟到终态。
  • 失败策略:CallFailureFastFail 下任一根 call 最终失败即 markStopDispatching,之后不再补发,drainCancelledCalls 在 inflight 归零后把余下 call 全部取消收敛。
  • 预算:根 call 每次 attempt BudgetMs = CallDeadlineMs(默认 5000ms,0 无限),只计 CPU + IO 后端 + 子调用 + sleep,排队不计;子调用拿父切出的 grantedBudgetMs
  • 预留生命周期(adapter):Bind 加入 reservations;正常终态 Unbind;取消路径 MarkDraining,等 Executor 终态帧 releaseOnTerminal 才释放;重连 era 清空全部预留。
  • 输出保留:MaxCollectedOutputBytes > 0 且累计输出超限时 task 以 OUTPUT_BYTES_EXCEEDED 停产并丢弃输出。

配置

来自 internal/worker/app/config.goWorkerFullConfig)与 internal/worker/core/config.goDispatcherConfig)。环境变量覆盖见 internal/worker/app/app.go

扩展点

  • 改调度策略:selectExecutor / betterExecutorCandidatedispatcher_internal.go)改 Filter / Score;tryDispatchNextFairdispatcher.go)改公平模型。跑 dispatcher_select_bench_test.godispatcher_fairness_bench_test.go 看回归。
  • 新增 core 事件或命令:在 dispatcher_types.goDispatchCommand 实现,在 orchestrator_dispatch.goexecuteDispatchCommands 加分支;新事件在 orchestrator_actor.goevXxx 类型和 applyXxx,并保证只在 actor 上改 dispCoreactor_invariants_test.go 会拦)。任何可能影响收敛的入口必须 markTaskDirty,收敛只在 FlushDirtyTasks 里发生。
  • 新增 UDS 消息类型或 payload 字段:api/uds/types.go 加常量与结构,adapters/executor/adapter.gohandleMessage 加路由,handlers.go 加处理器;同步改 Python 侧协议与 api/uds/codec_test.go
  • 新增 waitKindhandlers.gohandleCallWaiting / processWaitRequest 加分支;决定它是否进入 dispatcher WAITING 状态(function_code 就不进入)。
  • 新增 Executor spawn 方式:实现 executorSpawnerexecutorProcessspawner.go),在 newExecutorSpawner 注册,sandbox_config.go 补校验。
  • 调整 Executor 心跳字段:uds.HeartbeatPayloadhandlers.gohandleHeartbeat / heartbeatUpdateExistingcore.ExecutorSnapshot

测试

  • core 测试按主题分文件:dispatcher_test.go(基础调度)、dispatcher_subcall_test.godispatcher_stream_test.go(流式生产 / batch settle / 输出上限)、dispatcher_instance_fairness_test.godispatcher_fairness_quantum_test.goretry_backoff_test.godispatcher_stale_failure_test.godispatcher_wedge_test.go
  • adapter 测试通过 ExecutorAdapter stub 注入发送失败等路径:orchestrator_dispatch_test.goorchestrator_subcall_test.goorchestrator_send_failure_test.goorchestrator_capacity_failure_test.go
  • adapters/executor/ 测试用真实 UDS socket 起 fake executor:adapter_test.gopool_test.goresume_pool_test.gowrite_coalesce_test.gopayload_buffer_test.go
  • api/uds/codec_test.gobench_test.go 覆盖帧编解码与二进制 sidecar。
  • E2E 在 cmd/worker/worker_process_*_test.go,测试组织约定见 测试组织与命令 与 blockx 仓库 docs/specs/worker-test-organization.md

相关文档

blockx 仓库 spec:
  • docs/specs/call-execution-subsystem.md:本子系统设计(重点),含 Filter/Score、重试、子调用、流式生产。
  • docs/specs/worker-executor-connection-and-python-sdk-hook.md:连接附着、失效判定、UDS 消息 contract、SDK hook。
  • docs/specs/architecture.md §4.2.4:子系统在整体架构中的位置。
  • docs/specs/2026-07-23-execute-call-function-code-reference.mdExecuteCall 只带 digest、waitKind=function_code 按需取源码。
  • docs/specs/2026-07-30-python-executor-cpu-optimization.md:结论性影响是 BlockDB 请求 / 响应走 BXB1 二进制 sidecar,Worker 用原始字节做 IO cache key。
  • docs/specs/executor-sandbox-isolation.md:containerd 沙箱 spawn 模式。
站内相关页面: