mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 01:38:18 +00:00
refactor(replication): move resyncer pure decision logic into rustfs-replication (M2)
Pure-move milestone M2 of the ECStore replication split (backlog#1675 P1-17): relocate the resyncer's IO-free decision helpers, with their unit tests, into the crates they already belong to by type ownership. No behavior change. Moved into crates/replication: - resync.rs: resync_status_duration - delete.rs: resync_existing_delete_replication_info, replicate_delete_outcome, target_delete_version_id, delete_marker_purge_version_id, delete_marker_purge_mrf_entry - object.rs: version_identity_drifted, is_replication_target_offline_error, SsecPassthroughCapability, SsecPassthroughGate, ssec_passthrough_gate, ssec_passthrough_evidence_present (param-demoted to the echoed customer-algorithm string; ECStore keeps the HeadObjectOutput adapter) - filemeta.rs: NULL_VERSION_ID wire literal (crate-owned copy per the filemeta-independence contract) ECStore rewiring (Rule #14: imports stay in *_boundary.rs): - resync/object-decision/target boundaries re-export the moved symbols; resyncer call sites are unchanged - bucket_target_sys keeps only the verdict cache + TTL and re-exports the capability enum so existing consumer paths keep compiling Not moved (signatures carry ECStore or aws-sdk types): verify_resync_head_result, resync_target_error_detail, the SdkError classifiers, the replicate_all_* option/info builders, and the env-coupled bounded_resync_max_jobs admission clamp. README milestone table updated.
This commit is contained in:
@@ -36,12 +36,15 @@ use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
|
||||
pub(crate) use crate::bucket::bucket_target_sys::{
|
||||
AdvancedPutOptions, PutObjectOptions, PutObjectPartOptions, RemoveObjectOptions, SsecPassthroughCapability, TargetClient,
|
||||
resolve_read_api_version_id,
|
||||
AdvancedPutOptions, PutObjectOptions, PutObjectPartOptions, RemoveObjectOptions, TargetClient, resolve_read_api_version_id,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use crate::bucket::target::BucketTarget;
|
||||
pub(crate) use crate::bucket::target::BucketTargets;
|
||||
pub use rustfs_replication::SsecPassthroughCapability;
|
||||
pub(crate) use rustfs_replication::{
|
||||
SsecPassthroughGate, is_replication_target_offline_error, ssec_passthrough_gate, version_identity_drifted,
|
||||
};
|
||||
|
||||
use super::replication_config_store::ReplicationConfigStore;
|
||||
use super::replication_error_boundary::{Error, Result};
|
||||
@@ -149,52 +152,11 @@ pub(crate) fn replication_object_is_ssec_encrypted(user_defined: &HashMap<String
|
||||
rustfs_replication::is_ssec_encrypted(user_defined)
|
||||
}
|
||||
|
||||
/// Fail-closed decision for an SSE-C passthrough replication attempt, derived
|
||||
/// from the target's cached [`SsecPassthroughCapability`]. Pure so the policy
|
||||
/// can migrate with the worker (M2) without dragging the cache along; the
|
||||
/// caller computes `expired` from the cache record's age (see
|
||||
/// `SSEC_PASSTHROUGH_CAPABILITY_TTL`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SsecPassthroughGate {
|
||||
/// Not an SSE-C object, or the target has a fresh proof that it preserves
|
||||
/// the passthrough transport headers: replicate without a HEAD-back audit.
|
||||
Proceed,
|
||||
/// No usable verdict — first SSE-C attempt since the target was (re)built,
|
||||
/// or the recorded verdict (in either direction) aged out: PUT, then HEAD
|
||||
/// the replica back and require SSE-C evidence before reporting COMPLETED.
|
||||
ProceedWithAudit,
|
||||
/// The target was recently proven to drop the passthrough headers: do not
|
||||
/// send the PUT, report FAILED (the object stays on the normal MRF retry
|
||||
/// channel and re-audits once the verdict expires).
|
||||
FailClosed,
|
||||
}
|
||||
|
||||
pub(crate) fn ssec_passthrough_gate(ssec: bool, capability: SsecPassthroughCapability, expired: bool) -> SsecPassthroughGate {
|
||||
if !ssec {
|
||||
return SsecPassthroughGate::Proceed;
|
||||
}
|
||||
// An expired verdict — Supported or Unsupported — must be re-earned: a
|
||||
// stale Unsupported would otherwise stick forever after a target upgrade,
|
||||
// and a stale Supported would fail open after a backend swap behind the
|
||||
// same endpoint.
|
||||
if expired {
|
||||
return SsecPassthroughGate::ProceedWithAudit;
|
||||
}
|
||||
match capability {
|
||||
SsecPassthroughCapability::Supported => SsecPassthroughGate::Proceed,
|
||||
SsecPassthroughCapability::Unknown => SsecPassthroughGate::ProceedWithAudit,
|
||||
SsecPassthroughCapability::Unsupported => SsecPassthroughGate::FailClosed,
|
||||
}
|
||||
}
|
||||
|
||||
/// True when a replication-check HEAD of the replica proves the SSE-C
|
||||
/// material survived passthrough: a RustFS target restores the transport
|
||||
/// headers into the stored SSE-C keys and its HEAD echoes
|
||||
/// `x-amz-server-side-encryption-customer-algorithm` (the replication-check
|
||||
/// exemption skips key validation but not the metadata echo). A target that
|
||||
/// dropped the headers stored a plain object and echoes nothing.
|
||||
/// HeadObjectOutput adapter over the pure SSE-C passthrough evidence
|
||||
/// judgment owned by `rustfs-replication`: extract the echoed
|
||||
/// customer-algorithm header and let the crate-owned policy decide.
|
||||
pub(crate) fn ssec_passthrough_evidence_present(head: &HeadObjectOutput) -> bool {
|
||||
head.sse_customer_algorithm.as_deref().is_some_and(|algo| !algo.is_empty())
|
||||
rustfs_replication::ssec_passthrough_evidence_present(head.sse_customer_algorithm.as_deref())
|
||||
}
|
||||
|
||||
pub(crate) struct ReplicationTargetStore;
|
||||
@@ -960,53 +922,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// N2 fail-closed policy: SSE-C replication may only proceed silently
|
||||
/// against a target with a FRESH proof that it preserves the passthrough
|
||||
/// transport headers. Unknown targets must be audited; freshly-flagged
|
||||
/// dropping targets must never receive the PUT; an expired verdict in
|
||||
/// EITHER direction must be re-earned through the audit — a sticky
|
||||
/// Unsupported would outlive a target upgrade, and a sticky Supported
|
||||
/// would fail open after a backend swap behind the same endpoint.
|
||||
#[test]
|
||||
fn ssec_passthrough_gate_is_fail_closed_and_ttl_bounded() {
|
||||
for capability in [
|
||||
SsecPassthroughCapability::Unknown,
|
||||
SsecPassthroughCapability::Supported,
|
||||
SsecPassthroughCapability::Unsupported,
|
||||
] {
|
||||
for expired in [false, true] {
|
||||
assert_eq!(
|
||||
ssec_passthrough_gate(false, capability, expired),
|
||||
SsecPassthroughGate::Proceed,
|
||||
"non-SSE-C objects must never be gated on the passthrough capability"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
ssec_passthrough_gate(true, SsecPassthroughCapability::Supported, false),
|
||||
SsecPassthroughGate::Proceed
|
||||
);
|
||||
assert_eq!(
|
||||
ssec_passthrough_gate(true, SsecPassthroughCapability::Unknown, false),
|
||||
SsecPassthroughGate::ProceedWithAudit
|
||||
);
|
||||
assert_eq!(
|
||||
ssec_passthrough_gate(true, SsecPassthroughCapability::Unsupported, false),
|
||||
SsecPassthroughGate::FailClosed
|
||||
);
|
||||
// Expiry flips both directions back to the audit.
|
||||
assert_eq!(
|
||||
ssec_passthrough_gate(true, SsecPassthroughCapability::Unsupported, true),
|
||||
SsecPassthroughGate::ProceedWithAudit,
|
||||
"an expired Unsupported verdict must allow a re-audit (upgraded target recovers without operator action)"
|
||||
);
|
||||
assert_eq!(
|
||||
ssec_passthrough_gate(true, SsecPassthroughCapability::Supported, true),
|
||||
SsecPassthroughGate::ProceedWithAudit,
|
||||
"an expired Supported verdict must be re-proven (backend swap behind the same endpoint must not fail open)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Pins the HeadObjectOutput field extraction feeding the crate-owned
|
||||
/// evidence judgment (the gate/evidence policy matrix itself is pinned in
|
||||
/// `rustfs-replication`'s object tests).
|
||||
#[test]
|
||||
fn ssec_passthrough_evidence_requires_customer_algorithm_echo() {
|
||||
let with_evidence = HeadObjectOutput::builder().sse_customer_algorithm("AES256").build();
|
||||
|
||||
Reference in New Issue
Block a user