internal/sdk/ (the directory name is historical; it contains clients for each service, not a published SDK). Each subpackage wraps one remote service (BlockDB gRPC, chain node JSON-RPC, Meta gRPC, Iceberg/Glue) as an iocore.BackendAdapter that the IO subsystem understands, and the Worker or sync-invoker assembles it into WorkerIOScope at startup. Builders, Writer Plugins, and Function Code View use them through TaskIO.Read/Write or by holding an adapter directly. See Architecture overview for the overall picture and IO access subsystem for the upper half of the call chain.
Responsibilities and boundaries
- Each SDK package does exactly one thing: translate
iocore.ReadReq/iocore.WriteReqinto one remote call and put the response intoReadResult.Data/WriteResult.Data. Request types are defined by the SDK package, not byinternal/io/core. - The SDK does no caching, no singleflight, no task windows, and no admission control. All of that lives in the IO subsystem; the SDK only provides
Registration(...)or anassembly.Moduleto help callers wrap the adapter inadaptive.WrapBackend. - The SDK is responsible for error classification: it implements
iocore.ErrorClassifier.ClassifyError(deciding retryable / non-retryable / param) andadaptive.OutcomeClassifier.ClassifyAdaptiveOutcome(deciding whether AIMD should shrink). Local context cancellation / timeout never counts as downstream overload. - The BlockDB SDK exposes only atomic RPCs. The BatchWrite Init → PUT → Commit orchestration lives in
internal/plugin/event/batchwrite, not in the SDK (docs/specs/2026-07-30-bundle-write-batch-api.md§2). - The bridge paths for the Python executor (BlockDB bridge, NodeRPC bridge, capability backends) only pass protobuf/JSON-RPC bytes through; they never rebuild request bodies.
- Connection policy belongs to the adapter instance: gRPC clients pointing at an NLB use
grpcclient.NewMultiConn, the same address within one adapter shares oneClientConn, and there is no process-level connection registry. internal/duckdb/borrowedonly provides a connection pool and result decoding on top of the native DuckDB bindings; SQL assembly, S3 credentials, and filter pushdown stay ininternal/plugin/callbuilder/bundlescan.
Code location
Core types and interfaces
All adapters implement the same interface (internal/io/core/model/adapter.go):
blockdb
AdapterConfig(adapter.go): one address field per gRPC service (TableReadAddr,TableWriteAddr,TableScanAddr,BlockReadAddr,BlockWriteAddr,BatchWriteAddr,BlockSubscribeAddr,L2BlockAddr,TimeReadAddr,TimeWriteAddr) plusMaxRecvMsgSize(0 means 32 MiB). Identical addresses share oneClientConn; an unconfigured service returnsblockdb:service_not_configured(a param-class error) when called. TheTableSubscribeAddrfield exists and is read fromBLOCKDB_TABLE_SUBSCRIBE_ADDR, butNewAdapternever dials it; it is only used ininternal/worker/app/function_code.goas one of the conditions for enabling the BlockDB Function Code View.Adapter(adapter.go): the typed backend,BackendBlockDB = "blockdb".Readuses a type switch to dispatchReadOp[*blockdbpb.GetRowRequest],BatchGetRowsRequest,FilterRowsRequest,GetBlockRowsRequest,GetEventRequest,GetStateRequest, andGetValueRequest, converting theResultSetinto a JSON row array; JSON/LIST/DICT logical columns are restored to objects according toColumnMeta.logical_type.WritedispatchesWriteOp[*UpsertRowsRequest],WriteOp[*DeleteRowsRequest],WriteOp[*UpsertTimeRowsRequest],BlockWriteReq,InitWriteJobReq,CommitWriteJobReq, andCancelWriteJobReq.ReadOp[T]/WriteOp[T](types.go): generic wrappers that turn any proto request into aniocore.ReadReq/WriteReq, carryingKey(cache key),Op(stats name), andRTO/WTO(timeouts).Filter,EqFilter,AndFilter, andFilterJSONgenerate parameterized WHERE clauses;Value(any)converts to*blockdbpb.Value.- Three table semantics (
proto/table.proto,block.proto,time.proto): Table (L1 row tables:GetRow/BatchGetRows/FilterRows/UpsertRows/DeleteRows/Scan), Block (L2 block-scoped:GetEvent/GetStatetakecurrent_block,UpsertBlocktakesBlock{block_id,height,timestamp},L2BlockReadService.GetBlockRows), and Time (GetValue(row_id,time_at),UpsertTimeRows(time_at)).BlockWriteReqis a multi-table L2 write:Data map[table][]*WriteRow, and the adapter issues oneUpsertBlockper table. - BatchWrite (
batch_write.go,proto/batch.proto):InitWriteJobReq/CommitWriteJobReq/CancelWriteJobReqeach wrap oneBatchWriteServicerequest, and the response is placed intoWriteResult.Dataas proto bytes. Uploading to the presigned URL, checkingjob_idconsistency, and similar orchestration live ininternal/plugin/event/batchwrite/writer.go. BridgeAdapter(bridge_adapter.go):BackendBlockDBBridge = "blockdb_executor". It receives the raw request protobuf sent by the Pythonblockdb_bridge.BridgeChannelthrough the UDS binary sidecar, extracts the gRPC method path withcapabilitywire.DecodeBinaryRequest, and forwards it according to thebridgeReadMethods/bridgeWriteMethodswhitelists;bridgeBlockedWriteMethodsrejects BatchWrite lifecycle calls initiated by function code; the streaming methodsScan/Subscribeare unsupported. Response bytes are placed intoReadResult.Dataunchanged.ScanClient/Adapter.ScanAll(online.go): streaming reads overTableScanService.Scan; Function Code View reuses the typedAdapter.ScanAll.SubscribeClientcurrently only has a constructor and holds the connection; it has noSubscribemethod.- Error classification (
adapter.go,adaptive.go):classifyBlockDBErrorparsesError NNNN (SQLSTATE)from the gRPC status into amysql:NNNN/proxysql:NNNNcode, then maps by gRPC code;classifyAdaptiveOutcometreatsResourceExhausted/DeadlineExceeded/Unavailable, transport errors, and a set of MySQL/TiDB/ProxySQL timeout phrases as overload. Registration(cfg, adaptive.Config, opts...)/BridgeRegistration(...): return aniocore.BackendRegistration;WithMultiConnConfiginjects agrpcclient.MultiConnConfig.- Test doubles:
DelayedGRPCStub(grpc_stub.go, mounted on a real gRPC server bycmd/blockdb_grpc_stub) andDelayedBridgeStubAdapter(bridge_stub.go, an in-process bridge stub enabled bySTUB_BLOCKDB_DELAY_MS).
noderpc
NodeRPCReq(types.go): a JSON-RPC 2.0 request invoked directly from Go,BackendNodeRPC = "rpc";Operation()returns the method andTimeout()is fixed at 10s.Adapter(adapter.go):ReadPOSTs the request body toendpoint; for bridge requests (arawBridgeRequestersent by the Python executor) it takesparams.chainfrom the JSON metadata, POSTs toendpoint/<chain>, and uses the sidecar bytes directly as the body.Writeis unsupported. Every request carries thex-load-deadline/x-load-priority/x-load-retriesheaders; an{"error":...}inside an HTTP 2xx response is parsed intojsonRPCError, and non-2xx becomesHTTPStatusError.HTTPClientConfig{TransportCount, RefreshIntervalMs}+rotatingHTTPTransport(transport.go): 8 independenthttp.Transportshards used round-robin, with one replaced smoothly every 30s;HTTPPoolSizesForAdmissionderives the idle connection budget from the admission initial / maximum concurrency.Registration(endpoint, adaptive.Config, opts...).- Semantic batching (
batching_backend.go,batch_core.go,batch_protocol.go,batch_config.go,batch_window*.go):BatchingBackendimplementsBackendAdapterand wraps outside admission (assembly.Module.OuterWrap).BatchConfig{Mode, MaxWait, MaxItems, FallbackCooldown},BatchMode=off|shadow|on,BatchConfigFromEnv(),NewBatchingBackend(inner, cfg, BatchObserver). The only batchable methods aregetAddressCode,getStorageAt,getAddressBalance, andgetAddressNonce, and the block context must be a fixed hash/height; the physical request is the internal methodblockx_stateReadBatchwith a BSRB/1 binary payload.batchobserved.Observer()provides the obs metric hooks. - Binary sidecar: the pass-through path for bridge requests is
Adapter.readFromBridge; a missingRawBinaryRequest()returnsnoderpc:invalid_bridge_requestdirectly, with no JSON-rebuild fallback. The Python side is inpython/blockx_executor/noderpc_bridge.py.
meta and logicaltypes
- The
go_packageininternal/sdk/meta/proto/*.protopoints at the blockdb repo;make proto-metauses--go_opt=M...to remap the three files intointernal/sdk/meta/gen(package namegen, usually aliased asmetapbby callers). logicaltypes.Adapter(logicaltypes/adapter.go):BackendLogicalTypes = "logicalTypes",ReadReq{TableID, RTimeout}→metapb.TableMetaServiceClient.GetTable→ JSON-encodedTableSchema{Type string, Columns []logical_types.Column}.ClassifyErrortreats schema / configuration gRPC codes as non-retryable.LocalStubAdapterreturns a local schema by table ID ("*"as the fallback).Registration(host, adaptive.Config, opts...).meta.Adapter(meta/meta.go): the legacy client for HTTP GET/api/v1/meta/get_table_metadata?id=,BackendMeta = "meta",MetaReadReq. No production code in the repo imports it; only the in-package unit tests and the//go:build livemeta_live_test.godo. The Worker’sMETA_ADDRgoes throughlogicaltypes.
iceberg
Config{Namespace, Region},NewPlanner(ctx, cfg)(planner.go): builds a Glue catalog using the AWS default credential chain (the instance role on EC2) and does not touch Glue/S3 at construction time. TheDataFilePlannerinterface has onlyPlanDataFiles(ctx, tableName) ([]string, error);Planner.PlanDataFilesloads the table and plans all data files of the current snapshot, and errors out immediately if it encounters a delete file.ResolveDataFilesReq{TableID, PhysicalTableID, RequiredBundle, RTimeout}(io_adapter.go):BackendIcebergResolve = "iceberg";CacheKey()uses only the logicalTableID. It implementsCachedReadValidator.AcceptCachedRead: the cached index is reused only if its high water is ≥RequiredBundle, otherwise a full-table refresh is triggered (corresponding to the commit “iceberg: refresh indexes above bundle high water”).IcebergIOAdapter: read-only; encodes the path list into aBFI1index (bundle_file_index.go: shared prefix + one record per bundle; two live files in the same bundle is an error).LookupBundleFileIndex(encoded, bundle)andBundleFileIndexHighWater(encoded)are used bybundlescanfor decoding.- The Worker registers it as a module with
SystemCache: trueandReadOnly: true; admission is the fixed concurrencyIO.SystemIOCacheRefreshConcurrency(default 2). Seedocs/deploy.md§3.3 in the blockx repo for the required IAM (glue:GetTable,s3:GetObject, pluskms:Decryptfor SSE-KMS).
localtestservice
Service(service.go) directly implements the generatedLocalTestServiceServer:HelloWorldreturns"hello, <name>";BlockDBGetRowissues oneReadOp[*blockdbpb.GetRowRequest]through the injectedBlockDBReader(the host’s raw blockdb adapter or a stub).Adapter(adapter.go):BackendLocalTestService = "localtestservice"; unwraps the capabilitywire binary envelope, runsproto.Unmarshalby method path, and callsServicein-process.Module(blockDB BlockDBReader) assembly.Module: read-only, fixed admission of 4.examples/local_test_service/is the Python-side end-to-end example (blockx-py’sLocalTestServiceclass → BridgeChannel → worker); seedocs/capability-backend-guide.mdin the blockx repo for a file-by-file template for adding a capability backend.
router
Adapter(router/adapter.go):BackendRouter = "router"; supports onlyrouterpkg.RouterFindFunctionsFullMethodName, and the business logic is inService.FindFunctionsof thegithub.com/chaintable/router/godependency.Module()is read-only with a fixed admission of 100.
grpcclient
New(target, opts...): an insecure ClientConn with DNSround_robin.NewMultiConn(target, MultiConnConfig, opts...): registers a custom balancer namedblockx_multi_connthat maintainsConnectionsSubConns inside oneClientConnand swaps one everyRefreshIntervalMs;DefaultMultiConnConfig()= 8 connections / 10000 ms.grpc.WithDisableServiceConfig()prevents the resolver-delivered service config from overriding the policy.DelimitedWriteRowEncoder(delimited_write_row.go): decides theValueoneof kind fromlogical_types.Columnand writes length-delimitedblockdb.v1.WriteRowbytes directly, without constructing the generated proto message; the BatchWrite upload file uses it.
duckdb/borrowed
Pool(pool.go):NewPool(db, maxOpen, maxIdle),Acquire(ctx),Release(conn),Close();WatchContext(ctx, conn)(context.go) propagates context cancellation to DuckDB throughduckdb_interrupt;ScanResult(ctx, res, yield)(rows.go) iterates chunk by chunk and hands VARCHAR/BLOB values to the callback as zero-copy views; the values become invalid once the callback returns, so copy them if you need to keep them.- The conversion logic is adapted from
duckdb-goand callsduckdb-go-bindingsdirectly;LICENSE.duckdb-go,LICENSE.duckdb-go-bindings, andREADME.mdrecord the origin and ownership.
cmd/blockdb_grpc_stub
main.gostarts a real gRPC server withblockdb.RegisterDelayedGRPCStub(registrar, readDelay): read RPCs wait-read-delayand then return a fixed token shape (GetRow,GetState), write RPCs return immediately;-probe-addressmode only probes an already-running stub and verifies its delay. Seecmd/blockdb_grpc_stub/README.mdfor its purpose and the Worker-side environment requirements.
Data flow / execution flow
1
Assembly
internal/worker/app/app.go (cmd/syncinvoker/io.go for the sync-invoker) declares each backend as an assembly.Module{Kind, Enabled, Build, Stub, Admission, OuterWrap}. When Enabled is false, Stub is used (e.g. devstub when NODE_RPC_ENDPOINT is empty, logicaltypes.LocalStubAdapter when META_ADDR is empty). assembly.Build uniformly applies the metrics-instrumented admission wrapper through adaptiveobserved.WrapBackendRegistration (internal/io/assembly/assembly.go).2
Request entry
Go callers construct an SDK request type (e.g.
blockdb.ReadOp[*blockdbpb.GetBlockRowsRequest]) and hand it to TaskIO.Read; Python callers’ gRPC/JSON-RPC bytes reach the Worker through the UDS sidecar and are wrapped into an IORequest that carries RawBinaryRequest(), whose Backend() is determined by the backend name on the wire.3
SDK execution
An adapter only looks at request types it recognizes and returns an error for anything else. Before a gRPC call the client id is propagated with
clientid.IntoOutgoingGRPC(ctx); HTTP uses clientid.IntoHTTP.4
Error return path
The error returned by the adapter is first fed to AIMD through
ClassifyAdaptiveOutcome, then turned into an IOError through ClassifyError, which decides retries at the Call/IO layer. Both treat local context termination as Ignore / timeout rather than downstream overload.State and lifecycle
- gRPC connections: the
NewMultiConnbalancer first creates a candidate SubConn for each slot, and only promotes it and drains the old connection once it is READY; the first refresh is randomly delayed within(0, refreshInterval].Adapter.Close()closes allClientConns. - NodeRPC transport:
rotatingHTTPTransportretires one shard every refresh interval; a retired shard waits for in-flight requests to finish before closing;Close()rejects new requests and waits for everything to drain. - Batching groups:
groupCollecting → groupFlushing → groupDone; requests with the same chain, same fixed block, same timeout, and same client id join the same group, and a flush is triggered byMaxWait(default 500µs, timed with timerfd on Linux) orMaxItems(default 16). Receiving-32601enters a Worker-level cooldown (default 60s), after which only one half-open probe batch is let through.Close()fails the groups still collecting and cancels in-flight physical requests. - Idempotency: every BlockDB RPC is idempotent, so the IO layer can do limited retries of Init/Commit/Cancel in the Result phase; NodeRPC has only the Read path.
Configuration
The Worker fields are ininternal/worker/app/config.go (WorkerFullConfig), the environment variable overrides are in internal/worker/app/app.go, and defaults come from DefaultWorkerFullConfig().
Extension points
- Add a BlockDB RPC: edit
internal/sdk/blockdb/proto/*.proto→make proto-blockdb→ add aReadOp[*NewRequest]branch inAdapter.dispatchRead/dispatchWrite→ if it should be exposed to Python, add theFullMethodNametobridgeReadMethods/bridgeWriteMethodsinbridge_adapter.go→ add cases inblockdb_test.go(bufconn + gomock server). - Add a backend: implement
BackendAdapter(optionallyErrorClassifierandOutcomeClassifier), provide aRegistrationorModule, then add a declaration tobackendModulesininternal/worker/app/app.go. For a gRPC-style capability backend, copyinternal/sdk/localtestservicedirectly; the steps are indocs/capability-backend-guide.md. - Add a batchable NodeRPC method: edit
batchMethodsinbatch_core.goand the BSRB kind inbatch_protocol.go, change the Leafage-side handler at the same time, and follow the version evolution rules indocs/specs/noderpc-semantic-batching.md§7.3. - Adjust connection policy: only change the
grpcclient.MultiConnConfigof the corresponding adapter; do not add a global connection pool. - Regenerate proto: requires
protoc,protoc-gen-go, andprotoc-gen-go-grpcin$(go env GOPATH)/bin(the install commands are at the top of theMakefile).make protofirst runsproto-blockdb,proto-meta, andproto-localtestservice, then generatesapi/grpc/*. All three SDK targets userequire_unimplemented_servers=false.
Testing
blockdb:blockdb_test.gostarts a gRPC server withbufconn, andmock_server_test.gois the mockgen-generated service mock;partial_config_test.gocovers clean failures for unconfigured services;batch_write_test.goverifies that the proto matches the BlockDB repo contract;bridge_*_test.gocovers the bridge whitelists and the raw-bytes path.noderpc:noderpc_test.gouseshttptest.Server;batching_backend_test.gocovers cross-task batching, error isolation, the-32601fallback, and the cooldown probe;batch_window_linux_test.gocallst.Skipwhen timerfd is unavailable.- Tests that need external services are skipped:
TestAdapter_ReadRealneedsNODERPC_REAL_URL(and not-short);meta_live_test.goneeds-tags liveandMETA_API_URL. CI’smake testuses-short, so it never touches real services. iceberg:planner_test.goonly tests delete-file rejection and index encoding; it does not access Glue.duckdb/borrowed:pool_test.go,cells_test.go, androws_test.goneed cgo and the DuckDB bindings, and compile as part ofgo test.- E2E:
e2e/system/logical_type/starts a local Meta gRPC service withmetapbto test logical types and dbscan; process-level tests each start their own mock gRPC server (cmd/worker/worker_process_blockdb_bridge_test.goimplementsTableReadServiceServer,cmd/worker/worker_process_function_code_test.gousesinternal/testutil.StartMockFunctionBlockDB); performance benchmarks usecmd/blockdb_grpc_stub.
Related docs
- blockx repo
docs/specs/2026-07-30-bundle-write-batch-api.md: the BatchWrite Job lifecycle; the SDK only provides the three atomic RPCs. - blockx repo
docs/specs/noderpc-semantic-batching.md: admission rules, group state machine, BSRB/1 protocol, configuration, and fallback for the batching facade. - blockx repo
docs/specs/2026-08-06-noderpc-binary-sidecar.md: NodeRPC bridge binary pass-through. - blockx repo
docs/specs/2026-07-23-grpc-client-balancing.md: the design ofNewMultiConnand per-component configuration. - blockx repo
docs/specs/io-subsystem.md,docs/io-backend-module-design.md,docs/capability-backend-guide.md: the backend adapter contract and the guide for adding a backend. - blockx repo
docs/deploy.md§3.3 (Iceberg IAM), §9.2 (Worker environment variables). - On this site: IO access subsystem, Plugin system, Function Code View, Python Executor, Bundle clusters, Protocols and interfaces, Testing.