spec: a pool of serve sessions behind an executor interface - #320
Closed
turbolytics wants to merge 4 commits into
Closed
turbolytics wants to merge 4 commits into
turbolytics wants to merge 4 commits into
Conversation
6 tasks
Owner
Author
|
Implemented in #322, which contains these spec and plan commits. Closing this in favour of that one once it is reviewed. |
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.
Spec only. No code, no config, nothing shipped.
What this changes
sqlflow serveanswers one request at a time: every request waits on one mutex around one DuckDB connection. Measured against the Bluesky demo on Render on 2026-09-16, after rollup tables cut the query itself to 13 ms:Throughput stops climbing at four clients and latency grows in proportion after that. The same request reports
elapsed_msof 13 idle and 945–1,219 under sixteen clients: the query is unchanged, and the rest is queueing counted as work. Before #295 the same test failed 66% of requests at sixteen clients, so cheaper queries raised the ceiling from 1.1/s to 13/s; only concurrency raises it again.The spec proposes a pool of sessions behind an
Executorinterface, plus the metrics that size it, since serve records none today.The parts worth reviewing
Executor,SessionandStatementinterfaces ininternal/serve/executor.go, withinternal/serve/duckdb.gothe only file importing ADBC. The interface exists because the engine underneath may change: the Postgres sink had to leave DuckDB's postgres extension for pgx in A keyed Postgres sink on pgx, and the leak loops that found the demo's growth #290, and that move was expensive because the driver was welded into the write path.Session.Runreturnsarray.RecordReaderand serve keeps one encoder. Arrow is sqlflow's in-process format, and the four documented rendering rules (UTC timestamps, naive timestamps, exact decimals, NaN as a string) each exist because Arrow's own rendering was wrong for JSON. One encoder is how they stay true across backends. The spec states the costs: a non-Arrow backend pays a conversion,columns[].typeis a SQL name reconstructed from an Arrow type, and buffer lifetimes become part of the interface.SET TimeZone='UTC'on every session, rather than trusting the config to reach them.GET /metricson the existing listener, since Render routes one port, but the labels name every dataset and the port is public.Measurements in the spec
DuckDB 1.5.2 through ADBC, two connections to one in-memory database with a Postgres attached:
ATTACHon connection 1pg.*without attachingATTACHthe same alias againBinder Error: database with name "pg" already existsSET TimeZone='UTC'on connection 1America/New_York, the host zoneSET pg_connection_limit = 4on connection 1So
commandsmust run once, and the timezone must be set per session or a correct config returns buckets in the host zone.Resident memory,
memory_limit='128MB', N sessions each running the demo's fold at once:Four sessions fit a 256 MB box. The bounds from #295 are what make that true:
memory_limitis one budget shared by every session, so without a cap on what a request may read, concurrent requests fail together rather than queueing.Verification
go build ./...andgo test -short ./...unaffected.uv run --locked pytest tests/tooling -q— no registry or status file changedmake coverage-page— no feature status changeduv run --locked pytest tests/release -q— no CLI surface changedmake soak— no allocating path changedNotes for the reviewer
Maxrssis bytes and on Linux it is KiB, so build order task 1 repeats the measurement in the release container and changes the default if it says so. And whether an attached Postgres shares its connections across sessions or holds them per session is an integration test, not an assumption.session_commandsblock, so the timezone is the only per-session setting until something needs more.instances × pool.size × pg_connection_limitagainst one database, and instances share nothing, so each pulls the same hot rows for the same popular ranges. Scaling out today trades a bottleneck for a worse one. That reframes caching as the prerequisite for horizontal scale rather than a latency optimisation.