cmd/bundle_coordinator 和 cmd/bundle_worker 组成。
它复用 block 集群的 Coordinator / Worker 装配代码,只在入口 profile 上收窄能力、切换 etcd registry 前缀,并默认开启 stream build。系统全貌见 架构总览。
一个 block bundle 是按块高连续切分的一段数据,1000 个区块一段,由 BundleRef.Number 标识。数据以 parquet 文件形式落在 S3 / Iceberg 表的 bundle=N 分区。
bundle backfill 任务把 parquet 逐行喂给用户函数,再通过 BlockDB BatchWrite 一次性覆写目标表的同一个 bundle。
职责与边界
- 负责:
- 接受
BlockBundleCallConfig/InputsCallConfig类型的 task。 - 用 DuckDB 从 parquet 流式扫描行,按批产出 call。
- 用
BlockTableWriteHandler(bundle 实现)/NormalTableWriteHandler把输出经 BlockDB BatchWrite Job 写回。写入流程是 Init → presigned PUT → Commit。
- 接受
- 不负责:
- 不注册单块实时 dbscan(
DBScanBuilder)、ReturnValuehandler、devstub 等 block 能力。相关任务会在准入时被拒。 - 不改变 task 协议、slot 状态机、heartbeat 等公共语义。这些语义归 Coordinator 与 Worker。
- 不编排上游路由。SDK 何时把流量切到 bundle endpoint 不在本组件内。
- 不注册单块实时 dbscan(
- 不变量 1:成员发现隔离。block worker 只注册到
workers类 registry,bundle worker 只注册到bundle-workers类 registry;两个 Coordinator 各 watch 自己的精确前缀列表。Profile.Validate在创建任何外部依赖前拒绝错配。 - 不变量 2:同一套代码,不同 profile。
internal/worker/app.Run/internal/coordinator/app.Run各只有一份;cmd/*只声明Profile。- bundle Coordinator 只暴露
bundlecoordinator.v1.BundleCoordinatorService,block Coordinator 只暴露coordinator.v1.CoordinatorService。 - proxy 按 gRPC method path 区分两类流量。
- 不变量 3:stream build 生产必开。bundle profile 的
TuneDefaults置StreamBuild.Enabled=true;操作者显式STREAM_BUILD_ENABLED=false仍可回滚到全量物化Build()路径。 - 不变量 4:静态错误零执行,动态错误部分执行。
PrepareStream在 Builder 阶段完成全部静态校验,失败则零 call 派发;Run中途失败时先前批次可能已执行。 - 不变量 5:BatchWrite 是唯一写路径,且启动期强校验。注册
BundleWrite(或 bundle profile 的TableUpserts)时blockdb.batchWriteAddr为空,进程在启动期失败,不等到 Writer 阶段。 - 不变量 6:Commit 结果不确定时不 Cancel、不建替代 Job,返回不可重调度失败并保留原
job_id。
代码位置
核心类型与接口
app.Profile(internal/worker/app/app.go):Deployment/Service/WorkerRegistryPrefix/UsageService(chaintable-usage 服务名,由入口固定)/Builders/Plugins/TuneDefaults。bundle 入口的实际值:
app.Profile(internal/coordinator/app/app.go):Deployment/Service/WorkerRegistryPrefixes []string;registerCoordinatorService按Deployment决定注册哪个 gRPC service。- registry 前缀(
api/etcd/registry.go):etcdapi.LegacyBundleWorkerRegistryPrefix=/blockx/bundle-workers/ProdDefaultBundleWorkerRegistryPrefix=/blockx/prod/lanes/default/bundle-workers/TestDefaultBundleWorkerRegistryPrefix是测试环境默认值。ParseRegistryPrefix返回RegistryPrefix{Format, Environment, Lane, WorkerKind}。
adapters.BundleCoordinatorServer(internal/coordinator/adapters/bundle_grpc_server.go):只做 message 转换,调CoordinatorServer.ReserveWorkerSlot。bundlescan.BundleScanBuilderDecl/BundleSource(internal/plugin/callbuilder/bundlescan/types.go):on-wirefunctionCallConfig.config。- bundle 定位字段:
blockBundle.number - source 字段:
triggerSources[].tableId|bundleBucket|operator|function|code|param|forceResultCache - 缺文件标记:
NoBundleFile = "NO_BUNDLE_FILE"
- bundle 定位字段:
bundlescan.BundleScanBuilder(bundlescan.go/stream.go):同时实现types.CallBuilder与types.StreamingCallBuilder;SetStreamLimits在启动时注入批边界。types.StreamingCallBuilder/CallStreamProducer/BatchEmit/ErrProductionStopped(internal/plugin/callbuilder/types/types.go):流式两阶段契约。bundlescan.DuckDBParquetReader/DuckDBParquetReaderConfig(duckdb.go):StreamBundleWithPlan(ctx, objectURI, bundleNumber, plan, yield)流式读取 bundle。- filter 下推到
read_parquet(...) WHERE,dedup 在 Go 侧逐行执行。 - S3 凭证懒初始化。收到
ExpiredToken且尚未 yield 时,刷新 secret 并重试一次。
bundlescan.BundleRowYield(parquet.go):借用行契约——行与其中的 string/[]byte 只在回调期间有效。borrowed.Pool/borrowed.ScanResult(internal/duckdb/borrowed/):连接池与 chunk 迭代。icebergsdk.ResolveDataFilesReq/IcebergIOAdapter/Planner.PlanDataFiles(internal/sdk/iceberg/):- 一次解析整张表当前 snapshot 的全部 live 文件,并编码为 bundle 索引。
LookupBundleFileIndex(data, bundle)从索引中选文件。细节归 后端适配器。
batchwrite.Writer.Submit(ctx, taskIO, initReq, outputs, columns) (Result, error)(internal/plugin/event/batchwrite/writer.go):- 按计划 → Init → PUT → Commit 的顺序执行。
- 返回
Result{JobID, RowCount, ContentBytes}。 commitStatusUnknownError永远满足Retryable() == false。
blocktablewrite/bundle的Plugin/Config{TargetTable, BlockBundle};tableupserts.NewPlugin(block)与tableupserts.NewBatchPlugin(bundle)。
数据流 / 执行流程
1
Reserve + Submit
调用方先向 bundle coordinator 调用
BundleCoordinatorService.ReserveWorkerSlot。拿到 worker_addr / slot_id 后,再向目标 worker 提交 SubmitTask。调度、slot TTL 和 heartbeat 与 block 集群一致,见 Coordinator。2
Builder 阶段:PrepareStream(builderSem 下,短)
BundleScanBuilder.PrepareStream 反序列化 BundleScanBuilderDecl,并对每个 source 调用 prepareBundleSource 执行静态准备:- 解析数据源:
resolveBundleSourceBucket在bundleBucket非空时直接使用该值。否则读 logical-types 判断 event / state;state 表转到<tableId>._archive,再通过ResolveDataFilesReq解析唯一的 parquet URI。 - 处理缺失数据:返回
NO_BUNDLE_FILE或 snapshot 中没有该 bundle 时,跳过当前 source,产出 0 个 call。 - 归一化调用:归一化 inline code;archive state 表还会把 operator 中的
id重映射为original_id。 - 校验扫描计划:干跑
validateDuckDBReadPlan和ToFilterSQLArgs,然后预建ParquetJSONEncoder。 - 决定缓存策略:
markProvablyUniqueSources决定哪些 source 的 call 需要设置NoResultCache。
BUILDER_FAILED,且不派发 call。3
Calls 阶段:Run(scanSem 下,长)
Orchestrator 在
scanSem 下启动 runCallProduction:- 扫描并发:
bundleScanProducer.Run使用 errgroup,最多并发扫描 3 个 source。 - 构造 call:每行经
bundleRowCallBuilder.buildNext编码成Call。row args 直接写入 argspool 缓冲区,并设置ArgsPooled=true。 - 切分批次:
bundleSourceBatchAppender在maxRows或maxArgsBytes先到上限时执行flush和emit。 - 建立背压:
emit阻塞在MaxOutstandingBatches信用上,从而限制未完成批次的数量。
emit 返回 ErrProductionStopped 表示 worker 因 fast-fail 受控停止,不算 builder 失败。4
Dispatch / Execute
通用路径,见 Call 执行子系统 与 Plugin 系统。
5
Writer 阶段:BatchWrite
Calls 收敛成功后,
bundle.Plugin.Execute 校验 targetTable / blockBundle,读取列定义,然后调用 batchwrite.Writer.Submit:- Plan:
planDelimitedWriteRows无保留遍历 outputs,计算精确的行数和字节数。超过 10,000,000 行或 5,000,000,000 bytes 时,在 Init 前失败。 - Init:
InitWriteJob创建 Job,返回job_id和data_url。 - PUT:编码器通过
io.Pipe向 presigned URL 发起一次 HTTPPUT。请求使用准确的Content-Length,且不跟随重定向。 - Commit:上传成功后调用
CommitWriteJob(row_count)。 - Cancel:PUT 失败或 Commit 前 ctx 取消时,best-effort 调用
CancelWriteJob。该调用使用context.WithoutCancel和 5s 独立超时。
written_rows / finish_time / job_id。DebugMode 只预览输出,不创建 Job。NormalTableWriteHandler 在两类 worker 上使用不同写路径:- bundle worker:复用
batchwrite.Writer,策略为NormalTableUpsertStrategy{Condition, UpdateColumns}。 - Init 前校验:
planNormalUpsertDelimitedWriteRows要求每行的有效字段集合一致、id非空,且至少包含一个非 ID 列。它会把首行推导出的update_columns显式写入请求。 - block worker:同名 handler 仍调用
TableWriteService.UpsertRows(sync=false)。 - 配置边界:写路径由 profile 在装配期固定,task 配置中没有
writeApi。
状态与生命周期
- registry 归属:worker 启动时先执行
Profile.Validate,配置加载后再执行validateWorkerRegistryPrefix。这保证bundledeployment 只会写入bundle-workers类前缀(legacy 或 v2)。coordinator 端的validateRegistryPrefixes执行同类校验,并拒绝重复前缀。 - stream build 两阶段:
- Builder 阶段只持有 builderSem,执行
PrepareStream。 - Calls 阶段持有 scanSem 和 executorSem,运行 production。production 结束或失败后释放 scanSem。
- 一个 scanSem 对应一个 task 的生产过程,内部最多并发扫描 3 个 source。worker 级 DuckDB 扫描并发上限约为
ScanSlots × 3。
- Builder 阶段只持有 builderSem,执行
BatchWrite 失败分类
分类逻辑位于batchwrite/classified_error.go 和 presigned_upload.go。插件把分类结果透传为 PluginError.Retryable。
Iceberg 解析缓存
ResolveDataFilesReq.CacheKey() 只使用逻辑表 ID,请求走 Worker system cache。默认 TTL 为 20 分钟,由 SystemIOCacheTTLms 控制。
缓存索引的高水位覆盖 RequiredBundle 时,AcceptCachedRead 复用缓存;否则刷新一次整表索引。
配置
以下是 bundle worker 最相关的配置。结构体字段来自internal/worker/app/config.go、internal/worker/core/config.go 和 internal/worker/adapters/orchestrator.go。
配置优先级是:内置默认 → TuneDefaults → WORKER_CONFIG 文件 → 环境变量。
bundle coordinator:
COORDINATOR_REGISTRY_PREFIXES(逗号分隔精确列表)整体替换 profile 默认的 [/blockx/bundle-workers/, /blockx/prod/lanes/default/bundle-workers/];其余配置与 block coordinator 相同。
扩展点
- 调 bundle profile 默认值:改
cmd/bundle_worker/main.go的TuneDefaults;只许设默认,不许后处理最终配置(操作者显式值必须能覆盖)。对应更新cmd/bundle_worker/profile_test.go、internal/worker/app/profile_test.go。 - 给 bundle worker 增/减 Builder 或 Plugin:
- 修改 profile 的
Builders/Plugins。 - 在
internal/worker/app/app.go的newBuilderRegistry/newPluginRegistry中增删对应 case。 - 新 plugin 依赖 BatchWrite 时,还要把它加入
validateProfileRuntimeConfig。
- 修改 profile 的
- 改 bundlescan 的 source 语义(新字段、新的解析路径):
bundlescan/types.go(decl)→source_resolution.go(解析)→bundlescan.go的prepareBundleSource(所有静态校验必须在这里,两条路径共用)→source_calls.go(call 拼装)。不要把校验放进Run,否则破坏静态失败零执行。 - 改 DuckDB 读取:SQL 拼装、S3 secret、filter 下推、dedup 在
bundlescan/duckdb.go;连接池 / 行解码 / 类型支持在internal/duckdb/borrowed/(不放业务策略)。 - 改 BatchWrite 生命周期(重试、上传方式、容量上限):
internal/plugin/event/batchwrite/。BlockDB SDK 只保留InitWriteJobReq/CommitWriteJobReq/CancelWriteJobReq三个原子请求,不要把编排下沉到 SDK。 - 新增一种 registry 前缀格式:
api/etcd/registry.go的ParseRegistryPrefix,两端Validate自动生效。 - 新的 bundle coordinator RPC:改
api/grpc/bundlecoordinator/bundle_coordinator.proto→make(见Makefileproto 目标)→internal/coordinator/adapters/bundle_grpc_server.go只做转换,调度逻辑仍在共享CoordinatorServer。
测试
internal/plugin/callbuilder/bundlescan/*_test.go:bundlescan_test.go:Build 路径、source 解析、archive 重映射。stream_test.go/TestBundleScanStream_*:MaxRows/MaxArgsBytes边界、单行超限、静态错误零 emit、sibling source 零 emit、emit 拒绝停扫、ctx 取消。duckdb_test.go:reader 配置、句柄释放、连接池取消。parquet_test.go/json_encoder_benchmark_test.go:parquet 编码与性能覆盖。
internal/plugin/event/blocktablewrite/bundle/write_test.go与batchwrite/*_test.go:Init/PUT/Commit/Cancel 状态机、错误分类、Content-Length 校验;tableupserts/table_upserts_test.go覆盖两种模式。cmd/bundle_worker/bundle_worker_process_test.go:TestBundleWorkerProcess_RejectsMissingBatchWriteAddr要求缺少BLOCKDB_BATCH_WRITE_ADDR时启动失败。TestBundleWorkerProcess_BootsWithBundleProfile覆盖启动、就绪和只注册 bundle profile 能力。profile_test.go钉住 deployment / service / registry 值。cmd/bundle_coordinator/*_test.go覆盖 coordinator 侧同类行为。
internal/coordinator/adapters/bundle_grpc_server_test.go:bundle adapter 复用共享调度并透传 gRPC status。- 测试组织总览见 测试。
相关文档
blockx 仓库docs/specs/:
docs/specs/2026-07-21-block-bundle-clusters.md— block / bundle 部署拆分:双入口、双 registry、系统不变量(重点)。docs/specs/2026-07-28-registry-prefix-migration.md— legacy → v2 registry 前缀格式、WORKER_REGISTRY_PREFIX/COORDINATOR_REGISTRY_PREFIXES覆盖规则。docs/specs/2026-07-23-bundle-coordinator-api.md— 独立BundleCoordinatorService的决策与上线顺序。docs/specs/2026-07-30-bundle-write-batch-api.md— BatchWrite 写路径的执行流程、契约边界、失败状态机(规范性)。docs/specs/plugin-system.md§6.1(StreamingCallBuilder契约、NoResultCache四条件)、§10(BlockBundleCallConfig/BlockTableWriteHandler/NormalTableWriteHandler契约)。docs/specs/2026-07-30-ec2-worker-profiling.md— 面向 block / bundle 两套 EC2 Worker 集群的采样方案。
- Plugin 系统:Builder / Writer 通用契约。
- Worker:阶段准入与 orchestrator。
- Coordinator:调度与 watcher。
- Call 执行子系统:通用 Calls 阶段。
- IO 访问子系统:system cache 与 retry 分类。
- 后端适配器:BlockDB / Iceberg 客户端。
- 部署概览:进程形态与发布方式。