WorkerIOScope)里嵌套 task 级作用域(TaskIOScope),请求生命周期嵌套在 task 生命周期里。
职责与边界
负责:- 提供
TaskIOScope.Read / Write这一个同步入口,供 Builder、Executor IO 回送、Plugin 共用。 - 请求路径固定为:Worker 级读缓存 → task 内 singleflight → task IO 窗口 → backend admission → adapter。
- 持有 Worker 级共享读缓存(IO cache)和独立的 system cache。
- 对带 Builder / Result retry scope 的请求执行有界重试。
- 把请求生命周期绑定到 task:scope 关闭后取消在途请求、拒绝新请求、丢弃迟到结果。
- 统一错误模型(
model.IOError)与 per-task IO 统计。
- 决定 call 是否被调度(那是 Dispatcher 的事,见 Call 执行子系统)。
- 真实协议调用、连接池、错误分类的具体逻辑——这些在
internal/sdk/*的 adapter 里,见 后端适配器。 - call result cache(归 Call 执行子系统)。
- 跨 task 的写幂等或事务恢复。
- 每个 backend kind 只有一个 admission controller,由
assembly.Build强制包装(adaptive.WrapBackend);TaskIOScope前面不再叠第二层共享 quota。 - 写请求不进缓存、不做 singleflight,但同样经过 task 窗口和 backend admission。
req.Timeout()只约束获得 permit 之后的真实 adapter 调用;task 窗口和 admission 排队没有本地超时,只受请求 context 与 scope 生命周期约束。- 空
CacheKey()表示”不可缓存”,跳过缓存和 singleflight,直接走窗口 + adapter。 TaskIOScope.Close幂等;关闭后不得再回填缓存或 singleflight。- IO cache key 由
(backend, operation)命名空间 + 请求归一化 key 组成,不同后端 / 接口不会碰撞。
代码位置
核心类型与接口
model.BackendAdapter(internal/io/core/model/adapter.go):adapter 必须实现的最小接口。Write返回(*WriteResult, error)。
model.ReadReq/model.WriteReq(同文件):请求接口。ReadReq有Backend() BackendKind、Operation() string、CacheKey() string、Timeout() time.Duration;WriteReq没有CacheKey。具体请求类型由各 SDK 包定义(如internal/sdk/blockdb/types.go),Executor 回送的请求由executor.IORequest实现。model.ErrorClassifier:adapter 可选实现ClassifyError(err) *IOError;没实现时未知错误一律归retryable_io。model.CachedReadValidator:可缓存读的可选能力AcceptCachedRead(data []byte) bool,返回 false 时旁路缓存值并进入同 key 的 singleflight 刷新(当前使用者是internal/sdk/iceberg/io_adapter.go)。model.IOError/model.IOErrorKind:统一错误模型。Kind取值system_error、param_error、retryable_io、non_retryable、timeout、scope_closed;Code是细粒度可观测码(如io:no_adapter_registered、context:timeout);StructuredError()生成回给 Python SDK 的errorKind / retryable / detailCode。model.BackendRegistration:Kind、Adapter、SystemCache bool(读走 system cache)、ReadOnly bool(Write直接以io:write_not_supported拒绝)。core.WorkerIOScope(worker_scope.go):NewWorkerIOScope(WorkerIOScopeParams)、NewTaskScope(*commontypes.TaskCtx)、Close()、Snapshot()。WorkerIOScopeParams.DisableSharedReadCache供 Sync Invoker 关闭跨 call 结果复用。core.TaskIOScope(task_scope.go):Read、Write、Close(reason)、Snapshot()、IOStats()、BackendHits()、AddCallIO / FinalizeCallIO、BackendIOStats()、DrainIOErrorClusters()。core.RetryScope/core.WithRetryScope(retry.go):只有RetryScopeBuilder、RetryScopeResult两个框架标签能开启 IO-local retry;未知标签 fail-closed。adaptive.Backend/adaptive.WrapBackend(next, Config)(adaptive/backend.go):给任意 adapter 外包一个独占Limiter;同时实现ErrorClassifier,把 admission 错误映射为 canonical 的context:timeout/context:canceled/io:temporarily_unavailable。adaptive.Limiter/adaptive.Permit/adaptive.Outcome(adaptive/limiter.go):Acquire(ctx) (*Permit, error)、Permit.Done(outcome)(幂等);Outcome三值OutcomeSuccess / OutcomeOverloaded / OutcomeIgnore。adaptive.OutcomeClassifier:adapter 可选实现ClassifyAdaptiveOutcome(err) Outcome;没实现时成功记Success,错误记Ignore。实现者见internal/sdk/noderpc/adaptive.go、internal/sdk/blockdb/adaptive.go、internal/sdk/logicaltypes/adaptive.go、internal/sdk/meta/adaptive.go。adaptive.AIMDConfig(adaptive/config.go):运维面的 AIMD 配置;ResolveAdmission(initialLimit)在Enabled=false时退化为FixedConfig。assembly.Module/assembly.Build(assembly/assembly.go):见”扩展点”。capabilitywire.DecodeBinaryRequest(kind, req) (method, payload, error):见”扩展点”。
数据流 / 执行流程
一次来自 Executor 的读请求(Python SDK → UDS → Worker → backend): 要点:- 入口是
internal/worker/adapters/executor/handlers.go的processWaitRequest:按CallWaitingPayload的Mode / Operation / Backend / CacheKey / TimeoutMs / Request构造executor.IORequest(带二进制 sidecar 时用NewIORequestWithBinary,DeriveCacheKeyFromBinary=true时以 sidecar 字节直接做 cache key),再调IOHandler.HandleIO。 Orchestrator.HandleIO(orchestrator_dispatch.go)按taskID找到taskRuntime,把 instance id 写进 ctx(供公平 lane 用),Executor 来源的blockdb写在这里被拒绝,然后调rt.io.Read / Write;成功后把BackendLatencyMs记进 per-call IO 统计。- 回程:
ResumeCall.BudgetUsedMs取ReadResult.BudgetChargedMs()——缓存 / singleflight 命中为 0,真实读为 adapter 测得的后端时长;admission 和窗口排队时间不计入 budget。请求经二进制 sidecar 到达的,结果Data也走 sidecar 回去(binary-in → binary-out)。 - Builder 与 Writer 不经 UDS,直接持有
TaskIOScope(Builder 拿的是cbtypes.TaskIOReader,plugin 拿types.TaskIO),ctx 分别带RetryScopeBuilder/RetryScopeResult。 - 写路径与读路径的差别只在:跳过缓存与 singleflight、先检查
ReadOnly;其余(窗口、admission、重试、错误分类)相同。
缓存与命名空间
WorkerIOScope 持有两个 cache.Cache[ioCacheKey] 实例:
- IO cache:默认 4096 entries / 16 MiB,TTL 固定 1 分钟(
workerIOCacheTTL),跨 task 共享,Worker 关闭时Purge。 - system cache:默认 64 entries / 512 MiB / 20 分钟,只服务注册时声明
SystemCache: true的 backend(当前是iceberg的resolve_data_files)。
ioCacheKey{namespace, request} 中的 namespace 是 (backend, operation) 的数字 ID:BlockDB 两个 kind、logicalTypes、meta、router、localtestservice、iceberg 的已知 operation 在 knownIOCacheNamespace 里有编译期 ID;NodeRPC 的 JSON-RPC 方法是开放集合,与其他未知组合一起走有界(4096)动态注册表,注册表耗尽时该请求退化为不可缓存读。第三类缓存 call result cache 不在本子系统内。
自适应准入
adaptive.Backend 是每个 backend 唯一的进程级准入门。Limiter 实现窗口化 AIMD:
- 每个采样窗口(默认 1s)内首次
Overloaded立即乘法下降(BackoffRatio默认 0.5,下限MinLimit);同窗口再次下降要求新代际样本达到RepeatBackoffMinSamples且过载比例达到RepeatBackoffOverloadRatio,且不超过MaxDecreasesPerWindow。 Ignore比例达到IgnoreRatioThreshold(0.10)时窗口失去增长资格,达到IgnoreBackoffRatioThreshold(0.30)时窗口结算时收缩一次。- 增长只在窗口结束时发生,要求无过载、样本够、饱和(最大 in-flight 达到 limit)、连续健康窗口达到
IncreaseAfterHealthyWindows;步长IncreaseStep(0 表示 initial 的 1%,至少 1)。 - 超过已学习的
softLimit时增长变成探测:探测代际过载则精确退回probeBaseLimit,连续失败按窗口数指数退避(上限ProbeBackoffMaxWindows)。 - limit 降到 in-flight 以下时不取消存量请求,用 shrink debt 偿还。
- 不起 goroutine,窗口由
Acquire / Done事件惰性推进。
Config.LaneKeyFromCtx 非空时进入公平 lane 模式(adaptive/lanes.go):等待者按 key 分道,空出的容量给 in-flight 最少的 lane;AIMD 策略不变。Worker 通过 IOFairQueueBackends 配置对指定 backend 开启,key 取 clientid.FromContext。
observed.WrapBackendRegistration 在包装的同时按 BackendKind 注册 blockx_io_adaptive_limit / _pressure / _overloads_total / _ignored_total / _limit_transitions_total / _admission_waits_total / _admission_wait_seconds_total。IO 层其余指标是 blockx_task_io_ops_total、blockx_task_io_backend_duration_milliseconds、blockx_task_io_cache_hits_total、blockx_task_io_singleflight_hits_total,定义在 internal/obs/metrics.go 与 internal/obs/metrics_io_adaptive.go。
状态与生命周期
TaskIOScope 只有”活跃”和”closing”两个状态:
- 创建:task 激活时
Orchestrator调ioScope.NewTaskScope(ioTaskCtx)(orchestrator_phases.go),失败记为task_activation_failed。scope 持有派生自 task ctx 的scopeCtx。 - 请求:每次
Read / Write派生ioCtx,context.AfterFunc(scopeCtx, ioCancel)使 scope 关闭能取消在途 adapter 调用;每个 attempt 单独Acquire / Releasetask 窗口,退避期间不占窗口和 permit。 - 关闭:task 终态时
rt.io.Close("task_terminal")。Close用CompareAndSwap保证幂等,然后依次:取消scopeCtx、sfTable.DrainAll(scope_closed)唤醒所有 waiter、从WorkerIOScope.activeScopes移除。 - 迟到结果:leader 在
runIORetry成功返回后再检查一次closing,已关闭则返回scope_closed,不写缓存也不广播;sflight.Table.Complete与DrainAll在同一把锁下做幂等 close,避免 double-close panic。 - Worker 关闭:
WorkerIOScope.Close先关所有活跃 task scope,Purge两个缓存,再逐个adapter.Close()(adaptive wrapper 先关 limiter 唤醒 waiter,再关 raw adapter)。
- 等待期错误由
classifyWaitContextError归一:scope 已 closing →scope_closed/io:temporarily_unavailable;DeadlineExceeded→timeout/context:timeout;Canceled→scope_closed/context:canceled;其他 →system_error。没有独立的”排队超时”错误。 - backend 错误由
classifyBackendError归一:*IOError原样透传 → adapter 的ClassifyError→ 否则retryable_io。 shouldRetryIO只在ioErr.Retryable()、未用完RetryMaxRetries、ctx 带已知 retry scope 且 ctx 未结束时放行。退避用backoff/v5:默认三次窗口 250–500ms、500ms–1s、1–2s,单次上限 3s。- 写重试是 at-least-once:IO core 只重放当前 typed write request。BlockDB 全部 API(含 BundleWrite 的
InitWriteJob / CommitWriteJob / CancelWriteJob)保证幂等,因此 Result scope 内的 BlockDB 写保持可重试;此前的WithoutRetryScope退出机制已在 commitaae6b77a删除。接入不满足幂等契约的写 API 时必须先调整 retry scope 或 backend 契约。 - singleflight leader 拥有整个 retry 循环,waiter 只等最终结果;缓存只回填最终成功值。
配置
Worker 侧 IO 配置在WorkerFullConfig.IO(类型 iocore.WorkerIOConfig,定义在 internal/io/core/model/adapter.go,internal/io/core/types.go 只做别名;默认值来自 internal/worker/app/config.go 的 DefaultIOConfig):
backend admission 配置在
WorkerFullConfig 顶层:
Worker 与 Sync Invoker 的装配表注册的 backend kind 是
rpc、blockdb、blockdb_executor、logicalTypes、iceberg、localtestservice、router;internal/sdk/meta 定义了 BackendMeta,但两处装配表都没有注册它。
Sync Invoker(cmd/syncinvoker/io.go)只设 TaskMaxInflightIO: 1024,并以 DisableSharedReadCache: true 构造 WorkerIOScope。
扩展点
新增一个 capability backend
以internal/sdk/localtestservice/ 为模板(docs/capability-backend-guide.md 是逐文件手册),平台代码零改动。Go 侧需要:
1
定义 kind 与 wire 契约
在
internal/sdk/<kind>/ 下声明 const Backend<X> iocore.BackendKind = "<kind>"(不得复用存量 kind),写 proto/<kind>.proto 作为 gRPC 服务定义,生成代码进 gen/。2
实现 service 与 adapter
service.go 实现生成的 server 接口,业务逻辑全在这里。adapter.go 实现 model.BackendAdapter:Read 用 capabilitywire.DecodeBinaryRequest(string(Backend<X>), req) 拆出 (grpc_method, protobuf 字节),proto.Unmarshal 后调 service,响应 proto.Marshal 进 ReadResult.Data;未知方法返回 IOErrParam + <kind>:unsupported_method,信封 / proto 不合形返回 <kind>:bad_request。只读能力的 Write 不会被调到(module 声明 ReadOnly: true)。外部依赖型后端应再实现 model.ErrorClassifier 和 adaptive.OutcomeClassifier。3
提供 assembly.Module
导出
Module()(可带依赖入参)返回 assembly.Module{Kind, ReadOnly, Build, Stub, Admission}。进程内轻后端用 adaptive.FixedConfig(n);外部依赖型后端用 AIMDConfig.ResolveAdmission,Enabled 判断配置是否齐全,Stub 给 dev 回退(不给则未配置时拒绝启动)。需要借用其他 backend 时按 localtestservice.BlockDBReader 的方式注入 raw adapter:借用不持有(Close 不关它)、准入自负、本地 ctx 结束的错误先原样放行再委托依赖分类。4
注册到两个 binary
internal/worker/app/config.go 加开关字段,internal/worker/app/app.go 加 env override 并 backendModules = append(backendModules, <kind>.Module(...));cmd/syncinvoker/io.go 同样 append。deploy/env/worker.env.example 补一行。5
测试
adapter_test.go 覆盖:正常路径、参数默认值、未知 operation、malformed 信封、malformed proto、Write 拒绝、Module() 声明;组合后端再加 typed 请求转发、依赖未注入干净失败、依赖错误分类透传。e2e 在 cmd/worker/ 照 worker_process_localtestservice_test.go 写正例(FUNCTION_CODE_AUDIT_MODE=enforce 下 task SUCCEEDED)和 <KIND>_DISABLED=true 负例(task FAILED);devstub 函数注册在 internal/worker/app/function_code.go。connection.py channel getter、executor BridgeChannel 接线、python/blockx_audit/tables.py 五处表增量)见 Python Executor 和 docs/capability-backend-guide.md §3–§4。
assembly.Module 的字段:
OuterWrap 是唯一允许放在 admission 外侧的层(当前只有 NodeRPC 语义合批 noderpc.NewBatchingBackend,由 NODE_RPC_SEMANTIC_BATCH_MODE 门控,默认 off)。
其他常见改动
- 给热点
(backend, operation)加编译期 cache 命名空间:改worker_scope.go的knownIOCacheNamespace和对应枚举,并更新TestKnownIOCacheNamespacesAreDistinctEnums。 - 调整某个 backend 的 AIMD 反馈:改该 SDK 包的
adaptive.go(ClassifyAdaptiveOutcome),不要动internal/io/adaptive;判断传输层错误用adaptive.IsTransportFailure / IsTransportFailureMessage。 - 新增可缓存但需要请求级校验的读:让请求类型实现
model.CachedReadValidator。
测试
internal/io/core/core_test.go:请求路径主干——缓存命中 / 跨 task 共享(TestWorkerScope_CacheIsSharedAcrossTaskLifetimes)、singleflight(TestTaskScope_ReadSingleflight)、task 窗口获取 / 释放(TestTaskScope_TaskWindowWaitsUntilCapacityIsReleased、TestTaskScope_IOBackendAttemptReleasesTaskWindow)、scope 关闭与迟到结果(TestTaskScope_ActiveBackendContextLifecycle、TestTaskScope_ReadAfterClose、TestTaskScope_CloseIdempotent、TestWorkerScope_CloseCascades)、只读后端拒写、空 cacheKey 旁路(task_scope_bypass_test.go)。internal/io/core/retry_test.go:retry scope 门控、单个 leader 拥有重试、attempt 间释放资源、退避边界。internal/io/core/task_scope_drain_test.go:Complete与DrainAll并发不 double-close。internal/io/adaptive/limiter_test.go、lanes_test.go、backend_test.go、backend_budget_test.go:AIMD 各条规则、探测、公平 lane、admission 排队不计入 budget、超时在 admission 后才开始。internal/io/assembly/assembly_test.go:stub 回退、重复 kind、失败时关闭已建 adapter、OuterWrap。internal/io/capabilitywire/wire_test.go:信封三方绑定、shape 错误 fail-closed。internal/io/sflight/sflight_test.go、internal/io/cache/cache_test.go:singleflight 与缓存的独立单测。- 基准:
internal/io/core/bench_test.go、adaptive/lanes_bench_test.go。 - 与 Executor 联动的进程级 e2e 在
cmd/worker/worker_process_*_test.go(如worker_process_blockdb_bridge_test.go、worker_process_localtestservice_test.go)。更多见 测试组织与命令。
相关文档
blockx 仓库中的 spec 与设计文档:docs/specs/io-subsystem.md:本子系统的行为规范(Scoped Resource Context、adapter 职责、AIMD 反馈分类、Close 语义、缓存策略与 fork 风险、配置项)。docs/io-backend-module-design.md:backend module 装配层的设计动机与不变量。docs/capability-backend-guide.md:新增能力后端的逐文件操作手册与检查单。docs/specs/architecture.md§4.2.5 / §4.2.6:IO 访问子系统与共享状态缓存在总体架构中的定位。docs/specs/noderpc-semantic-batching.md:NodeRPC 语义合批(OuterWrap的使用者)。docs/deploy.md:IO / AIMD 相关环境变量的部署说明。
Worker
WorkerIOScope 的宿主,task scope 的创建与关闭时机。
后端适配器
BlockDB / NodeRPC / logical-types / Iceberg 等 adapter 的具体实现与错误分类。
Python Executor
IO 请求在 Python 侧如何被拦截并经 UDS 回送。
Call 执行子系统
HandleIO 所在的 Orchestrator 与 call result cache。