Skip to content

[fence 5/9] libsql-server: migration target lifecycle and import capability - #45

Closed
tszymczyszyn-shopify wants to merge 9 commits into
namespace-fence/3-read-fencefrom
namespace-fence/4-target-lifecycle
Closed

tszymczyszyn-shopify wants to merge 9 commits into
namespace-fence/3-read-fencefrom
namespace-fence/4-target-lifecycle

Conversation

@tszymczyszyn-shopify

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

Copy link
Copy Markdown

Parked (2026-10-14). We are not pursuing this stack for now, so please do not review or merge it. The existing per-namespace block_writes and block_reads settings, together with operator checks, cover what we need today. The branches stay up for reference.

If we need more server support later, the likely follow-ups are small, separate changes: refusing replication and /dump while reads are blocked, a conditional POST /config, and block flags at namespace creation.


Creates migration targets in quarantine, issues operation-owned import and validation capabilities, and implements the seal drain, validation, publication (readable, writes fenced) and the irreversible EnableTargetWrites. open_import_session into a quarantined target is the internal API that bulk import builds on.

Also includes, next to the code they cover:

  • the replay/resume fix (stale DRAINING receipts stay historical; resumed attribution is preserved);
  • the legacy block_* mirror fix and its tests, plus crash-boundary tests for the read fence, seal and write enable;
  • the admin-shell quarantine acceptance test.

Review focus

Capability checks at the WAL (only the owning operation's import capability can write a quarantined target) and the seal/validate/publish/enable transitions.

Since the rebase onto #53: ImportSession loads through namespace::dump_import (buffered by default; load_dump_with selects the streaming importer and returns its statistics), and a migration target is refused over a name that a create, fork or reset still holds reserved.

Commits

Stack

Part 5 of 9, based on namespace-fence/3-read-fence. The stack sits on #53 (tszymczyszyn/atomic-namespace-create), which sits on #51: review and land #51 → #53 → #41 → … → #49 bottom-up, restacking after each squash or rebase merge.

The first version of these PRs carried #35's fence diff unchanged on v0.9.30-shopify-patches. On 2026-10-13 the stack was rebased onto #53 (how the conflicts were resolved is summarised in a comment on #41) and gained the commits marked above.

@tszymczyszyn-shopify

Copy link
Copy Markdown
Author

New in this PR after the rebase onto #53 (summary in #41): the dump importers now run on any connection, so ImportSession::load_dump uses namespace::dump_import (buffered by default) and load_dump_with can select the streaming importer. name_in_use also consults the in-flight reservations, so a migration target is never created over a name a create/fork/reset still holds. Two new tests cover a streaming import into a quarantined target and a streaming import whose caller goes away mid-dump.

tszymczyszyn-shopify and others added 9 commits October 13, 2026 22:49
load_dump and both importers took a PrimaryConnection. Take any
Connection instead, so that an import into a quarantined migration
target can run the same importers on its capability connection. The
buffered importer shares the connection with its per-statement blocking
task through an Arc rather than requiring Clone. No behaviour change.
Add `NamespaceStore::create_target_quarantined` (and route the admin
`CreateTargetQuarantined` command through it), which creates a namespace
as a quarantined migration target atomically with namespace creation:

- A name the server already knows (config in memory, or a namespace
  cache entry) is refused with FENCE_PRECONDITION_FAILED/namespace_exists
  without touching its gate.
- Otherwise the controller publishes an in-memory target-creation gate
  before the metastore transaction writes the marker, config row, record
  and receipt. The gate refuses every class but maintenance and
  observability, and `check_available` refuses the name, so create, fork
  and `with()` neither store nor set anything up for it.
- The committed record's quarantine gate replaces it; only then is the
  config published into the in-memory map (from the durable row, with
  the record's own block values) and the namespace loaded, so its first
  connection maker is created behind the quarantine gate.
- The command runs on its own task under the transition lock; a replay
  returns the stored result and completes the publication and load of a
  commit that was not acknowledged, or of a creation interrupted between
  its marker and its commit.

`CreateTargetRequest` is the typed entry point for the admin route and
bulk import. The target's log id is not written back into the record, to
keep the marker rule for same-revision records intact (documented).

Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
Add the operation-owned import path into a quarantined migration target and
the seal that ends it (docs/NAMESPACE_FENCE.md sections 7, 10.2 and 11).

- MigrationCapability (server-issued, fields private) and CapabilityPurpose.
  The fence controller keeps the live capability set and a count of running
  import calls; every published transition drops capabilities whose state,
  owner or revision no longer match.
- FenceConnState::with_capability: the WAL admits a capability connection's
  write transaction only while its capability matches the fence and is live;
  a validation connection never writes.
- NamespaceStore::open_import_session and ImportSession::{with_raw,
  load_dump}: the only way to write into TARGET_QUARANTINED. The dump loader
  is split into load_dump_sql so that the loader used for namespaces created
  from a dump also runs under an import capability.
- SealTargetImport closes import admission in memory, persists
  TARGET_IMPORT_DRAINING (invalidating every import capability), waits on
  release notifications for running import calls and for any import
  transaction holding the write slot (force_rollback rolls it back at the
  deadline), then persists TARGET_VALIDATING. A deadline leaves
  TARGET_IMPORT_DRAINING durable and closed until the owner's seal resumes it.

Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
Keep stale DRAINING receipts historical once their exact drain state and revision have been superseded, and preserve resumed attribution when a live drain completes. This prevents old commands from cancelling or completing work in newer states while keeping replay responses and audit classification accurate.

Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
Walk a source and two targets through every stored fence state and check
the protection an older binary gets (docs/NAMESPACE_FENCE.md section
13.2): the config row's block_* fields hold the fence's mirror and
nothing else in the row changes, a config write through the metastore is
refused and changes nothing, and an older binary's delete of the config
row fails on the fence row's foreign key. Release and write enable put
the namespace's own values back.

The test found that a restart overwrote the in-memory config of a
released source or a writable target with the block_* values saved when
the fence was acquired, although config writes after the operation had
stored the namespace's own values in the row since: a namespace blocked
after its release could come back unblocked. Loading the metastore, the
target config publication and adoption now take the namespace's own
config through fence_store::own_config, which uses the row as it is once
the record no longer mirrors the fence.

Crash the server at each persistence boundary of SetSourceReadFence,
SealTargetImport and EnableTargetWrites, and while a read or seal drain
waits for a reader or an import call, then restart it on the same
directory: the prior or the committed state is recovered with no
in-memory gate, reader or import call left, reads and import stay
closed once their draining state committed, target writes open only if
TARGET_WRITABLE committed, the marker is repaired, and a replay of the
same command completes an interrupted drain at once.

Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
Add a test that the admin shell can neither read nor write a
quarantined migration target, through its own entry point and with a
raw write that skips its read admission, while the operation's import
session keeps working.

Map every acceptance requirement in docs/NAMESPACE_FENCE.md section 17
to the tests that cover it, correct the names of the corrupt-state
tests, list the transition and CAS tests that cover ownership, revision
and replay outcomes, and note the replica connection path in the
code-path coverage table.

Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
name_in_use looked at the metastore and the namespace cache, but a name
reserved by a create, fork or reset in progress is pinned in in_flight,
and the cache may evict the reserved entry before the creation puts its
config in memory. A target created in that window would have its config
row deleted when the reservation is undone. Consult in_flight as well.
Load the representative dump through ImportSession::load_dump_with with
the streaming importer and compare the sealed target with the source,
and check that a streaming import whose caller goes away mid-dump leaves
the target empty and quarantined.
@tszymczyszyn-shopify
tszymczyszyn-shopify force-pushed the namespace-fence/4-target-lifecycle branch from 5a38436 to 950c901 Compare October 13, 2026 20:51
@tszymczyszyn-shopify
tszymczyszyn-shopify force-pushed the namespace-fence/3-read-fence branch from da708e3 to 10d7857 Compare October 13, 2026 20:51
@tszymczyszyn-shopify

Copy link
Copy Markdown
Author

Closing. This stack is parked; the note at the top of the description explains why. The branch is kept at 950c90169e, so this PR can be reopened if an automated mover or concurrent moves make an operation-owned fence necessary.

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