syncinvoker)是 BlockX 的同步函数调用服务。调用方发一次 gRPC unary Invoke,服务端在本地 Python executor 池里执行一次函数,同一次 RPC 里返回结果、call_id 和执行归因时长。它独立部署,不经过 Coordinator,也不走 Worker 的 slot / task 流程;它只复用 Worker 的 executor 池、UDS 协议、Function Code View 和只读 IO 子系统。整体位置见 架构总览。
职责与边界
负责:- 接收
Invoke/DebugInvoke,执行已注册函数(function_id)或临时源码(inline_source),返回result_json/Failure、execution_duration_ms、queue_wait_ms。 - admission:在
N个 executor ×K个 context 槽位里选一个负载最低的 executor 派发;打满时返回RESOURCE_EXHAUSTED。 - deadline:到点只发软
CancelCall,然后等 executor 的真实终态(协作 cancel / SIGALRM)或兜底 sweep(wedge / 心跳超时)收敛。 - subcall:父函数发起的子调用在同一个 executor 上派发,父子共享一个 deadline、一个 code epoch 和一个 IO scope。
Precheck:对源码只做静态审计,不执行;返回pass + findings。- 执行归因:
execution_duration_ms= 整棵 subcall tree 的 effective CPU + 本 call 的 IO backend 时长;EXECUTOR_LOST一律计 0 且 retryable。 - 失败来源标注:每个失败带
FailureOrigin(USER/SYSTEM/CAPACITY/UNKNOWN),准入层失败通过google.rpc.ErrorInfo.metadata携带。
- 鉴权、API Key、限流、计费、业务级并发控制(网关侧)。
- 任何写路径:
HandleIO拒绝mode == "write";不接 BlockDB writer / Event Writer / ResultHandler。 - task 编排:没有 readyQueue、fairness、失败重试、call result cache、callbuilder / writer plugin。
- 服务端重试:一个 call 至多进一次
EXECUTING,retry 由 SDK 发新 call。
- 终态不可逆:
core.Registry.Finish是唯一终态门,每个 call 只放行一次;迟到的 executor / IO / subcall 结果丢弃。 - 一个 call 至多进一次
EXECUTING(Registry.Bind只接受ACCEPTED)。 - 每次 sync-call(含 inline)在
ACCEPTED时 pin 当前 FCV epoch,随ExecuteCallPayload.TaskCodeEpoch下发,子调用继承。 - 计量数字只来自 executor 的真实终态 payload,或在
EXECUTOR_LOST时明确为 0;不从心跳快照推断。 - 配置约束:
MaxCallDeadlineMs + heartbeat_interval < SchedulerStallTimeoutMs,否则进程启动时拒绝(cmd/syncinvoker/main.go)。 [reg.Bind → SendExecuteCall]、deadline / client-gone 的 subtree 清理、executor-lost fan-out 三者共享Service.dispatchMu,两两互斥。
代码位置
cmd/syncinvoker/ 约 1100 行非测试代码,比一般 cmd/ 入口重。原因是它没有 internal/syncinvoker/app/ 包:executor 池、FCV 三种来源、IO backend 装配、health probe 都直接在 cmd/ 里镜像 cmd/worker 的装配逻辑。改装配时先看 cmd/worker 对应文件是否已有同款实现。核心类型与接口
core(Sans-IO)
core.CallState(internal/syncinvoker/core/call.go):ACCEPTED/EXECUTING+ 六个终态;IsTerminal()。core.FailureCode/core.FailureOrigin/core.Failure:与 proto 枚举同值但不依赖 proto;FailureTerminal(f)按 code 选终态并用defaultFailureOrigin补 origin。core.Terminal:交给等待者的最终结果,带EffectiveDurationUs、QueueWaitUs、IOBackendDurationMs、Debug。core.Registry:Accept/AcceptSubcall/Bind/Finish/Drop/Subtree/CallsByRoot/InflightCount/MarkTerminating/ObserveSnapshot。executor 健康只有HealthReady/HealthTerminating,用 attach generation 区分 replacement 与被杀进程的残影。core.CallMeta:accept 时 pin 的Epoch、CacheKey(环检测)、DeadlineMs、Root。core.PickExecutor(loads, maxInflight)/core.RankExecutors(loads, maxInflight, shuffle)(admission.go):默认策略与 admit-spread 策略。
adapters
adapters.ServiceConfig(service.go):DefaultDeadlineMs、ExecutorMaxInflight、MaxCallDeadlineMs、MaxAcceptedDeadlineMs、SchedulerStallTimeoutMs、AdmitSpread。adapters.Service:NewService(cfg, exec, codeView)构造时就把回调挂到 executor adapter 上(wireExecutorCallbacks)。主要方法:
adapters.ExecutorAdapter(exec_adapter.go):Bind/BindSubcall/Unbind/MarkDraining/SendExecuteCall/SendResumeCall/SendCancelCall/Snapshots+SetCallbacks/SetExecutorLostCallback/SetIOHandler/SetSubcallHandler。测试注入 stub,生产传*executor.Adapter。adapters.FunctionCodeView:CurrentEpoch()+Fetch(functionID, epoch),fccore.SnapshotStore直接满足。可为 nil(只支持 inline)。adapters.GRPCServer(grpc_server.go):Invoke/DebugInvoke/Precheck三个 handler;准入失败时mirrorCallID把call_id写进 trailerx-blockx-sync-call-id。callScope(service.go,未导出):按 sync-call 建的iocore.TaskIOScope+ cancel + in-flight IO 计数;整棵 subcall tree 共用 root 的 scope。
协议(api/grpc/syncinvoker/sync_invoker.proto)
两层错误语义:准入层失败返回 non-OK gRPC status(
INVALID_ARGUMENT / NOT_FOUND / RESOURCE_EXHAUSTED / UNAVAILABLE),call_id 与 failure_origin 放在 ErrorInfo.metadata;执行层失败返回 OK + success=false + Failure{code, message, retryable, origin}。
数据流 / 执行流程
Service.Invoke 是主路径。core 只做决策(Registry 状态转换、PickExecutor),adapter 围绕它做 Bind / Send / 计时 / IO。
关键步骤(service.go):
1
pin epoch 与解析源码
pinEpoch 读 FunctionCodeView.CurrentEpoch();resolveSource 校验 oneof exactly-one,function_id 按该 epoch Fetch,inline 直接返回源码但仍 pin epoch。2
audit gate 与参数校验
auditSource 按 FUNCTION_CODE_AUDIT_MODE 决定是否 gate:off 不审、enforce fail-closed、dark 只记日志;space 在 allowlist 里的函数跳过。normalizeArgsJSON 一次扫描校验 JSON 数组并压缩空白,同一份字节既做 cycle key 又下发给 executor。3
admission
admit:readyLoads 从 Snapshots 里过滤 Healthy 且 AvailableContexts > 0 的 executor,用 Registry.ObserveSnapshot 做 attach generation 对账,再用 Registry.InflightCount(Bind +1 / Finish -1)作负载键;PickExecutor 选最低负载,exec.Bind 失败重选一次,仍失败 UNAVAILABLE。ADMIT_SPREAD=1 时改为 RankExecutors 逐个试 Bind,候选耗尽 RESOURCE_EXHAUSTED。4
dispatch 与等待
openCallScope 建 IO scope;SendExecuteCall 发 uds.ExecuteCallPayload{TaskID: callID, TaskCodeEpoch, CallDeadlineMs, Debug, Audit}。然后 select 三路:w.ch 终态、deadline timer、ctx.Done()。5
终态与归因
onCallTerminal 走 Registry.Finish 单次门,Unbind,把 treeEffective(子调用 effective µs 累加)折进 root,关闭 scope 并 FinalizeCallIO,signal 给等待者。buildOutput 计算 execution_duration_ms = effective/1000 + IOBackendDurationMs,EXECUTOR_LOST 恒为 0。queue_wait_ms 直接取终态 payload 的 QueueWaitUs,由 executor 上报,不并入 execution_duration_ms。状态与生命周期
core.CallState 的转换由 Registry 独占。TERMINAL_ADMISSION_DENIED 有两种落地:Bind 前失败走 Registry.Drop(不记终态,call 直接从表里移除);Bind 后 openCallScope / SendExecuteCall 失败才 Finish(StateTerminalAdmissionDenied)。
deadline 路径(onDeadline)分层收敛,每层都产出真实终态:
- 对
Subtree和 root 只发软SendCancelCall,都不Finish;quiesceCallScope停新 IO。 - 有界等
w.ch,窗口deadlineEscalationWindow = max(5s, 2×SchedulerStallTimeoutMs + 2s)。四条路径都会送信号:executor 协作 cancel 或 SIGALRM 自愈发真实终态;CheckWedgedExecutors检测 scheduler 冻结超过SchedulerStallTimeoutMs且RunningCallId已过 deadline →reapExecutorCause("wedge_sweep");心跳超时 → adapter 的 executor-lost 回调 →reapExecutor;超窗 →reapExecutorCause("deadline_escalation")强制收敛。 - 收到终态后用
CallsByRoot清掉 grace 期间新加的 subcall,最终Failure统一覆盖为TIMED_OUT(originUNKNOWN)。
onClientGone)不同:立即 Finish root、取消子树、MarkDraining,返回 gRPC context error;只把 Go 侧已观察到的子调用 CPU 和 IO 时长记入。
executor 侧:Registry 只跟踪 {ready, terminating}。reapExecutor 把 executor 标 terminating、fan-out EXECUTOR_LOST 给所有 in-flight call、调 PoolManager.KillExecutor;pool 的 waitLoop 拉起替代进程,ObserveSnapshot 见到更高 attach generation 才重新参与 admission。
与 Worker 的差异
复用边界(
docs/specs/sync-invoker.md §6):
internal/worker/adapters/executor:Adapter(UDS server、Bind/BindSubcall/Unbind/MarkDraining、心跳、Snapshots)、PoolManager(fork N 个 Python executor、崩溃拉起、KillExecutor)、IOHandler/SubcallHandler/IORequest接口。internal/worker/core:只用ExecutorSnapshot。internal/worker/devstub:StubBackendAdapter,未配置外部后端时的 IO stub。internal/functioncode/*:SnapshotStore、Redis / BlockDB syncer、audit.Pool。internal/io/core、internal/io/assembly、internal/io/adaptive:WorkerIOScope/TaskIOScope、backend 装配、AIMD admission。internal/sdk/*:blockdb / noderpc / localtestservice / router 客户端。api/uds:ExecuteCallPayload/CallCompletedPayload/CallFailedPayload/ResumeCallPayload。
Precheck 与 audit gate
Precheck 与 Invoke 的 audit gate 用同一个 audit.Auditor(audit.NewPool,进程启动时对 off / enforce / dark 三种模式都启动,Size: 2),但两者解耦:
FUNCTION_CODE_AUDIT_MODE只管 Invoke gate:SetAuditGateOff(true)(off)/SetAuditDark(true)(dark)/ 都不设(enforce)。Precheck无视 mode 与 space allowlist,固定按_entry 审计,返回真实判定。审计不过是OK + pass=false + findings;只有产不出判定才 non-OK:FAILED_PRECONDITION(无 auditor)、UNAVAILABLE(auditor 宕机)、INVALID_ARGUMENT(空源码)、CANCELED/DEADLINE_EXCEEDED(调用方 ctx 取消,单独计数)。- enforce 模式下 auditor 启动失败进程拒绝启动;off / dark 降级为无 auditor(Invoke 不 gate,Precheck 返回
FAILED_PRECONDITION)。
失败分类与归因
执行层失败在failedTerminal(service.go)里从 uds.CallFailedPayload.ErrorKind 映射,不解析 message:
classifyFailure:execute_call_rejected→CODE_LOAD_FAILED;runtime_error/cancelled→CALL_FAILED;deadline_exceeded→TIMED_OUT;transport_lost/resume_send_failed/terminal_decode_failed→EXECUTOR_LOST;executor_capacity_exhausted→EXECUTOR_CAPACITY_EXHAUSTED;IO classifier 的system_error/retryable_io/param_error/non_retryable/timeout/scope_closed→IO_PROXY_FAILED。failureRetryable:EXECUTOR_LOST/EXECUTOR_CAPACITY_EXHAUSTED恒 true;IO_PROXY_FAILED尊重 payload 的 retryable;其余 false。classifyFailureOrigin:按ErrorKind+ 内部DetailCode(如grpc:NotFound→USER,syncinvoker:io_not_configured→SYSTEM)+ChildOrigin(subcall_failed继承子调用 origin);deadline_exceeded归UNKNOWN。完整表见docs/specs/sync-invoker-failure-origin.md。- 准入层:
denyAdmission→withCallID把call_id与failure_origin写进ErrorInfo.metadata,classifyAdmissionOrigin按 reason / gRPC code 定 origin。
配置
全部在cmd/syncinvoker/config.go,环境变量覆盖 DefaultConfig()。
ShutdownTimeoutMs 固定 5000,没有环境变量覆盖。FunctionSnapshotRetention() 同时给 FCV syncer 和 ServiceConfig.MaxAcceptedDeadlineMs。
观测
指标定义在internal/obs/metrics_syncinvoke.go,口径与看板见 blockx 仓库 docs/sync-invoker-grafana.md。最重要的几个:
blockx_syncinvoke_calls_total{status, failure_code}:执行层终态计数(不含准入拒绝)。blockx_syncinvoke_admission_denied_total{reason}:dispatch 前拒绝。blockx_syncinvoke_call_duration_milliseconds/blockx_syncinvoke_billed_duration_milliseconds/blockx_syncinvoke_queue_wait_milliseconds:端到端墙钟、归因时长、排队。blockx_syncinvoke_deadline_overruns_total{resolved_by}:超 deadline 的 call 由哪一层收敛(completed_late/cooperative_cancel/in_process_timeout/executor_reaped/escalation_reap)。blockx_syncinvoke_executor_reaps_total{cause}/blockx_syncinvoke_executor_lost_victims_total{cause}:hard kill 频率与连坐量,健康时应为 0。blockx_syncinvoke_precheck_total{result}。
obs.Tracer("blockx-syncinvoker"),span syncinvoker.invoke 下有 resolve_source / admission / dispatch / wait_terminal / build_output。
部署形态
sync-invoker 作为与 worker 平行的独立 fleet 部署(EC2 systemd +nerdctl + host containerd sandbox),gRPC 9900、metrics/health 9901;详见 部署概览 与 blockx 仓库 docs/deploy/syncinvoker-systemd-nerdctl.md。
扩展点
- 新增失败分类:改
classifyFailure/failureRetryable/classifyFailureOrigin(service.go),若要新 code 需同步core.FailureCode、protoFailureCode、failureCodeToProto、failureCodeLabel。 - 新增 admission 策略:在
core/admission.go加纯函数,在Service.admit里按ServiceConfig开关切换;只依赖ExecutorLoad。 - 新增只读 IO backend:在
cmd/syncinvoker/io.go的ioBackendModules追加一个assembly.Module,与cmd/worker保持同款注册。 - 新增 FCV 来源:在
cmd/syncinvoker/function_code.go的setupFunctionCodeView增加分支,返回functionCodeSetup{view, start, close, desc, devstub}。 - 新增 RPC:改
api/grpc/syncinvoker/sync_invoker.proto→make proto→GRPCServer加 handler →Service加传输无关的XxxInput/XxxOutput。 - 新增配置项:
Config字段 +DefaultConfig+LoadConfigFromEnvChecked里的envXxx,需要时透传到ServiceConfig。
测试
internal/syncinvoker/core/*_test.go:Registry终态门、executor 健康 / attach generation、PickExecutor/RankExecutors。internal/syncinvoker/adapters/service_test.go:admission、subcall 竞态、onDeadline/onClientGone、duration 聚合、失败分类;audit_gate_test.go:enforce / dark / allowlist;precheck_test.go:Precheck 各种返回码。cmd/syncinvoker/syncinvoker_process_test.go:startSyncInvoker起真实进程,覆盖 inline / function_id / debug / 只读 IO / 写拒绝 / subcall /RESOURCE_EXHAUSTED/ CPU hog 自愈 / wedge hard kill / graceful shutdown / health probe。cmd/syncinvoker/syncinvoker_perf_test.go:延迟分解 perf 测试;syncinvoker_sandbox_e2e_test.go带 build tagsandbox_e2e。Makefile把cmd/syncinvoker列为SLOW_TEST_PACKAGES,make test对它逐文件跑(每个文件独立SYNCINVOKER_TEST_TIMEOUT,默认 3m),避免整包超时。
相关文档
- blockx 仓库
docs/specs/sync-invoker.md:形态、协议、admission、deadline 分层、subcall、生命周期、复用边界、归因、容量。 - blockx 仓库
docs/specs/sync-invoker-failure-origin.md:FailureOrigin语义与映射表。 - blockx 仓库
docs/sync-invoker-grafana.md:指标清单、PromQL、排障路径。 - blockx 仓库
docs/deploy/syncinvoker-systemd-nerdctl.md:EC2 runbook。 - 站内:Worker、Call 执行子系统、Python Executor、Function Code View、IO 访问子系统、可观测性、协议与接口。