mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-12 16:16:55 +00:00
test(replication): pin the scanner existing-object compensation matrix (#5877)
P1-20 (rustfs/backlog#1675 B2, test-only). No prior test wrote objects BEFORE the replication rule arrived, leaving the scanner's existing-object resync pass — the only channel for such objects — without end-to-end coverage, and the enqueue truth table partially unpinned at unit level. e2e (both negative cells are contracts, asserted over multiple fast-scanner cycles next to a replicated control key that proves the scanner and the live path are running): - test_scanner_compensates_existing_objects_across_write_paths: plain PUT, CopyObject and Snowball auto-extract products written pre-rule all converge via scanner compensation; a null-version object (PUT before the bucket became versioned) is pinned as never compensated (the scanner heal gate skips nil-version objects). - test_scanner_never_compensates_when_existing_object_replication_disabled: ExistingObjectReplication=Disabled is a contract, not a delay — existing keys stay absent while post-rule writes replicate normally. Unit truth-table pins (crates/replication): - queue.rs: an empty replicate decision (Disabled existing-object, inbound REPLICA) skips heal queueing for every status; Completed without a resync decision skips. - operation.rs: existing-object resync without a reset replicates exactly the never-replicated (Empty) objects. Helper: put_bucket_replication_with_statuses parameterizes the previously hardcoded ExistingObjectReplication status; the nextest count comments are refreshed to the post-rebase totals.
This commit is contained in:
@@ -635,6 +635,38 @@ mod tests {
|
||||
assert!(!should_use_existing_delete_replication_info(false, false));
|
||||
}
|
||||
|
||||
/// P1-20 truth-table pin (rustfs/backlog#1675): without any reset in play
|
||||
/// (no per-target reset header on the object, empty reset id on the
|
||||
/// target) the existing-object resync decision compensates exactly the
|
||||
/// never-replicated objects — Empty replicates, any recorded status does
|
||||
/// not.
|
||||
#[test]
|
||||
fn resync_target_without_reset_replicates_only_empty_status() {
|
||||
let user_defined = HashMap::new();
|
||||
let object = ReplicationResyncTargetObject {
|
||||
mod_time: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(10)),
|
||||
user_defined: &user_defined,
|
||||
};
|
||||
|
||||
for (status, expected) in [
|
||||
(ReplicationStatusType::Empty, true),
|
||||
(ReplicationStatusType::Completed, false),
|
||||
// "COMPLETE" on disk parses to this legacy variant, so objects
|
||||
// written by older versions reach the decision through it.
|
||||
(ReplicationStatusType::CompletedLegacy, false),
|
||||
(ReplicationStatusType::Pending, false),
|
||||
(ReplicationStatusType::Failed, false),
|
||||
(ReplicationStatusType::Replica, false),
|
||||
] {
|
||||
let label = format!("{status:?}");
|
||||
let decision = resync_target_for_object(&object, "arn:target", "", None, status);
|
||||
assert_eq!(
|
||||
decision.replicate, expected,
|
||||
"existing-object resync without a reset must replicate only never-replicated objects (status {label})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resync_target_includes_object_at_reset_before_boundary() {
|
||||
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
||||
|
||||
@@ -360,6 +360,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// P1-20 truth-table pin (rustfs/backlog#1675): when no target replicates
|
||||
/// — the decision is empty because ExistingObjectReplication is Disabled
|
||||
/// for a never-replicated object, or because the object is an inbound
|
||||
/// REPLICA (must_replicate returns an empty decision for those) — the
|
||||
/// heal pass must skip entirely, whatever the recorded status says. The
|
||||
/// scanner never compensates these objects.
|
||||
#[test]
|
||||
fn heal_queue_action_skips_when_no_target_replicates() {
|
||||
for status in [
|
||||
ReplicationStatusType::Empty,
|
||||
ReplicationStatusType::Failed,
|
||||
ReplicationStatusType::Replica,
|
||||
] {
|
||||
let mut roi = ReplicateObjectInfo {
|
||||
bucket: "bucket".to_string(),
|
||||
name: "object".to_string(),
|
||||
replication_status: status,
|
||||
dsc: ReplicateDecision::new(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let action = replication_heal_queue_action(&mut roi);
|
||||
|
||||
assert!(
|
||||
matches!(action, ReplicationHealQueueAction::Skip),
|
||||
"an empty replicate decision must skip heal queueing (status {:?})",
|
||||
roi.replication_status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// P1-20 truth-table pin: a Completed object with no resync decision has
|
||||
/// nothing left to heal — the scanner must not requeue it.
|
||||
#[test]
|
||||
fn heal_queue_action_skips_completed_object_without_resync() {
|
||||
let mut roi = replicate_object_info(ReplicationStatusType::Completed);
|
||||
|
||||
let action = replication_heal_queue_action(&mut roi);
|
||||
|
||||
assert!(matches!(action, ReplicationHealQueueAction::Skip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heal_queue_action_routes_failed_objects_to_heal_queue() {
|
||||
let mut roi = replicate_object_info(ReplicationStatusType::Failed);
|
||||
|
||||
Reference in New Issue
Block a user