On this page
What the backup store is and is not
4. What the backup store is — and is not
The configured backup backend is a disaster-recovery write mirror and a
conditional read fallback, not a second write authority. Primary mutations
commit first and are mirrored afterwards. Ordinary clients may retry a read
that failed at the primary against the backup, but a successful absent answer
from the primary remains authoritative. Every such retry is said on standard
error as [storage-failover] primary <operation> failed for <path or prefix>: <the primary's error>; read from the backup instead, so a host whose primary
refuses every read while the backup answers is visible from its log; the
backup's own answer, a refusal included, is returned as it is. The object API
server and other authority-sensitive readers use primary-only mode and return
the primary error. No read path promotes the backup to writer. Cross-provider
DR remains the explicit fenced migration in steps 2–3.
The coordinator's fleet-shape sweep checks, on every pass, that the resolver
and the config file agree about the mirror (replica-cannot-resolve): a file
that declares storage.backup.backend while the resolver answers that no
replica is configured is a finding, since one half of the binary then writes
to a replica the other half says does not exist; a file that declares none is
a note (<primary>: no replica declared); and a config file the coordinator
cannot read is a finding of its own (the config file cannot be read, so the declaration cannot be compared with the resolver's answer: …), never a quiet
pass. The sweep's health check (health-green-boundaries-down) fires only
when the object API's healthz states ok and degraded as booleans with
at least one boundary stated closed.
For the fixed ~/.stado/local-storage (A) and ~/.stado/local-backup (B)
roots on a host, use the narrower resident transaction. It requires the running
object API to report which of these exact paths it actually constructed as its
primary and mirror; a different root is refused rather than substituted.
stado repair stado --step storage-root --target TARGET --apply --json
The step starts the fenced, source-preserving reconciliation under one durable
transaction and then reads that transaction's receipt until it settles, so one
command covers the run and status phases of the protocol. The receipt
reports the checkpoint, the verified object count, the writer fence and the
accepted resident owner; without --apply the command reports what it would
do and writes nothing.
The protocol itself has five phases — run, resume, status, rollback
and finalize. run and resume address the same durable ID; resume never
creates a second checkpoint or transaction identity. status only reads the
receipt and the current native owner. rollback restores the exact prior
route before the data-activation boundary, and finalize records completion
only after the ordinary coordinator has finished typed lifecycle cleanup.
Stado Desktop drives those five phases individually from the selected host's Storage reconciliation sheet, through the authenticated host route rather than a second implementation. Each action uses the sheet's transaction ID and interprets the same durable receipt the repair step reads. Switching between the command and the sheet does not create a second workflow: use the same target and transaction ID when resuming or inspecting an operation.
Preflight identifies the real runtime
Before changing service or queue state, the running object API must report its constructed A and B roots, which root is primary, which root is its mirror, its serving PID, and the write-fence protocol it implements. The transaction also captures the object API executable and requires it to match the staged binary for the exact Stado release declared for the target. A plist path, a configured default, or a listener alone is not proof of the storage authority or runtime.
Before a lifecycle fence exists, a new run or resume selects that exact
Stado release from the current canonical registry, not from an older captured
target declaration. If the registry cannot be read, the attempt refuses to
fence writers; it does not substitute a cached declaration. Once the lifecycle
fence exists, every later phase remains pinned to the release and runtime
captured in that fence.
Fence first, inventory second
The resident owner pauses and drains the queue, stops the other writers, and
then acquires the reconciliation's exclusive filesystem lock against the
shared lock used by ordinary object writers. It records a durable write-fence
intent before treating the fence as held. The object API can continue serving
reads in this interval, but object writes are refused with HTTP 503 and the
name of the transaction holding the fence. The public status API exposes the
same condition as storage.write_fence and reports the constructed backup as
storage.backup.
Only after that fence is effective does Stado capture complete A and B inventories and compare authenticated HTTP reads byte-for-byte. An inventory taken earlier could race a queue claim, lease renewal, or mirrored write and is not preflight evidence. If B differs from A and the API's constructed authority is A, the transaction refuses a B-winning copy. This also protects A's object metadata when the body bytes happen to match.
The object API is stopped after those read-path comparisons and before the immutable physical checkpoints are made. Checkpoints preserve physical metadata as well as contents, including the recorded modes and ownership needed for exact restoration. HTTP comparison answers a different question: whether clients receive the same bytes. It deliberately ignores filesystem mode, uid, gid, and other physical metadata. A byte match is therefore not a substitute for a full physical checkpoint, and a metadata difference alone is not an HTTP-byte conflict.
The additive data step copies only the qualified ecosystem/ objects and
matching metadata sidecars selected by the transaction. A-only objects remain
and B is not modified. The lifecycle fence captures both the exact prior
primary and the exact prior mirror; rollback never assumes that B was the sole
prior route.
Activation, rollback, and fence release
Before any checkpoint exists, rollback can unwind a preparation whose queue
was drained and whose placement leases were all captured. It restores the
captured prior route and service state, releases any write fence, and restores
the queue. Earlier preparation is refused with
preparation rollback requires the recorded drained queue and complete placement leases;
it has not acquired the storage write fence. An interrupted preparation rollback
remains a rollback when the same transaction is resumed.
After checkpointing, rollback remains possible only before data commit. It restores the captured prior route, not an assumed B-only layout. The source roots are checked against their saved inventories; the immutable checkpoints remain evidence, and a partial additive copy in inactive A is retained rather than silently replacing the whole root.
Forward activation proves the constructed A primary and B mirror and compares
the complete qualified API byte set before releasing the write fence. A
checkpointed rollback proves the captured prior route and its corresponding
byte set. A preparation rollback instead reports
constructed-runtime-without-data-mutation: no data-copy step occurred, so
this is not a claim that a full byte comparison ran. After the applicable proof,
Stado releases the write fence, restores other writers, and restores the queue.
Repeating an active run or resume observes the existing owner instead of
starting a competitor. An incompatible action is refused, for example
native reconciliation is already executing run; cannot accept rollback;
the refusal does not queue rollback behind a forward activation. If an owner
was recorded as executing, status reports it that way only while the native
manager still reports the same PID. Otherwise it reports interrupted, or
unobserved when the manager cannot be read, while retaining the stored value
as recorded_status.
Read the whole receipt
The lifecycle-fence schema is version 5. Its roots object records
primary, backup, prior_primary, prior_backup, and runtime; those
captured values, not conventional directory names, define activation and
rollback. write_fence records status, intent, acquired_at, and
released_at. A missing released_at means the fence has not been released.
preflight_evidence is nullable until fenced inventories and API comparisons
have actually been captured; null never means that both roots were compared
or found equal.
A mutating command may return accepted as soon as the resident native owner
accepts the operation. That is a launch acknowledgement, not success,
activation, restoration, cleanup, or completion. Continue reading status
for the same transaction ID: reconcile owner observation with
recorded_status, inspect the preflight evidence and root identities, and
confirm the final A byte proof and released write fence. On the forward path,
only finalize after ordinary coordinator cleanup writes complete. For a
rollback, require the durable restored/rolled-back outcome and released fence;
an accepted rollback is no more complete than an accepted run.
On macOS, saved autostart state accepts both launchd's true/false
overrides and its disabled/enabled names. Exact native-unit restoration
reads back permissions, owner, group, and digest. Snapshot access may use an
already available sudo -n capability when a captured root contains files
the owner cannot read directly; it never asks for a password or changes the
live roots' permissions.
See configuration for the storage layout and
stado repair for the generated option reference.
Source: this website