fix(rustfs-mirror): sit through a node reboot instead of amplifying it #496

Merged
binjovi-bot merged 1 commit from sean/fix-mirror-reboot into trunk 2026-09-09 17:40:20 +00:00 AGit
Owner

A kernel-roll reboot took rustfs-1 away for two minutes; the mirror turned that into twenty, by crash-restarting and re-listing 300k objects into a store whose RPC replay cache was already at capacity. A failed pass is now waited out in-process. The fix then had the same class of bug it was fixing -- a run that compared nothing reported a pass -- caught by a new fake-mc harness that executes each Job script and is kept as tests/rustfs-mirror-behaviour.sh. https://claude.ai/code/session_01KZoQin34jeyt6nDGqvJA76

A kernel-roll reboot took rustfs-1 away for two minutes; the mirror turned that into twenty, by crash-restarting and re-listing 300k objects into a store whose RPC replay cache was already at capacity. A failed pass is now waited out in-process. The fix then had the same class of bug it was fixing -- a run that compared nothing reported a pass -- caught by a new fake-mc harness that executes each Job script and is kept as tests/rustfs-mirror-behaviour.sh. https://claude.ai/code/session_01KZoQin34jeyt6nDGqvJA76
feat(rustfs): deliver the binjovi-logs key and mirror its bucket
All checks were successful
binjovi/ci Binjovi completed the frozen plan
b5814c844b
Stage 3, bucket two of five. Source side only: this delivers the scoped
credential to namespace binjovi and copies the bucket. sean/binjovi still
reads MinIO after this lands, and is repointed in its own commit once the
mirror is green.

The delivery chain lands BEFORE the repoint rather than with it, which is the
opposite of what this file's header said. The mirror writes with this
identity's own scoped key -- the migration never holds a root credential on
either side -- so the key has to exist before there is anything to repoint to.
Namespace binjovi gains a second Secret it does not read yet.

The shaped Secret uses access_key / secret_key, not the minio_access_key /
minio_secret_key the MinIO chain used. object-store.json already records those
two field names for this identity, and the store name and remote key change in
the repoint regardless, so there is nothing to gain by carrying the old word
into RustFS. The declaration, the delivered Secret and the workload now agree.

MEASURED before writing the mirror: 297,895 objects in 75 MiB, an average
object of about 260 bytes. Six times loki's object count in a thirtieth of its
bytes, so the cost here is per-object round trips and the two full listings
each pass does, not throughput.

The gate is loki's, for the same reason and with the same bound. MinIO carries
binjovi-logs-v1-expire-30d, so the source deletes objects while the mirror
runs and destination-only lines appear on every pass; requiring an empty diff
would require that expiry to stop. Nothing may differ, ever. Up to 64 missing
is the in-flight residual of a control plane still archiving to MinIO, and it
must reach zero on the run after the repoint. That run is the cutover gate.

Both class guards cover the new files the moment they land, and both were
confirmed red first. tests/rustfs-mirror.sh picks the alphabetically first
bucket for its seven probes, which is now binjovi-logs.yaml, so its generic
controls fire against this Job rather than tempo's. Four further controls were
run by hand against the new delivery chain -- conditions removed, reader scope
narrowed past the round-trip key, the generated key unfrozen, and the endpoint
pointed back at MinIO -- and each went red.

Claude-Session: https://claude.ai/code/session_01KZoQin34jeyt6nDGqvJA76
fix(rustfs-mirror): sit through a node reboot instead of amplifying it
Some checks failed
binjovi/ci Binjovi failed the frozen plan
c3749df6ba
MEASURED 2026-09-09, during the binjovi-logs bulk transfer. The kernel roll
rebooted seanfarm-worker-07b866, which holds rustfs-1. RustFS lost a peer for
about two minutes. What followed was not two minutes of degradation:

  mc mirror got HTTP 503 and exited 1
  set -eu killed the script
  restartPolicy: OnFailure restarted it from pass 1
  pass 1 re-listed the whole bucket on both sides -- ~300k objects
  that listing storm drove RustFS's internode RPC replay cache to capacity
    (failure_reason: replay_cache_capacity, rpc_path ReadVersion)
  a full replay cache makes RustFS reject LEGITIMATE internode RPCs as
    signature verification failures -- 1331 on rustfs-0, 1879 on rustfs-2
  which produced more 503s, which restarted the container again

Five restarts in twenty minutes, against a reboot that lasted two. The mirror
was not the cause of the outage and it was most of its duration.

Rolling a patched kernel onto every node with no drama is what this cluster is
for, so a node reboot is ROUTINE, and a mirror that cannot sit through one is a
mirror that will do this again on every roll. A failed pass is now caught,
waited out and retried IN THE SAME PROCESS. `mc mirror` copies only what
differs, so the wait costs nothing and the next pass resumes where the last
one stopped. A failed `mc diff` is treated the same way: the exit status is
still not the parity gate -- the output is, and that has not changed -- but a
listing that never COMPLETED must not be read as agreement between the stores.

tempo.yaml gets the same shape though its bucket is 268 objects. The property
is not about size; it is that no file here should have to be the one that
learns this again.

AND THE FIX HAD THE BUG IT WAS FIXING. The counters are initialised before the
loop so `set -u` cannot abort at the gate. If every pass then failed on a
transient, the counters still held their initial values, the bounded gate read
missing=1 as a measurement, and the Job reported a successful bulk transfer
with one object in flight -- a run that compared nothing, reporting a pass.
Caught by a fake-mc harness before it ran anywhere. Each mirror now carries a
`compared` flag and fails loudly if parity was never read.

tests/rustfs-mirror-behaviour.sh is that harness, kept. It extracts each Job's
script and EXECUTES it with mc and sleep stubbed, so a node reboot, a store
outage and a corrupt copy are each reproducible in a second with no cluster.
Eight behaviours per mirror, 24 in all. The static guard reads these Jobs as
text and catches a missing gate; it cannot catch a gate that is present and
wrong, and it did not catch this one.

Four negative controls on the new guard, each confirmed red: the
compared-nothing gate removed, a failed mirror pass exiting the script, an
incomplete listing counted as parity, and the differing-object gate removed.
The harness also proves its own stub can fail, because a stub that cannot go
red makes every case above inert.

Claude-Session: https://claude.ai/code/session_01KZoQin34jeyt6nDGqvJA76
sean force-pushed sean/fix-mirror-reboot from c3749df6ba
Some checks failed
binjovi/ci Binjovi failed the frozen plan
to 9ab7ea8dd8
Some checks failed
binjovi/ci Binjovi failed the frozen plan
2026-09-09 17:07:55 +00:00
Compare
sean force-pushed sean/fix-mirror-reboot from 9ab7ea8dd8
Some checks failed
binjovi/ci Binjovi failed the frozen plan
to e55e6d1614
Some checks failed
binjovi/ci Binjovi failed the frozen plan
2026-09-09 17:22:02 +00:00
Compare
sean force-pushed sean/fix-mirror-reboot from e55e6d1614
Some checks failed
binjovi/ci Binjovi failed the frozen plan
to f3ebc76ef1
All checks were successful
binjovi/ci Binjovi completed the frozen plan
2026-09-09 17:33:23 +00:00
Compare
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sean/seanfarm!496
No description provided.