functionId -> 源码 维护成一串不可变快照,让每个 task 在整个生命周期里都看到同一份代码。函数代码审计是挂在同一条链路上的准入门:派发前用 Python 静态审计器检查源码,不通过则拒绝这次 call。两者都不单独部署。它们在系统中的位置见 架构总览。
职责与边界
负责:- 维护 Worker 本地的
SnapshotStore:每次代码更新发布一份完整的新快照并分配新epoch;已发布快照不再修改。 - 为 task 激活提供
CurrentEpoch()与Pin(epoch);为 task 构造TaskFunctionView,把 snapshot 函数和 task 自带的 inline 源码统一解析成 source-free 的digest。 - 通过 syncer(BlockDB Online 表订阅,或 Redis 元数据 hash 轮询)把上游函数表变更发布为新快照。
- 在 Executor compiled cache miss 时按
digest回源,把源码交还 Executor。 - 在派发前按
(digest, entrySelector)查审计缓存,miss 时调用blockx_auditdaemon 做静态审计(Go 侧internal/functioncode/audit)。
- 定义发布侧如何写函数表,也不要求 BlockDB 保存
epoch或全局版本号。 - 让 Executor 直接访问 BlockDB 或直接查询 Function Code View。
- 保证不同 Worker 的
epoch一致或可比较。 - 审计规则本身。规则全部在 Python 包
python/blockx_audit/,Go 侧只做进程池、缓存和 gate。
epoch是 Worker 本地 opaque 字符串(bootID:seq),只在当前进程内有意义。- 同一个 task 在整个生命周期内固定同一个
epoch;新代码只影响新 task。 - 快照只增不改。
InstallSnapshot只接受完整的下一份视图,逻辑内容不变时不 bumpepoch。 - 单个 Worker 内不会发布顺序反转的视图:先看到函数
a变、再看到b变,就不会出现”b已新、a仍旧”的快照。 digest是 SHA-256(源码),永远在 Worker 可信入口(快照安装 / inline 归一化)本地计算;上游声明的Digest不进入执行路径。空源码的 digest 是 SHA-256(""),不是空字符串。- 审计 enforce 模式 fail-closed:违规和审计器不可用都拒绝派发。
代码位置
核心类型与接口
SnapshotStore(internal/functioncode/core/snapshot_store.go):Worker 本地快照仓库。CurrentEpoch()、Pin(codeEpoch)、Fetch(functionID, codeEpoch)、InstallSnapshot(next, publishedAt)、PruneSnapshotsBefore(cutoff)。SnapshotView(同文件):task 可 pin 的只读句柄。
FunctionCode/FunctionCodeRef(同文件):前者是带源码的 payload(SourceCode、Digest、VerifiedDigest、Space),VerifiedDigest标记为json:"-",不会从序列化输入进入。后者是 source-free 的可信身份(SourceDigest、Space),是快照对外发布的唯一形式。SnapshotMeta(同文件):Epoch、PublishedAt、SupersededAt、FunctionCnt。SupersededAt是回收时钟的起点。TaskFunctionView(internal/functioncode/core/task_view.go):task 级 overlay。Resolve(functionID, code)是源码入口边界;ResolveForDispatch(functionID, expectedDigest)返回FunctionCodeRef与entrySelector,不加载源码;SourceByDigest(digest)只在审计 miss 或 Executor miss 时被调用。SourceDigest(source)/PrepareFunctionCode(code)(internal/functioncode/core/digest.go):SHA-256 派生。blockdb.Syncer(internal/functioncode/adapters/blockdb/syncer.go):Bootstrap(ctx)(subscribe → scan → replay → 首个快照)、Run(ctx)(稳态 subscribe/apply/reconnect 循环)。依赖Deps{Store, ScanClient, SubscribeClient, RowReader, InstallAudit}。redis.Syncer/redis.RedisSource(internal/functioncode/adapters/redis/):Bootstrap/Run语义同上,用PollInterval轮询LoadAll。audit.Pool(internal/functioncode/audit/auditor.go):实现Auditor(Audit(ctx, source, entrySelector),源码优先,给 snapshot 报告和 sync-invoker 用)和DigestAuditor(AuditByDigest(ctx, digest, entrySelector, loadSource),Worker 派发用)。audit.Result/audit.Finding/audit.RejectionError(result.go、gate.go):daemon 返回的 wire 结构、单条违规、确定性拒绝错误。audit.IsRejection(err)区分”违规”和”审计器故障”。audit.SpaceAllowlist(space_allowlist.go):按FunctionCode.Space绕过 gate 的运维白名单;空Space永不命中。adapters.FunctionAuditGate(internal/worker/adapters/audit_gate.go):Check(ctx, callID, digest, entrySelector, loadSource) (*uds.AuditGatePayload, error)。adapters.FunctionCodeViewProvider/adapters.EpochResolver(internal/worker/adapters/task_runtime.go):Orchestrator 对 Function Code View 的两个依赖接口,分别只有Pin和CurrentEpoch。uds.AuditGatePayload/uds.ExecuteCallPayload.FunctionCodeDigest(api/uds/types.go):下发给 Executor 的 gate 与 digest。
数据流 / 执行流程
代码更新路径
BlockDB syncer 的关键行为(internal/functioncode/adapters/blockdb/syncer.go):
Bootstrap:先Subscribe([system.function])并把事件缓冲进subscriptionPump,再ScanAll得到基础视图,然后按顺序重放缓冲事件,最后InstallSnapshot。bootstrap 建立的订阅直接交给Run继续消费,避免交接窗口漏事件。Run:每收到一个changeEvent,按RowIDs用BatchGetRows读取受影响行的当前值,在上一份视图上 copy-on-write 得到下一份完整视图,再InstallSnapshot。io.EOF和 transient 错误按ReconnectDelay(默认 200ms)重连,并以最近一次已应用事件的CreatedAt作为StartAt。- 只读
id和code两列(readColumns());space列目前只是机会式读取,缺失时为空字符串,因此永远不会命中 allowlist。 - 每次安装后调用
pruneIfNeeded()与obs.RecordFunctionCodeSnapshotStore。
internal/functioncode/adapters/redis/)走的是同一个 InstallSnapshot 入口,只是来源换成每 PollInterval 一次 HGetAll 全量刷新。decodeFunctionValue 兼容 hash value 为裸源码或 JSON(code / sourceCode / source_code / source 任一字段)。
InstallSnapshot 只比较 FunctionCodeRef(digest + Space)。同一份源码在相邻快照之间复用已算好的 digest(cloneFunctionRefs 用上一份快照的 sourcesByDigest 反查),所以一次只改一个函数的更新只做一次 SHA-256。查询路径
1
task 激活时 pin epoch
Orchestrator.activateAndRun(internal/worker/adapters/orchestrator_phases.go)调用 epochResolver.CurrentEpoch(),再 functionCodeViewProvider.Pin(epoch) 得到共享只读 SnapshotView,并校验 snapshotView.Epoch() == epoch。然后用它构造 TaskFunctionView 存进 taskRuntime.functionCodeView。任何一步失败都走 HandleTaskActivationFailed。2
进入 Dispatcher 前解析 digest
builder 产出 CallList 后,
resolveCallDigests 对每个 call 调用 TaskFunctionView.Resolve(functionID, code):snapshot 函数做一次 Lookup,inline 函数按 *FunctionCode 指针注册一次源码。之后 call 生命周期只携带 FunctionCodeDigest,builder 的 Code 指针被清空。3
dispatch 时取元数据并过审计 gate
处理
DispatchCallCmd 时(orchestrator_dispatch.go),resolveFunctionCodeForDispatch 调 ResolveForDispatch(functionID, expectedDigest),只拿 Space 和 entrySelector,不读源码。若 gate 存在且 Space 不在 allowlist,调用 FunctionAuditGate.Check;loadSource 回调是 resolveFunctionCode(taskID, digest),只有审计缓存 miss 才会执行。拒绝时发 evCallFailed,errorKind 为 function_code_audit_rejected,不可重试。4
digest 下发 Executor,miss 时回源
ExecuteCallPayload 只设置 FunctionCodeDigest、EntrySelector 和可选的 Audit,不携带源码。Executor ModuleRegistry.load_callable 未命中 digest 时发 CallWaiting(waitKind=function_code);Worker 侧 handleFunctionCodeRequest 通过 SetFunctionCodeResolver 注册的 resolveFunctionCode 调 TaskFunctionView.SourceByDigest,用 ResumeCall(resumeKind=function_code) 回复。这条请求不进入 dispatcher 的 WAITING 状态,也不计 IO 指标。Executor 收到后重算 SHA-256 与 digest 比对,然后 _verify_audit_gate 复核 sourceDigest 与 pass。审计调用链
- 两个审计入口共用一个
audit.Pool:newFunctionCodeAudit(internal/worker/app/function_code.go)在 enforce / dark 模式下启动 2 个python -u -m blockx_auditdaemon。 - 快照安装审计:
InstallAudithook 在每次新 epoch 发布后异步跑audit.BuildReport,逐函数以functionId作为entrySelector审计并打印function-code install audit日志。它不影响快照内容,主要作用是预热(digest, entrySelector)缓存,让首个 dispatch 通常直接命中。 - 派发审计:
FunctionAuditGate.Check→audit.CheckByDigest→Pool.AuditByDigest。缓存 miss 时 singleflight leader 调loadSource,重算 digest 与可信 digest 比对,再通过 4 字节长度前缀的 JSON 帧发给 daemon(proc.go)。daemon 返回Result{Pass, SourceDigest, EntryName, Findings};Pool再校验res.SourceDigest == digest。 - Python 侧
blockx_audit.audit(source, function_id):AST 节点默认拒的白名单(_ALLOWED_NODES)、free-name / import / 属性 allowlist(tables.py)、kind 追踪与能力边界(kinds.py)、以及只在 walker 无 finding 时运行的字节码 backstop(bytecode.py),合成一个pass位。它只ast.parse,不执行用户代码,运行时纯 stdlib。 - 结果对 task / call 的影响:enforce 下
RejectionError(违规)和普通 error(daemon 超时 / 崩溃)都让这次 call 以function_code_audit_rejected失败;dark 下只打 warn 日志并以无 gate 的 payload 照常派发;Space命中 allowlist 时跳过审计且 Executor 在非受限 namespace 加载。
Precheck 复用同一个 audit.Pool 的源码优先接口 audit.Check / audit.Observe,只审计不执行,详见 Sync Invoker。
状态与生命周期
epoch生成:SnapshotStore.installSnapshot在写锁下seq++,epoch = fmt.Sprintf("%s:%d", bootID, seq);bootID由newFunctionCodeBootID()生成为boot-<pid>-<unixnano>。- 保留与回收:
PruneSnapshotsBefore(cutoff)只删非当前、且SupersededAt早于 cutoff 的快照。计时从被取代那一刻开始,不是从发布开始,所以一个长 task pin 住刚被取代的旧 epoch 仍有完整保留窗口。两个 syncer 都在每次安装后以now - SnapshotRetention调用它,没有独立定时器。 - 已
Pin的SnapshotView由 task 的 Go 引用保活;store 中删掉该 epoch 不影响已激活 task。calls phase 结束时orchestrator_actor.go把rt.functionCodeView置nil,让 writer phase 不再持有历史快照。 - 启动:
setupFunctionCodeView在 gRPC 服务启动前完成syncer.Bootstrap;失败直接os.Exit(1)。因此 Worker 对外可见时首个快照必然已发布。 - 幂等:
InstallSnapshot对逻辑相同的视图返回当前 epoch 且created=false;TaskFunctionView.RegisterInline对同一*FunctionCode指针不重复哈希;同一functionID绑定不同源码或与 snapshot 函数撞名时该 ID 被”毒化”,后续 dispatch 确定性失败。 - 审计模式(
FunctionCodeAuditMode):off(默认,不启 daemon、无 gate)、enforce(fail-closed;daemon 池启动失败则 Worker 启动失败)、dark(同样审计并打日志但不拦截;daemon 池启动失败降级为 off 并 warn)。
配置
以下字段来自internal/worker/app/config.go(WorkerFullConfig),环境变量覆盖见 internal/worker/app/app.go。
审计池的其余参数在
newFunctionCodeAudit 里写死:Size: 2、Module: "blockx_audit";audit.Config.withDefaults 补齐 CallTimeout 10s、MaxCache 4096。BlockDB syncer 的 ReconnectDelay 默认 200ms,Worker 未暴露配置。
setupRedisFunctionCodeView 打印的 redis(key=... poll=...) 描述在未配置 PollIntervalMs 时写的是 120s,实际生效的是 fcredis.SyncConfig.withDefaults 的 60s。扩展点
- 新增一种代码来源:实现一个和
redis.Source形状类似的加载器,写一个持有*fccore.SnapshotStore的 syncer,只通过InstallSnapshot发布完整视图;在setupFunctionCodeView增加分支并返回functionCodeSetup{provider, resolver, start, close}。不要让查询路径访问外部 IO。 - 扩大审计 allowlist:改
python/blockx_audit/tables.py(以及涉及 kind 时的kinds.py、auditor.py),同步更新python/blockx_executor/restricted_runtime.py的 facade 和python/tests/test_audit_*.py。维护表和自检清单见 blockx 仓库docs/specs/function-code-python-whitelist.md§13。 - 修改 daemon 协议:Go 侧
internal/functioncode/audit/proc.go与 Python 侧python/blockx_audit/daemon.py必须同步(帧格式、hello 帧、Result字段名),并更新python/tests/test_audit_protocol.py。 - 新增 gate 语义或错误分类:改
internal/functioncode/audit/gate.go(Check/Observe/RejectionError)和internal/worker/adapters/audit_gate.go。 - 本地开发注册测试函数:
internal/worker/app/function_code.go的newDevFunctionCodeStore()调MemoryFunctionCodeStore.Register。
测试
internal/functioncode/core/*_test.go:不可变性、epoch 不回退、Pin共享视图、pruned 后已 pin 视图仍可用、按SupersededAt回收、inline 与 snapshot 撞名拒绝。internal/functioncode/adapters/blockdb/syncer_test.go:Bootstrap的 subscribe/scan/replay、事件顺序、重连从lastApplied续订、重复事件不 bump。internal/functioncode/adapters/redis/*_test.go:payload 解码、刷新失败日志限流、InstallAudit触发条件;TestRedisSourceLive需要FUNCTION_CODE_REDIS_URL,否则 skip。internal/functioncode/audit/*_test.go:auditor_test.go会拉起真实 daemon(依赖python/.venv,缺失则 skip);gate_test.go、space_allowlist_test.go、proc_robustness_test.go覆盖 gate 语义和进程崩溃 / 超时替换。internal/worker/app/function_code_test.go、function_code_audit_test.go:来源选择、retention 解析、三种审计模式的启动行为。python/tests/test_audit_*.py:test_audit_features.py(子集规则)、test_audit_tables.py/test_audit_kinds.py、test_audit_bytecode.py、test_audit_protocol.py(daemon 帧协议)、test_audit_corpus.py(v12 函数语料)。test_executor_audit_gate.py覆盖 Executor 复核 gate。- E2E:
cmd/worker/worker_process_function_code_test.go(miniredis 端到端)、worker_process_activation_test.go(STUB_EPOCH_FAIL触发激活失败)、worker_process_localtestservice_test.go(FUNCTION_CODE_AUDIT_MODE=enforce下走 gate)。整体测试组织见 测试。
相关文档
blockx 仓库中的 spec:docs/specs/function-code-view.md:快照、epoch、保留回收与查询链路的详细设计。docs/specs/function-code-audit-design.md:审计机制、两道门、错误码、Executor 受限运行时。docs/specs/function-code-python-whitelist.md:完整 allowlist 清单,逐表对齐blockx_audit。docs/specs/2026-07-23-execute-call-function-code-reference.md:ExecuteCall只传 digest、miss 时回源的协议决策。docs/specs/architecture.md§4.2.4.4:Function Code View 在总体架构中的定位。docs/specs/sync-invoker.md§3.1:Precheck与审计模式的关系。
- Worker:task 激活与 Orchestrator。
- Call 执行子系统:dispatcher 与 executor adapter。
- Python Executor:
ModuleRegistry、受限运行时。 - Sync Invoker:
Precheck与同步调用的审计。 - 协议与接口:UDS
ExecuteCall/CallWaiting/ResumeCall。