StructuredBatch skips a checkpoint another connection's write refuses - #289
Merged
Merged
Conversation
The bluesky demo's first deploy of v2026.09.14 exited with: error reinitializing handler: checkpoint after truncate: Cannot CHECKPOINT: there are other write transactions active StructuredBatch checkpoints after every batch to reclaim the rows its truncate left (#247). DuckDB refuses a checkpoint while another connection holds an uncommitted UPDATE or DDL; an uncommitted INSERT or DELETE does not refuse it. #281 moved the window to connections of its own. Its watermark save is an UPDATE, and the sqlcommand sink drops and creates its batch table. A batch that re-initialised while either was open failed, and a failed batch stops the process. #283's rule, that every statement on the pipeline's connection holds its lock, stopped reaching the window when it moved, and no invariant said so. Init now skips a refused checkpoint; the next batch's checkpoint reclaims what it left. Any other checkpoint error still fails the batch. Each skip logs at debug and counts in handler_checkpoints_skipped_total, so a write held open for longer than a batch shows as a rising count rather than as unexplained memory growth. The enforced invariant pipeline.batch.independent_of_window_io says a window's sink write or close, however long, never fails the consume loop. The manager harness proves it for manager.watermark. It holds the window's flush open and runs three batches through the structured handler, then holds a close after its delete and watermark update, before the commit, and runs three more. Against the release code the second hold fails batch 1 with the production error. With the fix both holds pass, and both closes land once released. TestStructuredInit_SkipsACheckpointAnotherWriterHolds holds an uncommitted UPDATE on a second connection for 50 batches. Init returns no error, and table storage grows to 4,422 KiB. One batch after the writer commits, storage is back to 0 KiB. TestCoreConsumeLoop_CountsSkippedCheckpoints checks the counter. If this is wrong, a windowed StructuredBatch pipeline still exits when a close coincides with a batch, or skips checkpoints forever and grows the way #247 did, with the counter rising.
…base Review of #289: initHandler released the pipeline lock by hand so it could read CheckpointSkipped after Init. A panic inside Init would have left the lock held, and every later statement on the pipeline's connection would block on it. The defer is back, and the flag is read under the lock, where it is the one this Init set. TestStructuredInit_SkipsACheckpointAnotherWriterHolds repeated newADBCConn's libduckdb fallback to reach a second connection. openTestDatabase holds it once, and both use it. go test -short on core, handlers, managers and cli/run: pass.
turbolytics
force-pushed
the
fix/structured-checkpoint-busy
branch
from
September 14, 2026 15:17
5deb256 to
d025729
Compare
This was referenced Sep 14, 2026
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 changes
The bluesky demo's first deploy of v2026.09.14 exited minutes after starting:
handlers.StructuredBatchcheckpoints after every batch to reclaim the rows its truncate left (#247). DuckDB refuses a checkpoint while another connection holds an uncommitted UPDATE or DDL. #281 moved the window to connections of its own: its watermark save is an UPDATE, and thesqlcommandsink drops and createssqlflow_sink_batch. A batch that re-initialised while either was open failed, and a failed batch stops the process.Initskips a refused checkpoint, and the next batch's checkpoint reclaims what it left. Any other checkpoint error still fails the batch.handler_checkpoints_skipped_total. A write held open for longer than a batch shows as a rising count, not as memory growth nobody can explain.pipeline.batch.independent_of_window_io, enforced: a window's sink write or close, however long, never fails the consume loop. It applies tomanager.watermark, and the manager harness proves it with the structured handler. Every statement on the pipeline's DuckDB connection holds the shared … #283's rule, every statement on the pipeline's connection holds its lock, stopped reaching the window when it moved, and no row said so. A dropped check now leaves an enforced cell uncovered, andmake coverage-checkfails.If this is wrong, a windowed
StructuredBatchpipeline still exits when a close coincides with a batch. Or it skips checkpoints indefinitely and grows the way #247 did, with the counter rising.Verification
go test -short -race ./...: exit 0.go vet ./...is clean.uv run --locked pytest tests/tooling -q: 205 passed.make coverage-page && git status --short docs/coverage: clean after the commit. The page gains the invariant row,✅ uformanager.watermark, and the status file gains its line.uv run --locked pytest tests/release -q: not run. No flag, command or build stamp changed. The new series is asserted byTestExportedSeriesNames.make soakagainstturbolytics/sql-flow:checkpoint-busy-test, built from this branch at 5deb256, before d025729 restored the deferred unlock ininitHandler: PASS, verdict below. The soak driveshandlers.InferredMemBatch, so it measures the consume loop'sinitHandlerchange, not the skip.What refuses a checkpoint. A probe against DuckDB 1.5.2: a statement on a second connection, left uncommitted, then
CHECKPOINTon the first.CHECKPOINTINSERT, including a long-running oneDELETEof committed rowsUPDATEof committed rowsCREATE TABLE,DROP TABLECheckpoints refused in 3 seconds of the loop's shape (insert, truncate, checkpoint) beside each workload:
UPDATEthenCOMMITin a loopsqlcommandsinkTests that fail without the fix:
pipeline.batch.independent_of_window_iofirst holds the window's flush open and runs three batches. Then it holds a close after its delete and watermark UPDATE, before the commit, and runs three more. Againstmain:TestStructuredInit_SkipsACheckpointAnotherWriterHoldsholds an uncommitted UPDATE on a second connection for 50 batches.Initreturns no error andCheckpointSkipped()is true. Storage grows from 0 to 4,422 KiB; one batch after the writer commits, it is back to 0 KiB.TestCoreConsumeLoop_CountsSkippedCheckpoints: a handler that reports skips increments the counter, and one that does not records nothing.Soak verdict
A first run printed PASS over a dead process and does not count. Docker's disk filled at minute 7, the Kafka broker exited with
No space left on device, and the analyzer read the missing samples from minutes 7 to 10 as 0.0 MiB. The run above had every sample, the container logged no errors, and the broker stayed up.End to end
The bluesky demo's worker config on the same image, against compose Postgres, capped at
--cpus 0.5like the Render plan, from 14:30 to 15:47 UTC:ERRORline.handler_checkpoints_skipped_total1. That collision is the one that stopped the demo on v2026.09.14; here the batch continued.Notes for the reviewer
FORCE CHECKPOINTaborts the other transactions, so the window's close would fail. A retry loop would stall the consume loop behind a sink's network write. The skip costs one batch of reclamation.checkpointRefusedmatches the message text.HoldCommitwraps the manager's connection so its commit waits.NewWatermarktakes the transaction from the connection, so the harness can stop a close at that point without a hook in the manager.## Unreleasedabove## v2026.09.14, as Windows: reemit publishes the late rows alone, so the Postgres example drops #288 does. Whichever merges second keeps both entries.