> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blockx.chaintable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync Invoker

> 同步函数调用入口：一次 gRPC unary 请求执行一次 Python 函数并当场返回结果、call_id 与执行归因时长

Sync Invoker（二进制 `syncinvoker`）是 BlockX 的同步函数调用服务。调用方发一次 gRPC unary `Invoke`，服务端在本地 Python executor 池里执行一次函数，同一次 RPC 里返回结果、`call_id` 和执行归因时长。它独立部署，不经过 Coordinator，也不走 Worker 的 slot / task 流程；它只复用 Worker 的 executor 池、UDS 协议、Function Code View 和只读 IO 子系统。整体位置见 [架构总览](/architecture/overview)。

## 职责与边界

负责：

* 接收 `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/main.go`                       | 进程装配：executor UDS adapter、IO scope、FCV、audit pool、`Service`、pool manager、reclaim sweeper、gRPC server、drain 关闭        |
| `cmd/syncinvoker/config.go`                     | `Config` / `DefaultConfig` / `LoadConfigFromEnvChecked`，全部环境变量与默认值                                                   |
| `cmd/syncinvoker/function_code.go`              | `setupFunctionCodeView`：Redis hash source、BlockDB syncer 或 in-memory devstub 三选一                                     |
| `cmd/syncinvoker/io.go`                         | `ioBackendModules` / `setupIOScope`：只读 IO backend 装配（noderpc / blockdb / blockdb bridge / localtestservice / router） |
| `cmd/syncinvoker/health.go`                     | `/healthz` liveness、`/readyz` readiness（FCV 非 devstub、≥1 healthy executor、非 draining）                                |
| `internal/syncinvoker/core/call.go`             | `CallState`、`FailureCode`、`FailureOrigin`、`Failure`、`Terminal`、`DebugOutput`                                         |
| `internal/syncinvoker/core/registry.go`         | `Registry`：call 表 + executor 索引 + attach generation，终态门                                                              |
| `internal/syncinvoker/core/admission.go`        | `PickExecutor` / `RankExecutors`：按 in-flight 计数选 executor                                                            |
| `internal/syncinvoker/adapters/service.go`      | `Service`：admission → dispatch → wait 主流程、subcall、deadline 软取消 / hard kill、终态分类、duration 聚合、`Precheck`               |
| `internal/syncinvoker/adapters/grpc_server.go`  | `GRPCServer`：proto ↔ `InvokeInput` / `InvokeOutput` 映射，trailer 回填 `call_id`                                          |
| `internal/syncinvoker/adapters/exec_adapter.go` | `ExecutorAdapter` 接口 seam，生产实现是 worker 的 `*executor.Adapter`                                                         |
| `api/grpc/syncinvoker/sync_invoker.proto`       | `SyncInvokerService` 协议，生成代码在 `syncinvokerpb/`                                                                       |
| `internal/obs/metrics_syncinvoke.go`            | 全部 `blockx_syncinvoke_*` 指标定义                                                                                        |
| `internal/syncinvoker/**/*_test.go`             | core / adapters 单元测试（stub executor adapter）                                                                          |
| `cmd/syncinvoker/*_test.go`                     | 进程级 E2E（真实 Python executor）、perf、sandbox e2e、config 测试                                                               |
| `docs/specs/sync-invoker.md`                    | 主 spec                                                                                                               |
| `docs/specs/sync-invoker-failure-origin.md`     | `FailureOrigin` 分类方案                                                                                                 |
| `docs/sync-invoker-grafana.md`                  | 指标口径与排障路径                                                                                                            |

<Note>
  `cmd/syncinvoker/` 约 1100 行非测试代码，比一般 `cmd/` 入口重。原因是它没有 `internal/syncinvoker/app/` 包：executor 池、FCV 三种来源、IO backend 装配、health probe 都直接在 `cmd/` 里镜像 `cmd/worker` 的装配逻辑。改装配时先看 `cmd/worker` 对应文件是否已有同款实现。
</Note>

## 核心类型与接口

### 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`）。主要方法：

```go theme={null}
func (s *Service) Invoke(ctx context.Context, in InvokeInput) (InvokeOutput, error)
func (s *Service) Precheck(ctx context.Context, in PrecheckInput) (PrecheckOutput, error)
func (s *Service) HandleIO(ctx context.Context, taskID, callID string, req *executor.IORequest) (any, error)
func (s *Service) HandleSubcall(ctx context.Context, executorID, parentCallID, requestID, functionID string, argsJSON json.RawMessage, ancestry []string, _ int64) error
func (s *Service) CheckWedgedExecutors()
```

* `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` 写进 trailer `x-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`）

| RPC           | 请求                                                                              | 响应                                                                                               | 说明                      |
| ------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------- |
| `Invoke`      | `InvokeRequest{oneof function_id / inline_source, args_json, deadline_unix_ms}` | `InvokeResponse{call_id, success, result_json, failure, execution_duration_ms, queue_wait_ms}`   | Open API 路径             |
| `DebugInvoke` | `DebugInvokeRequest`（字段同上）                                                      | `DebugInvokeResponse`（多一个 `debug_output`：stdout/stderr 尾部 256KB、`error_stack`、`local_vars_json`） | 页面调试路径，只有这里回传 traceback |
| `Precheck`    | `PrecheckRequest{source_code}`                                                  | `PrecheckResponse{pass, findings[]}`                                                             | 只审计不执行                  |

两层错误语义：准入层失败返回 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。

```mermaid theme={null}
sequenceDiagram
    participant C as Client
    participant G as GRPCServer
    participant S as Service
    participant R as core.Registry
    participant X as executor.Adapter
    participant P as Python executor
    participant IO as callScope

    C->>G: Invoke(function_id 或 inline_source, args_json, deadline)
    G->>S: Invoke(InvokeInput)
    S->>S: pinEpoch, resolveSource(FCV Fetch), auditSource
    S->>R: Accept(callID, CallMeta)
    S->>X: Snapshots, readyLoads, PickExecutor
    S->>X: Bind(callID, executorID)
    S->>R: Bind(callID, executorID) 进入 EXECUTING
    S->>IO: openCallScope(deadline)
    S->>X: SendExecuteCall(ExecuteCallPayload)
    X->>P: ExecuteCall over UDS
    P-->>X: IO request 或 subcall request
    X->>S: HandleIO 或 HandleSubcall
    S->>IO: 只读 Read
    S->>X: BindSubcall + SendExecuteCall(child) 在同一 executor
    P-->>X: CallCompleted 或 CallFailed
    X->>S: onCallTerminal, Registry.Finish, signal(w.ch)
    alt 终态先到
        S-->>G: InvokeOutput(result, durations)
        G-->>C: InvokeResponse
    else deadline 先到
        S->>X: SendCancelCall(subtree 与 root)
        S->>S: 有界等 w.ch, deadlineEscalationWindow
        S-->>G: InvokeOutput(Failure TIMED_OUT)
        G-->>C: InvokeResponse success=false
    end
```

关键步骤（`service.go`）：

<Steps>
  <Step title="pin epoch 与解析源码">
    `pinEpoch` 读 `FunctionCodeView.CurrentEpoch()`；`resolveSource` 校验 oneof exactly-one，`function_id` 按该 epoch `Fetch`，inline 直接返回源码但仍 pin epoch。
  </Step>

  <Step title="audit gate 与参数校验">
    `auditSource` 按 `FUNCTION_CODE_AUDIT_MODE` 决定是否 gate：off 不审、enforce fail-closed、dark 只记日志；space 在 allowlist 里的函数跳过。`normalizeArgsJSON` 一次扫描校验 JSON 数组并压缩空白，同一份字节既做 cycle key 又下发给 executor。
  </Step>

  <Step title="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`。
  </Step>

  <Step title="dispatch 与等待">
    `openCallScope` 建 IO scope；`SendExecuteCall` 发 `uds.ExecuteCallPayload{TaskID: callID, TaskCodeEpoch, CallDeadlineMs, Debug, Audit}`。然后 `select` 三路：`w.ch` 终态、deadline timer、`ctx.Done()`。
  </Step>

  <Step title="终态与归因">
    `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`。
  </Step>
</Steps>

## 状态与生命周期

`core.CallState` 的转换由 `Registry` 独占。`TERMINAL_ADMISSION_DENIED` 有两种落地：Bind 前失败走 `Registry.Drop`（不记终态，call 直接从表里移除）；Bind 后 `openCallScope` / `SendExecuteCall` 失败才 `Finish(StateTerminalAdmissionDenied)`。

```mermaid theme={null}
stateDiagram-v2
    [*] --> ACCEPTED: Registry.Accept
    ACCEPTED --> EXECUTING: Registry.Bind
    ACCEPTED --> TERMINAL_ADMISSION_DENIED: Registry.Drop，校验、audit、无容量
    EXECUTING --> TERMINAL_ADMISSION_DENIED: openCallScope 或 SendExecuteCall 失败
    EXECUTING --> TERMINAL_SUCCESS: CallCompleted
    EXECUTING --> TERMINAL_FAILURE: CallFailed，CODE_LOAD、CALL、IO_PROXY
    EXECUTING --> TERMINAL_TIMEOUT: deadline 软 cancel 后收到终态
    EXECUTING --> TERMINAL_EXECUTOR_LOST: fanOutExecutorLost
    EXECUTING --> TERMINAL_CLIENT_DISCONNECTED: onClientGone
    TERMINAL_SUCCESS --> [*]
    TERMINAL_FAILURE --> [*]
    TERMINAL_TIMEOUT --> [*]
    TERMINAL_EXECUTOR_LOST --> [*]
    TERMINAL_CLIENT_DISCONNECTED --> [*]
    TERMINAL_ADMISSION_DENIED --> [*]
```

deadline 路径（`onDeadline`）分层收敛，每层都产出真实终态：

1. 对 `Subtree` 和 root 只发软 `SendCancelCall`，都不 `Finish`；`quiesceCallScope` 停新 IO。
2. 有界等 `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")` 强制收敛。
3. 收到终态后用 `CallsByRoot` 清掉 grace 期间新加的 subcall，最终 `Failure` 统一覆盖为 `TIMED_OUT`（origin `UNKNOWN`）。

client 断连（`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 的差异

| 维度                  | Worker                                                 | Sync Invoker                                                           |
| ------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------- |
| 入口                  | Coordinator 选址 → `RequestTaskSlot` → `SubmitTask`，异步回调 | gRPC unary 直打实例，同步返回；水平扩容靠网关 LB                                        |
| 抽象                  | task → callList → call，task dispatcher 编排              | 只有 sync-call；协议层 `taskId` 是 `sync_call_id` 别名                          |
| 重试 / cache / plugin | 有                                                      | 无                                                                      |
| IO                  | `TaskIOScope`，读写                                       | 每 call 一个 `callScope`（`TaskIOScope`），只读，`DisableSharedReadCache: true` |
| 写路径                 | BlockDB / Event Writer / ResultHandler                 | 全部 disable                                                             |

复用边界（`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`。

Worker 内部细节见 [Worker](/components/worker)、[Call 执行子系统](/components/call-execution)、[IO 访问子系统](/components/io-subsystem)、[Function Code View](/components/function-code)。

## 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()`。

| 环境变量                                                         | 字段                                | 默认值                       | 说明                                     |
| ------------------------------------------------------------ | --------------------------------- | ------------------------- | -------------------------------------- |
| `SYNC_INVOKER_LISTEN`                                        | `ListenAddr`                      | `localhost:18080`         | gRPC 监听地址                              |
| `EXECUTOR_COUNT`                                             | `ExecutorCount`                   | `4`                       | N：Python executor 进程数；≤0 回退默认          |
| `EXECUTOR_MAX_INFLIGHT`                                      | `ExecutorMaxInflight`             | `4`                       | K：每 executor 顶层 call 并发上限；`0` = 不限     |
| `EXECUTOR_MAX_RUNNABLE`                                      | `ExecutorMaxRunnable`             | `128`                     | executor 内 RUNNABLE 队列上限               |
| `DEFAULT_DEADLINE_MS`                                        | `DefaultDeadlineMs`               | `100`                     | 请求 `deadline_unix_ms=0` 时的默认 deadline  |
| `MAX_CALL_DEADLINE_MS`                                       | `MaxCallDeadlineMs`               | `100`                     | deadline 上限；`0` = 只受 FCV retention 约束  |
| `HEARTBEAT_INTERVAL_S`                                       | `HeartbeatIntervalS`              | `0.2`                     | executor 心跳间隔                          |
| `HEARTBEAT_TIMEOUT_MS`                                       | `HeartbeatTimeoutMs`              | `2000`                    | 心跳超时判 lost                             |
| `SCHEDULER_STALL_TIMEOUT_MS`                                 | `SchedulerStallTimeoutMs`         | `2000`                    | wedge sweep 阈值；`0` 关闭 wedge reap       |
| `FUNCTION_SNAPSHOT_RETENTION_MS`                             | `FunctionSnapshotRetentionMs`     | `DefaultDeadlineMs + 10s` | FCV 快照保留期，同时是 `MaxAcceptedDeadlineMs`  |
| `ADMIT_SPREAD`                                               | `AdmitSpread`                     | `false`                   | 开启 admit-spread 策略                     |
| `FUNCTION_CODE_AUDIT_MODE`                                   | `FunctionCodeAuditMode`           | `off`                     | `off` / `enforce` / `dark`             |
| `FUNCTION_CODE_AUDIT_SPACE_ALLOWLIST`                        | `FunctionCodeAuditSpaceAllowlist` | 空                         | 跳过 audit gate 的 space，逗号分隔             |
| `FUNCTION_CODE_REDIS_URL`                                    | `FunctionCodeRedis.URL`           | 空                         | 设置后 FCV 走 Redis，优先级高于 BlockDB          |
| `BLOCKDB_TABLE_READ_ADDR` / `_SCAN_ADDR` / `_SUBSCRIBE_ADDR` | `BlockDB.*`                       | 空                         | 三者全设走 BlockDB FCV；全空 devstub；部分设置启动报错  |
| `NODE_RPC_ENDPOINT`                                          | `NodeRPCEndpoint`                 | 空                         | 设置后 `rpc` backend 用真实 node RPC，否则 stub |
| `EXECUTOR_SPAWN_MODE`                                        | `ExecutorSpawnMode`               | `process`                 | `sandbox` 时需 `EXECUTOR_SANDBOX_*`      |
| `BLOCKX_PROMETHEUS_LISTEN_ADDR`                              | `Prometheus.Addr`                 | 空                         | 开启 `/metrics`、`/readyz`、`/healthz`     |

`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}`。

tracing：`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`；详见 [部署概览](/development/deployment) 与 blockx 仓库 `docs/deploy/syncinvoker-systemd-nerdctl.md`。

## 扩展点

* 新增失败分类：改 `classifyFailure` / `failureRetryable` / `classifyFailureOrigin`（`service.go`），若要新 code 需同步 `core.FailureCode`、proto `FailureCode`、`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`。

## 测试

```bash theme={null}
# 首次或 python/uv.lock 变更后
uv sync --project python

# core / adapters 单元测试（stub executor，不起 Python）
go test ./internal/syncinvoker/...

# 进程级 E2E（真实 Python executor，pythonBin() 优先用 python/.venv）
go test -v -timeout 300s ./cmd/syncinvoker/...
```

* `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 tag `sandbox_e2e`。
* `Makefile` 把 `cmd/syncinvoker` 列为 `SLOW_TEST_PACKAGES`，`make test` 对它逐文件跑（每个文件独立 `SYNCINVOKER_TEST_TIMEOUT`，默认 3m），避免整包超时。

更多见 [测试](/development/testing)。

## 相关文档

* 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](/components/worker)、[Call 执行子系统](/components/call-execution)、[Python Executor](/components/python-executor)、[Function Code View](/components/function-code)、[IO 访问子系统](/components/io-subsystem)、[可观测性](/components/observability)、[协议与接口](/architecture/protocols)。
