poc(kernel): pure-Go (CGO_ENABLED=0) dynamic loader — control + data plane, no latency regression - #438
Draft
msrathore-db wants to merge 3 commits into
Draft
poc(kernel): pure-Go (CGO_ENABLED=0) dynamic loader — control + data plane, no latency regression#438msrathore-db wants to merge 3 commits into
msrathore-db wants to merge 3 commits into
Conversation
Proof-of-concept that the closed-source SEA kernel can be driven from a PURE-Go binary — no cgo, no C compiler, no static linking — by dlopen-ing the kernel SHARED library at run time via ebitengine/purego, instead of static-linking libdatabricks_sql_kernel.a through cgo at build time. Why: the shipped kernel backend links a static .a via cgo, which forces CGO_ENABLED=1, a C toolchain on every builder, and breaks Go's free cross-compilation — the three blockers the SEA/kernel release design calls out against ever making SEA the default backend. Dynamic loading removes all three. It's the model gosnowflake uses for its own closed-source native core. Verified live against a warehouse, built CGO_ENABLED=0: dlopen -> config -> session_open -> execute -> real server query id -> teardown, all with no cgo. No kernel source change is needed — the kernel's Cargo.toml already declares crate-type cdylib and the C ABI fns are #[no_mangle] extern "C", so the .dylib already exports them. No user CUJ change: WithUseKernel and the existing build tags are untouched; this lives behind a NEW, separate tag (databricks_kernel_dynamic) and the default pure-Go build does not pull in purego. Scope: CONTROL plane only (DML/DDL happy path). The DATA plane (Arrow result batches) is deliberately excluded because arrow-go/v12's cdata importer is itself a cgo package — closing it needs a separate decision (purego C-Data importer, arrow-go v18, or a thin cgo shim). Documented in DYNAMIC_LOADER_POC.md along with the run-time lib-discovery and glibc/musl follow-ups. PoC-only shortcuts (not for merge as-is): a local `replace` for purego so the branch builds offline, dylib path via env var, and a hand-mirrored KernelError struct layout. All flagged in the doc. Co-authored-by: Isaac
Extends the CGO_ENABLED=0 purego kernel PoC from control-plane-only to a FULL working path, including result-row fetching, verified live on pecotesting with a head-to-head latency benchmark. Data plane (the previously-blocked part): arrow-go v12's cdata importer is cgo, so it can't be used from CGO_ENABLED=0. cdata_pure.go is a pure-Go port of that importer's C-Data *import* path — it reads the flat ArrowSchema/ArrowArray structs via unsafe, invokes each struct's release callback with purego.SyscallN, and builds arrow.Records zero-copy with array.NewData. dynamic_rows.go pulls batches through the dlopen'd kernel_result_stream_* and scans them with the SAME arrowscan scanner the cgo rows.go uses, so values are identical to the cgo backend by construction. Verified live on pecotesting (TestDynamicLoaderDataPlane, CGO_ENABLED=0): scalars, null, decimal-as-exact-string, high-precision DECIMAL(38,2) returning byte-exact (proves buffer import correctness), temporal, binary, nested array/map/struct as JSON, empty result set, and 100k rows across multiple batches. Latency (BenchmarkDynLargeResult vs BenchmarkCgoLargeResult, identical 500k-row query, both kernel artifacts built from the same pinned rev with tls-rustls): cgo static 7.91s/op, purego dynamic 6.17-8.59s/op — end to end dominated by warehouse+network, statistically indistinguishable, no regression. The purego per-cell path crosses no cgo boundary. Test matrix, all green: default pure-Go suite; dynamic-tagged (CGO_ENABLED=0) unit + live e2e; cgo-tagged (CGO_ENABLED=1) unit + full live TestKernelE2E* (0 failures, confirms the existing static path is not regressed). New files (all behind databricks_kernel_dynamic except the cgo bench, which is behind cgo && databricks_kernel): - cdata_pure.go / cdata_pure_finalizer.go: pure-Go C-Data importer - dynamic_rows.go: driver.Rows over the pure-Go importer - dynamic_bench_test.go / cgo_bench.go / cgo_bench_test.go: head-to-head benchmarks Still PoC (unchanged from prior commit): local purego replace for offline builds, dylib path via env, hand-mirrored structs. Documented in DYNAMIC_LOADER_POC.md. Co-authored-by: Isaac
Follow-up to the kernel work: with cgo accepted and the Arrow import path settled on cgo + arrow-go cdata (zero-copy C-Data, the ADBC driver-manager model), this PoCs the remaining open question — packaging the closed-source kernel as a SHARED library loaded at run time instead of a static .a baked into every binary. New build tag databricks_kernel_dynlib (added alongside databricks_kernel) selects cgo_dynlib_darwin.go, which links libdatabricks_sql_kernel.dylib with -l + an rpath instead of naming the .a. The static cgo_darwin.go is guarded with !databricks_kernel_dynlib so the two never both compile. The Arrow C-Data import path (rows.go, arrow-go cdata) is unchanged — dynamic vs static is invisible above the link layer. Verified live on pecotesting (darwin/arm64): - otool -L shows the binary references @rpath/libdatabricks_sql_kernel.dylib externally (not baked in); binary size drops ~61MB (static .a) -> ~12MB. - 20/20 TestKernelE2E* subtests pass through the runtime-loaded dylib. - Negative proof: moving the dylib away fails at load with "dyld: Library not loaded: @rpath/libdatabricks_sql_kernel.dylib" and prints the rpath search order; restoring it works again. - Static .a path and default pure-Go build both still build unchanged. One real gotcha surfaced + fixed: cargo's default dylib install_name is the absolute build path (not relocatable); it must be set to @rpath/libdatabricks_sql_kernel.dylib (install_name_tool -id, or a link arg in the kernel build). DYNAMIC_LINK_RELEASE.md documents the full release plan: .so publishing on the kernel release line (serves ODBC too), soname/major + a kernel_abi_version() load check for versioning, why ODBC and Go can run different kernel versions (separate processes), and a phased static->dynamic recommendation that keeps the CGO_ENABLED=0 pure-Go Thrift fallback intact (no user CUJ change). PoC scope: darwin/arm64 only (linux $ORIGIN + windows DLL noted as the remaining per-OS work); dylib staged locally and gitignored. Co-authored-by: Isaac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this proves
The shipped kernel backend static-links
libdatabricks_sql_kernel.athrough cgo, forcingCGO_ENABLED=1, a C toolchain on every builder, and no cross-compilation — the blockers the SEA/kernel release design flags against making SEA the default. This PoC drives the closed-source kernel from a pure-Go (CGO_ENABLED=0) binary bydlopen-ing the kernel shared library at run time viapurego— the modelgosnowflakeuses for its own closed-source core.Now complete: both control plane AND data plane, verified live on pecotesting.
Control plane
dlopen → config → session open → execute → query-id / affected-rows → teardown. No cgo.
Data plane (the previously-blocked part — now solved)
arrow-go v12's
cdataimporter is itself cgo, so it can't run underCGO_ENABLED=0.cdata_pure.gois a pure-Go port of that importer's C-Data import path: reads the flatArrowSchema/ArrowArraystructs viaunsafe, invokes each struct'sreleasecallback viapurego.SyscallN, buildsarrow.Records zero-copy witharray.NewData.dynamic_rows.gopulls batches through the dlopen'dkernel_result_stream_*and scans with the samearrowscanscanner the cgo path uses — so values are identical by construction.Verified live (
TestDynamicLoaderDataPlane,CGO_ENABLED=0): scalars, null, decimal-as-exact-string, high-precisionDECIMAL(38,2)byte-exact (9999999999999999999999999999.99— proves buffer import correctness), date/timestamp/binary/float, nested array/map/struct as JSON, empty result set, and 100k rows across batches.Latency: no regression (head-to-head on pecotesting)
Identical 500k-row × 3-col query, same drain loop, same scanner; both kernel artifacts built from the same pinned rev with tls-rustls.
BenchmarkCgoLargeResultvsBenchmarkDynLargeResult:End-to-end dominated by warehouse + network → statistically indistinguishable; purego is never meaningfully slower and crosses no per-cell cgo boundary. No regression.
Test matrix (all green)
CGO_ENABLED=0, no tags):go test ./...— pass.CGO_ENABLED=0 -tags databricks_kernel_dynamic): unit pass; e2e/data-plane pass live on pecotesting.CGO_ENABLED=1 -tags databricks_kernel): unit pass; fullTestKernelE2E*passes live on pecotesting, 0 failures — confirms the existing static path is not regressed.No user CUJ change
WithUseKerneland the existing build tags are untouched. The loader lives behind a new, separate tagdatabricks_kernel_dynamic; the default pure-Go build is unchanged and does not pull in purego. No kernel source change (itsCargo.tomlalready declarescdylib).Files
dynamic_loader.go— purego dlopen + C ABI binding (control plane + result stream)cdata_pure.go,cdata_pure_finalizer.go— pure-Go Arrow C-Data importerdynamic_rows.go—driver.Rowsover the pure-Go importerdynamic_bench_test.go,cgo_bench.go,cgo_bench_test.go— head-to-head benchmarksdynamic_loader_test.go— control + data-plane e2eDYNAMIC_LOADER_POC.md— full writeupStill PoC (must change before merge)
replacefor purego so the branch builds offline (real PR pinsv0.10.2via proxy).cKernelError/C-Data struct layouts (real PR addsunsafe.Sizeof/Offsetofasserts).cdata_pure.goports the type cases the kernel emits; union/run-end-encoded and the ArrowArrayStream reader are intentionally omitted (kernel uses next_batch pull).This pull request and its description were written by Isaac.