From 600d037b25eebc41d8ae79808de97b7961cdf37f Mon Sep 17 00:00:00 2001 From: houseme Date: Tue, 8 Sep 2026 21:00:18 +0800 Subject: [PATCH] test(heal): cover MRF idle checkpoint cleanup (#7497) Add a runtime cleanup regression test that publishes both retained replay and runtime committed checkpoints, writes scoped and legacy journals, and verifies idle cleanup removes every recovery anchor from the registered local disks. Co-authored-by: zhi22915 --- crates/heal/src/heal/mrf_queue.rs | 67 +++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/crates/heal/src/heal/mrf_queue.rs b/crates/heal/src/heal/mrf_queue.rs index d2738d501..2cf43814d 100644 --- a/crates/heal/src/heal/mrf_queue.rs +++ b/crates/heal/src/heal/mrf_queue.rs @@ -1138,6 +1138,7 @@ fn tick_action(dirty: bool, depth: usize, journal_on_disk: bool, retain_replay_j mod tests { use super::*; use rustfs_common::mrf_channel::{MrfIntent, MrfKind, MrfVerifiedRepairDisposition, MrfVerifiedRepairEvent}; + use serial_test::serial; use std::sync::Arc as StdArc; fn intent(bucket: &str, object: &str, attempts: u8) -> MrfIntent { @@ -1153,6 +1154,12 @@ mod tests { } } + fn encoded_payload(intent: &MrfIntent) -> Vec { + let mut payload = Vec::new(); + assert!(encode_intent(intent, &mut payload), "fixture intent must encode"); + payload + } + #[test] fn tick_action_table() { use TickAction::*; @@ -1286,6 +1293,66 @@ mod tests { ); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + #[serial] + async fn runtime_idle_cleanup_deletes_runtime_and_replay_recovery_anchors() { + let _env = rustfs_test_utils::TestECStoreEnv::builder() + .prefix("rustfs_mrf_runtime_idle_cleanup") + .build() + .await; + let disks = journal_disks().await; + assert!(!disks.is_empty(), "test environment must register local disks"); + + let replay_owner = Uuid::new_v4(); + let runtime_owner = Uuid::new_v4(); + let config = MrfConsumerConfig::default(); + let journal_max_bytes = config.journal_max_bytes; + let replay_payload = encoded_payload(&intent("cleanup-bucket", "replay-object", 0)); + let runtime_payload = encoded_payload(&intent("cleanup-bucket", "runtime-object", 0)); + snapshot::publish_committed_snapshot(&disks, replay_owner, 7, &replay_payload, journal_max_bytes) + .await + .expect("publish retained replay checkpoint"); + snapshot::publish_committed_snapshot(&disks, runtime_owner, 8, &runtime_payload, journal_max_bytes) + .await + .expect("publish runtime checkpoint"); + assert!(write_journal(MRF_SCOPED_JOURNAL_PATH, &runtime_payload).await); + assert!(write_journal(MRF_JOURNAL_PATH, &runtime_payload).await); + + let mut runtime = MrfRuntime { + queue: MrfQueue::new(2, usize::MAX), + config, + checkpoint_owner: Uuid::new_v4(), + next_checkpoint_sequence: 9, + new_since_flush: 0, + dirty: false, + journal_on_disk: true, + retain_replay_journal: false, + durable_replay_anchors: Vec::new(), + replay_cleanup: Some(ReplayCleanup::Committed { + owner: replay_owner, + sequence: 7, + }), + runtime_checkpoint: Some((runtime_owner, 8)), + backoff_until: None, + }; + + assert!( + runtime.delete_idle_recovery_anchors().await, + "idle cleanup should remove both runtime and replay recovery anchors" + ); + assert_eq!(runtime.replay_cleanup, None); + assert_eq!(runtime.runtime_checkpoint, None); + assert!( + snapshot::inspect_local_committed_snapshot(journal_max_bytes) + .await + .expect("inspect committed checkpoints after cleanup") + .is_none(), + "both committed checkpoint generations must be gone after idle cleanup" + ); + assert_eq!(read_journal(MRF_SCOPED_JOURNAL_PATH).await, None); + assert_eq!(read_journal(MRF_JOURNAL_PATH).await, None); + } + #[test] fn durable_replay_acquires_a_fresh_lease_before_manager_admission() { let unique = uuid::Uuid::new_v4();