feat(heal,scanner): best-effort repaired notices from the MRF consumer (#6283)

The scanner's pending-heal ledger and the MRF journal tracked the same
damaged objects with no cross-talk: once the consumer landed an intent
with the heal manager, the ledger's retry entry for that target kept
re-submitting a heal the manager already owned (backlog#1894 axis B).

Fan the acceptance out: both dispatch sites in the MRF queue (the live
consumer and the startup replay) record a compact MrfRepairedEvent
(bucket, object, version bytes) in a bounded process-wide ring owned by
rustfs-common. The scanner drains its own bucket's notices at the top
of retry_pending_scanner_heals and clears the matching Object-kind
ledger entries in one batched retain + sync (a mass-recovery first
sweep must not turn into thousands of full-table ledger clones on the
scan task), with nil notice UUIDs mapping to None per the repo-wide
defensive-UUID invariant so unversioned entries match unversioned
notices only. Notices are best-effort by design — a lost or capped-out
notice leaves the entry to expire through its own attempts/age limits,
because the ledger is a retry oracle, not a source of truth; other
buckets' notices stay queued for their own scanners. Neither persistent
format changes; old nodes that keep double-booking remain harmless.

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-08-20 01:31:26 +08:00
committed by GitHub
parent 99c3811d93
commit d6efb65588
4 changed files with 232 additions and 3 deletions
+14
View File
@@ -127,6 +127,20 @@ async fn decode_failure_intent_maps_to_urgent_mrf_heal_request() {
"MRF intent must reach the manager queue as an Urgent request (snapshot: {:?})",
manager.operations_snapshot().await
);
// Axis B (backlog#1894): the accepted dispatch must also fan out a
// repaired notice for the intent's bucket so the scanner ledger can drop
// its retry entry for the same target. Polled: the queue observation
// above can land between the manager push and the consumer's notice.
let noticed = wait_until(Duration::from_secs(10), || async {
!mrf_channel::take_mrf_repaired_events_for("mrf-bucket").is_empty()
})
.await;
assert!(noticed, "accepted intent must fan out a repaired notice");
assert!(
mrf_channel::take_mrf_repaired_events_for("mrf-bucket").is_empty(),
"notice take is destructive"
);
}
/// A journal left behind by a previous process must be replayed into the