cmd/bundle_coordinator and cmd/bundle_worker.
It reuses the block cluster’s Coordinator / Worker assembly code. Only the entry-point profile narrows its capabilities, switches the etcd registry prefix, and enables stream build by default. See Architecture overview for the full system.
A block bundle is a contiguous slice of data split by block height. BundleRef.Number = N covers heights N*1000+1 … (N+1)*1000; bundle 0 contains only height 0. The data is stored as parquet files in the bundle=N partition of an S3 / Iceberg table.
A bundle backfill task feeds the parquet rows to the user function, then uses BlockDB BatchWrite to overwrite the same bundle in the target table.
Responsibilities and boundaries
- Responsible for:
- Accepting tasks of type
BlockBundleCallConfig/CallListCallConfig. - Streaming rows from parquet with DuckDB and producing calls in batches.
- Writing output back through a BlockDB BatchWrite Job with
BlockBundleWriteResultHandler/TableUpsertsResultHandler. The write flow is Init → presigned PUT → Commit.
- Accepting tasks of type
- Not responsible for:
- Registering block capabilities such as single-block real-time dbscan (
DBScanBuilder), theReturnValuehandler, or devstub. Related tasks are rejected at admission. - Changing shared semantics such as the task protocol, slot state machine, or heartbeat. These belong to Coordinator and Worker.
- Orchestrating upstream routing. When the SDK shifts traffic to the bundle endpoint is outside this component.
- Registering block capabilities such as single-block real-time dbscan (
- Invariant 1: membership discovery isolation. block workers register only under
workers-kind registries, bundle workers only underbundle-workers-kind registries; each Coordinator watches its own exact prefix list.Profile.Validaterejects mismatches before any external dependency is created. - Invariant 2: one codebase, different profiles.
- There is one
internal/worker/app.Runand oneinternal/coordinator/app.Run;cmd/*only declares aProfile. - The bundle Coordinator exposes only
bundlecoordinator.v1.BundleCoordinatorService; the block Coordinator exposes onlycoordinator.v1.CoordinatorService. - The proxy separates the two traffic types by gRPC method path.
- There is one
- Invariant 3: stream build must be on in production. The bundle profile’s
TuneDefaultssetsStreamBuild.Enabled=true; an operator can still roll back to the fully materializedBuild()path with an explicitSTREAM_BUILD_ENABLED=false. - Invariant 4: static errors execute nothing, dynamic errors execute partially.
PrepareStreamcompletes all static validation in the Builder phase; on failure zero calls are dispatched. IfRunfails midway, earlier batches may already have executed. - Invariant 5: BatchWrite is the only write path, and it is strictly validated at startup. If
blockdb.batchWriteAddris empty whenBundleWrite(or the bundle profile’sTableUpserts) is registered, the process fails at startup rather than waiting for the Writer phase. - Invariant 6: when the Commit result is uncertain, do not call Cancel or create a replacement Job; return a non-reschedulable failure and keep the original
job_id.
Code location
Core types and interfaces
app.Profile(internal/worker/app/app.go):Deployment/Service/WorkerRegistryPrefix/UsageService(chaintable-usage service name, fixed by the entry point) /Builders/Plugins/TuneDefaults. The actual values for the bundle entry point:
app.Profile(internal/coordinator/app/app.go):Deployment/Service/WorkerRegistryPrefixes []string;registerCoordinatorServicedecides which gRPC service to register based onDeployment.- Registry prefixes (
api/etcd/registry.go):etcdapi.LegacyBundleWorkerRegistryPrefix=/blockx/bundle-workers/ProdDefaultBundleWorkerRegistryPrefix=/blockx/prod/lanes/default/bundle-workers/TestDefaultBundleWorkerRegistryPrefixis the default for test environments.ParseRegistryPrefixreturnsRegistryPrefix{Format, Environment, Lane, WorkerKind}.
adapters.BundleCoordinatorServer(internal/coordinator/adapters/bundle_grpc_server.go): only converts messages and callsCoordinatorServer.ReserveWorkerSlot.bundlescan.BundleScanBuilderDecl/BundleSource(internal/plugin/callbuilder/bundlescan/types.go): the on-wirefunctionCallConfig.config.- Bundle location field:
blockBundle.number - Source fields:
triggerSources[].tableId|bundleBucket|operator|function|code|param|forceResultCache - Missing-file marker:
NoBundleFile = "NO_BUNDLE_FILE"
- Bundle location field:
bundlescan.BundleScanBuilder(bundlescan.go/stream.go): implements bothtypes.CallBuilderandtypes.StreamingCallBuilder;SetStreamLimitsinjects the batch boundaries at startup.types.StreamingCallBuilder/CallStreamProducer/BatchEmit/ErrProductionStopped(internal/plugin/callbuilder/types/types.go): the two-phase streaming contract.bundlescan.DuckDBParquetReader/DuckDBParquetReaderConfig(duckdb.go):StreamBundleWithPlan(ctx, objectURI, bundleNumber, plan, yield)streams a bundle.- Filters are pushed down into
read_parquet(...) WHERE; dedup runs row by row on the Go side. - S3 credentials are initialized lazily. On
ExpiredTokenbefore anything has been yielded, the reader refreshes the secret and retries once.
bundlescan.BundleRowYield(parquet.go): the borrowed-row contract — the row and the string/[]byte values inside it are valid only during the callback.borrowed.Pool/borrowed.ScanResult(internal/duckdb/borrowed/): connection pool and chunk iteration.icebergsdk.ResolveDataFilesReq/IcebergIOAdapter/Planner.PlanDataFiles(internal/sdk/iceberg/):- Resolves all live files in the table’s current snapshot in one pass and encodes them as a bundle index.
LookupBundleFileIndex(data, bundle)selects a file from the index. See Backend Adapter for details.
batchwrite.Writer.Submit(ctx, taskIO, initReq, outputs, columns) (Result, error)(internal/plugin/event/batchwrite/writer.go):- Runs in this order: plan → Init → PUT → Commit.
- Returns
Result{JobID, RowCount, ContentBytes}. commitStatusUnknownErroralways satisfiesRetryable() == false.
bundlewrite.BundleWritePlugin/BundleWriteConfig{TargetTable, BlockBundle};tableupserts.NewPlugin(block) andtableupserts.NewBatchPlugin(bundle).
Data flow / execution flow
1
Reserve + Submit
The caller first invokes
BundleCoordinatorService.ReserveWorkerSlot on the bundle coordinator. After receiving worker_addr / slot_id, it submits SubmitTask to the target worker.Scheduling, slot TTL, and heartbeat match the block cluster. See Coordinator.2
Builder phase: PrepareStream (under builderSem, short)
BundleScanBuilder.PrepareStream deserializes BundleScanBuilderDecl. For each source, it calls prepareBundleSource to perform static preparation:- Resolve the data source:
resolveBundleSourceBucketusesbundleBucketdirectly when it is non-empty. Otherwise, it reads logical-types to identify an event or state table. State tables map to<tableId>._archive, thenResolveDataFilesReqresolves the unique parquet URI. - Handle missing data: when the result is
NO_BUNDLE_FILEor the snapshot does not contain the bundle, it skips the source and produces 0 calls. - Normalize the call: normalize inline code. For archived state tables, also remap
idin the operator tooriginal_id. - Validate the scan plan: dry-run
validateDuckDBReadPlanandToFilterSQLArgs, then prebuildParquetJSONEncoder. - Choose the cache policy:
markProvablyUniqueSourcesdecides which sources’ calls setNoResultCache.
BUILDER_FAILED, and no calls are dispatched.3
Calls phase: Run (under scanSem, long)
The Orchestrator starts
runCallProduction under scanSem:- Scan concurrently:
bundleScanProducer.Runuses an errgroup to scan at most 3 sources concurrently. - Build calls:
bundleRowCallBuilder.buildNextencodes each row into aCall. Row args are written directly into the argspool buffer withArgsPooled=true. - Split batches:
bundleSourceBatchAppenderrunsflushandemitwhen eithermaxRowsormaxArgsBytesreaches its limit. - Apply backpressure:
emitblocks onMaxOutstandingBatchescredits, limiting the number of unfinished batches.
emit returns ErrProductionStopped, the worker stopped in a controlled fast-fail path; this is not a builder failure.4
Dispatch / Execute
This is the shared path. See Call execution subsystem and Plugin system.
5
Writer phase: BatchWrite
After the Calls converge successfully,
BundleWritePlugin.Execute validates targetTable / blockBundle, reads the column definitions, and calls batchwrite.Writer.Submit:- Plan:
planDelimitedWriteRowswalks outputs without retaining them and computes the exact row and byte counts. More than 10,000,000 rows or 5,000,000,000 bytes fails before Init. - Init:
InitWriteJobcreates a Job and returnsjob_idanddata_url. - PUT: the encoder streams through
io.Pipein one HTTPPUTto the presigned URL. The request uses an exactContent-Lengthand does not follow redirects. - Commit: after a successful upload, call
CommitWriteJob(row_count). - Cancel: if PUT fails or ctx is canceled before Commit, call
CancelWriteJobon a best-effort basis. This call usescontext.WithoutCanceland an independent 5s timeout.
written_rows / finish_time / job_id. DebugMode only previews output and does not create a Job.TableUpsertsResultHandler uses different write paths on the two worker types:- bundle worker: reuses
batchwrite.WriterwithNormalTableUpsertStrategy{Condition, UpdateColumns}. - Validation before Init:
planNormalUpsertDelimitedWriteRowsrequires every row to have the same valid-field set, a non-emptyid, and at least one non-ID column. It writes theupdate_columnsderived from the first row explicitly into the request. - block worker: the handler with the same name still calls
TableWriteService.UpsertRows(sync=false). - Configuration boundary: the profile fixes the write path at assembly time. The task configuration has no
writeApi.
State and lifecycle
- Registry ownership: at worker startup,
Profile.Validateruns, thenvalidateWorkerRegistryPrefixruns again after config loading, guaranteeing that abundledeployment can only write to abundle-workers-kind prefix (legacy or v2). On the coordinator side,validateRegistryPrefixesdoes the same and also rejects duplicate prefixes. - Two-phase stream build:
- The Builder phase holds only builderSem and runs
PrepareStream. - The Calls phase holds scanSem and executorSem while production runs. It releases scanSem when production finishes or fails.
- One scanSem covers one task’s production process, which scans at most 3 sources concurrently. The worker-level DuckDB scan concurrency limit is about
ScanSlots × 3.
- The Builder phase holds only builderSem and runs
BatchWrite failure classification
Classification lives inbatchwrite/classified_error.go and presigned_upload.go. The plugin passes the classification through as PluginError.Retryable.
Iceberg resolution cache
ResolveDataFilesReq.CacheKey() uses only the logical table ID. Requests go through the Worker system cache. The default TTL is 20 minutes and is controlled by SystemIOCacheTTLms.
When the cached index’s high-water mark covers RequiredBundle, AcceptCachedRead reuses it. Otherwise, it refreshes the full table index once.
Configuration
The following configuration is most relevant to the bundle worker. Struct fields come frominternal/worker/app/config.go, internal/worker/core/config.go, and internal/worker/adapters/orchestrator.go.
Configuration precedence is: built-in defaults → TuneDefaults → WORKER_CONFIG file → environment variables.
bundle coordinator:
COORDINATOR_REGISTRY_PREFIXES (a comma-separated exact list) replaces the profile default [/blockx/bundle-workers/, /blockx/prod/lanes/default/bundle-workers/] as a whole; all other configuration is the same as the block coordinator.
Extension points
- Tuning bundle profile defaults: change
TuneDefaultsincmd/bundle_worker/main.go; it may only set defaults and must not post-process the final config (an operator’s explicit value must be able to override). Updatecmd/bundle_worker/profile_test.goandinternal/worker/app/profile_test.goaccordingly. - Adding or removing a Builder or Plugin for the bundle worker:
- Change the profile’s
Builders/Plugins. - Add or remove the corresponding case in
newBuilderRegistry/newPluginRegistryininternal/worker/app/app.go. - If a new plugin depends on BatchWrite, also add it to
validateProfileRuntimeConfig.
- Change the profile’s
- Changing bundlescan source semantics (new fields, new resolution paths):
bundlescan/types.go(decl) →source_resolution.go(resolution) →prepareBundleSourceinbundlescan.go(all static validation must live here, shared by both paths) →source_calls.go(call assembly). Do not put validation intoRun, or you break “static failure executes nothing”. - Changing DuckDB reads: SQL assembly, S3 secrets, filter pushdown, and dedup are in
bundlescan/duckdb.go; connection pool / row decoding / type support are ininternal/duckdb/borrowed/(no business policy there). - Changing the BatchWrite lifecycle (retries, upload method, capacity caps):
internal/plugin/event/batchwrite/. The BlockDB SDK keeps only the three atomic requestsInitWriteJobReq/CommitWriteJobReq/CancelWriteJobReq; do not push orchestration down into the SDK. - Adding a registry prefix format:
ParseRegistryPrefixinapi/etcd/registry.go;Validateon both sides picks it up automatically. - Adding a bundle coordinator RPC: change
api/grpc/bundlecoordinator/bundle_coordinator.proto→make(see the proto target in theMakefile) →internal/coordinator/adapters/bundle_grpc_server.goonly does conversion; scheduling logic stays in the sharedCoordinatorServer.
Testing
internal/plugin/callbuilder/bundlescan/*_test.go:bundlescan_test.go: Build path, source resolution, archive remapping.stream_test.go/TestBundleScanStream_*:MaxRows/MaxArgsBytesboundaries, single row over the limit, zero emits on static errors, zero emits from sibling sources, emit rejection stopping the scan, ctx cancellation.duckdb_test.go: reader configuration, handle release, connection pool cancellation.parquet_test.go/json_encoder_benchmark_test.go: parquet encoding and performance coverage.
internal/plugin/event/bundlewrite/bundle_write_test.goandbatchwrite/*_test.go: Init/PUT/Commit/Cancel state machine, error classification, Content-Length validation;tableupserts/table_upserts_test.gocovers both modes.cmd/bundle_worker/bundle_worker_process_test.go:TestBundleWorkerProcess_RejectsMissingBatchWriteAddrrequires startup to fail withoutBLOCKDB_BATCH_WRITE_ADDR.TestBundleWorkerProcess_BootsWithBundleProfilecovers startup, readiness, and registration of bundle-profile capabilities only.profile_test.gopins deployment / service / registry values.cmd/bundle_coordinator/*_test.gocovers the corresponding coordinator behavior.
internal/coordinator/adapters/bundle_grpc_server_test.go: the bundle adapter reuses shared scheduling and passes gRPC status through.- See Testing for an overview of test organization.
Related docs
docs/specs/ in the blockx repo:
docs/specs/2026-07-21-block-bundle-clusters.md— block / bundle deployment split: dual entry points, dual registries, system invariants (key).docs/specs/2026-07-28-registry-prefix-migration.md— legacy → v2 registry prefix format,WORKER_REGISTRY_PREFIX/COORDINATOR_REGISTRY_PREFIXESoverride rules.docs/specs/2026-07-23-bundle-coordinator-api.md— the decision on a standaloneBundleCoordinatorServiceand its rollout order.docs/specs/2026-07-30-bundle-write-batch-api.md— the BatchWrite write path’s execution flow, contract boundaries, failure state machine (normative).docs/specs/plugin-system.md§6.1 (StreamingCallBuildercontract, the fourNoResultCacheconditions), §10 (BlockBundleCallConfig/BlockBundleWriteResultHandler/TableUpsertsResultHandlercontracts).docs/specs/2026-07-30-ec2-worker-profiling.md— sampling plan for the two EC2 Worker clusters, block and bundle.
- Plugin system: shared Builder / Writer contracts.
- Worker: phase admission and orchestrator.
- Coordinator: scheduling and watcher.
- Call execution subsystem: shared Calls phase.
- IO access subsystem: system cache and retry classification.
- Backend Adapter: BlockDB / Iceberg clients.
- Deployment overview: process shapes and release methods.