Parse block payloads with hyperpb instead of dynamicpb - #873
Closed
sduchesneau wants to merge 0 commit into
Closed
Conversation
sduchesneau
force-pushed
the
feature/sink-sql-hyperpb
branch
3 times, most recently
from
August 12, 2026 00:26
b999755 to
c02144f
Compare
Contributor
Author
sduchesneau
force-pushed
the
feature/sink-sql-hyperpb
branch
from
August 12, 2026 00:46
c02144f to
6fb8c49
Compare
Contributor
Author
An error occurred while trying to automatically change base from
feature/sink-sql-local-cache
to
develop
August 17, 2026 18:45
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.
Stacked on #869.
The from-proto sink only ever has the module's descriptor at runtime, never a generated Go type, so every block payload goes through a dynamic parser. It used protobuf-go's
dynamicpb. This switches it to hyperpb, which compiles the descriptor into a parser once and then parses into an arena.The descriptor walk only ever read the message —
Descriptor,Get,IsValidand nothing else — so it now takes aprotoreflect.Messageand the decoder chooses the parser. That is the whole structural change; the walk's behaviour is untouched.Numbers
M4 Max, 200 entities per payload.
BenchmarkUnmarshal, parse in isolation:TestClientDecodeScaling, the real per-block work (parse + walk + buffer), wide entity, 16 cores:18x on the parse becomes 1.9x on the full decode path and 2.7x across the pool — the parse was about half the wide-entity cost and is now about a twentieth, so the walk is what is left (~90%).
Two conclusions in
benchmarks/README.mdchanged and have been rewritten rather than appended to:Constraints, and how they are handled
hyperpb is not a drop-in in every respect, and each of these is a panic rather than an error, so each is pinned by a test in
benchmarks/parser_test.go:TestHyperpbRejectsMutationfails loudly if a write is ever added to that path.attempted to parse message using in-use Context. This is why the decoder keeps one arena per block slot, not the one-per-worker arrangement that looks natural.Freeon an arena nothing was allocated in panics on a negative slice bound. A block whose module produced no output never parses, so a slot really can reach its first free untouched —decoder.arena.usedcovers it.BufferedInserteruntilapplyreplays them. So an arena is freed at the top of the nextdecodeAll, by which point the previous round has been applied and flushed.govalidatoroverflows onlinux/386ondeveloptoday).Breaking
sql.Database.WalkMessageDescriptorAndInsert,WalkMessageDescriptorAndInsertInto,BaseDatabase.WalkMessageDescriptorAndInsertWithDialectandsql.Dialect.AppendInlineFieldValuestake aprotoreflect.Messagewhere they took a*dynamicpb.Message.tui/print.goandpb/sf/substreams/v1/package.gostill usedynamicpband should: they arejsonpb.AnyResolverimplementations that hand an empty message to a JSON decoder to unmarshal into, which is mutation. hyperpb cannot serve that.Testing
TestParsersAgree— dynamicpb and hyperpb produce byte-identicalprotojsonoutput for both payload shapes, read purely through protoreflect, the same interface the walk uses. Ungated: milliseconds, no container.TestHyperpbArenaContract— pins the three arena rules above.sink/sqlsuite green withSF_SINK_SQL_INTEGRATION_TESTS=true, Postgres and ClickHouse, includingTestDbProtoPostgresDecodeWorkers(1 worker and 8 workers must produce identical tables, in block order).Also on this branch
Two commits that are not about the parser:
Stop the sink/sql integration containers dying mid-suite in CI—go test ./...builds several packages that each start their own containers and runs them in parallel, so more than one test binary was connected to the shared Ryuk reaper at a time. Runs failed with every container in one package going unreachable part-way through the suite — tests that had already passed, thenconnection refusedon both the Postgres and the ClickHouse mapped port — right as a different package's binary exited.TESTCONTAINERS_RYUK_DISABLEDis now set in CI: Ryuk buys nothing there, since the runner is destroyed when the job ends.Always run the sql integration tests—TestMainno longer checksSF_SINK_SQL_INTEGRATION_TESTSandos.Exit(0)s when it is unset. The suite needs a container runtime anyway, and a variable that quietly skips half of it only hides a broken environment behind a green run. Same reasoning as the container correctness test in Sink from-proto through a disk spool, and load without constraints #869. The variable now has no readers left in the repo and its export is gone frombin/test.sh; the workflow runs the tests on ubuntu only, since the macOS runner has no container runtime and keeps the build step.🤖 Generated with Claude Code