Skip to content

[fence 4/9] libsql-server: source read fence for SQL, dump and replication streams - #44

Draft
tszymczyszyn-shopify wants to merge 3 commits into
namespace-fence/2-write-gatefrom
namespace-fence/3-read-fence
Draft

tszymczyszyn-shopify wants to merge 3 commits into
namespace-fence/2-write-gatefrom
namespace-fence/3-read-fence

Conversation

@tszymczyszyn-shopify

@tszymczyszyn-shopify tszymczyszyn-shopify commented Oct 5, 2026 •

Copy link
Copy Markdown

Adds read leases on the fence controller and the source read fence: new SQL reads are refused with MIGRATION_READ_FENCED, open reads are drained and cancelled at the deadline. Covers ATTACH (an attached namespace's fence is checked), the admin shell, and /beta/listen.

Dump and replication streams hold read leases too: a fenced dump stops before the next row and before its final COMMIT;, and log_entries/snapshot streams end with a typed status. Adds an optional HTTP/2 keepalive so a dead peer's stream can be detected.

Review focus

The lease linearisation (gate checked under the lease lock) and the stream cancellation paths.

Commits

  • libsql-server: source read fence for SQL with read leases
  • libsql-server: end dump and replication streams on the read fence
  • libsql-server: make the fenced snapshot stream test independent of compaction timing

Stack

Part 4 of 9, based on namespace-fence/2-write-gate. Retargeted from #35 with no feature change: applied in order, the 9 PRs carry #35's fence diff (stable patch ID 70d97d6a) on v0.9.30-shopify-patches. Review and land bottom-up, restacking after each squash or rebase merge.

shopify-river and others added 3 commits October 5, 2026 15:33
SetSourceReadFence now closes read admission in memory, persists
SOURCE_READ_DRAINING, and waits for every read lease already held before
it persists SOURCE_READ_FENCED. Leases are taken only after checking the
gate under the controller's lease lock, so once admission is closed the
set of leases can only shrink.

Each SQL program holds a lease for as long as it runs (including a Hrana
cursor still producing rows), as does describe and each admin shell
query. ATTACH of a namespace is a read of that namespace: the attaching
connection keeps a lease on it for every later program until it is
detached. A connection idle inside a transaction holds no lease; its next
program is refused with MIGRATION_READ_FENCED and rolled back.
/beta/listen is refused where reads are denied and ends when reads are
fenced.

At the deadline (--namespace-fence-default-read-drain-ms, 30s) running
programs are cancelled through the connection's progress-handler cancel
flag and report the read fence; the drain still waits for the actual
releases and answers DRAINING if they do not come, and a replay of the
same command resumes it. ClearSourceReadFence reopens reads with writes
still fenced. Dump and replication stream leases follow.

Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
Dump and replication now hold read leases on the namespace fence, so the
source read fence drains them positively:

- /dump is admitted by the fence gate before a connection is created (a
  failure to create one is an error, not a panic) and the export holds a
  dump lease. At the read drain's deadline the export is cancelled before
  its next row, and a write blocked on a peer that stopped reading fails at
  once, so the lease is released without the peer. A cancelled dump ends
  its body with the fence error and never reaches its final COMMIT;.
- Replication hello, log_entries, batch_log_entries and snapshot are
  refused at their start with FAILED_PRECONDITION and x-libsql-fence-code
  while streams are denied; refusals are counted and logged at most once a
  minute per namespace. Streams (and the frames of a batch) are served
  through FencedStream, whose watcher ends the stream as soon as the gate
  closes or the drain cancels it, dropping the inner stream and the lease
  itself; the next poll yields the typed terminal status.
- With --enable-namespace-fence, the RPC server and the user HTTP server
  send HTTP/2 keepalive pings (--namespace-fence-keepalive-interval-s,
  default 30 s, 20 s timeout) so dead peers are detected.

Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
…mpaction timing

`snapshot_stream_ends_typed` polled `get_snapshot_file(1)` and unwrapped
its result while the compactor was still writing the first snapshot and
the snapshot merger could be replacing merged files. The lookup lists
the snapshot directory and then opens the chosen file, so under load it
could fail with `NotFound` (directory not created yet, or a file removed
by a merge between the listing and the open) and the test panicked.

Open the stream through the `snapshot` RPC itself in a bounded loop that
treats only "snapshot not found" and the vanished-file error as "not
yet", and asserts that no read lease is held after a failed attempt. An
opened stream holds its file open, so a later merge cannot affect it.

Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants