Skip to content

[fence 6/9] libsql-server: namespace fence admin API and lifecycle interlocks - #46

Closed
tszymczyszyn-shopify wants to merge 4 commits into
namespace-fence/4-target-lifecyclefrom
namespace-fence/5-admin-api
Closed

tszymczyszyn-shopify wants to merge 4 commits into
namespace-fence/4-target-lifecyclefrom
namespace-fence/5-admin-api

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.


Adds the admin fence routes and capability discovery, the first externally reachable surface of the fence. Mutating routes require an admin auth key. While an incompatible fence is active, delete, reset, fork, restore, generic config writes and schema changes are refused with typed errors.

Review focus

Route shapes and authentication preconditions against section 4 of the contract.

Since the rebase onto #53: the fence checks of create and fork run inside Reservation::acquire, under the entry lock, and undoing an unfinished creation leaves a name with fence state alone. From review: a deterministic test parks fork(X→Y) and fork(Y→X) with their source's transition lock held and requires both to be refused; it times out if the reservation stops refusing existing destinations.

Commits

Stack

Part 6 of 9, based on namespace-fence/4-target-lifecycle. 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 fence's checks run inside Reservation::acquire under the entry lock, and undoing an unfinished creation now leaves a name with fence state alone. discard checks the fence inside its transaction and returns the fence's refusal, and the clean-up then keeps the directory. Before, the fence record's foreign key kept the config row, but only as an opaque SQLite error, and a directory still marked incomplete was discarded anyway.

shopify-river and others added 4 commits October 13, 2026 22:49
Serve the fence contract of docs/NAMESPACE_FENCE.md section 4 on the
admin listener (new http/admin/fence.rs):

- GET /v1/fence/capabilities, always served: protocol version, whether
  fences are enabled, served commands, states, proxy stable_code support,
  server build and instance id, and the number of active fences counted
  from the fence registry.
- GET /v1/namespaces/:ns/fence (InspectFence): the durable record, the
  owning operation's receipts (or ?receipts=all), live admission, drain
  counters and the live replication log id; read-only, and also served
  while fence tables exist with the flag off.
- One POST route per source and target command, all through
  NamespaceStore::execute_fence_command, and validation-query, which runs
  one read-only program under the operation's validation capability with
  a 10 000-row bound.

Command routes answer 404 unless --enable-namespace-fence is on, refuse
to run without an admin auth key (admin_auth_required) or on a replica
(not_primary), parse bodies strictly (invalid_argument), and refuse
restore options on target creation (restore_not_allowed). Responses carry
the outcome, replay flag, fence view, receipt and drain counters with the
outcome's admin status.

Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
Refuse config changes, delete, reset, fork (as source or destination),
create over an existing record (with or without a dump URL), linking to a
shared schema and schema migration while a namespace's fence denies
lifecycle work.

A single check reads the fence registry, so it also sees in-memory gates
(a closing transition, a target being created, an indeterminate commit)
and never loads the namespace. Paths that persist through the metastore
are refused again inside its transaction.

A fork reads the source's log without a read lease, so it now holds the
source's transition lock for its whole run and checks the gate under it:
a write or read fence command either completes first and the fork is
refused, or waits for the fork. Reset writes nothing to the metastore and
is checked under the same lock. The fork destination is checked before
anything is stored and again with its namespace entry held; previously a
fork onto an existing, unloaded namespace would publish its config in
memory and remove its directory. A dump URL is no longer fetched for a
create that is refused.

Registering a schema migration job checks the schema and every linked
namespace, so a link written by a binary that does not know fences cannot
lead to a migration step being refused halfway through a job.

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

Undoing an unfinished create or fork discards the name's config and,
if it is still marked incomplete, its directory. A name that has fence
state (a quarantined target, a fenced namespace, or state that cannot
be established) was not made by that creation. The config row was
already kept by the fence record's foreign key, but only as an opaque
SQLite error, after which the directory was still discarded. Check the
fence inside the discard transaction, return the fence's refusal, and
keep the directory when the fence refused.
fork holds its source's transition lock while it reserves its
destination. With the destination's entry checked only for a loaded
namespace, fork(X->Y) and fork(Y->X) on unloaded namespaces each held
the other's source entry and waited for it, which also froze every fence
command on both namespaces. The reservation refuses a destination that
exists as soon as it holds its entry, so the two cannot wait for each
other. Pin that with a deterministic test: a hook point parks both forks
with their source's transition lock held, then both must be refused.
The test times out if the reservation stops checking existence.
@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/5-admin-api branch from c1d0470 to 892546d 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 892546d530, 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