mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-07 20:46:11 +00:00
fix(ecstore): make transitioned cleanup crash-safe (#6978)
* fix(ecstore): fence transitioned object cleanup * fix(ecstore): address ILM recovery review findings * fix(ecstore): complete crash-safe tier cleanup recovery * test(ecstore): avoid typo false positive * fix(ecstore): stabilize decommission error buckets * fix(ecstore): stabilize transition delete validation * fix(ecstore): resume authorized tier delete dispatch * fix(ecstore): satisfy feature clippy
This commit is contained in:
@@ -1521,6 +1521,8 @@ pub struct TierMutationControlResponse {
|
||||
pub error_info: ::core::option::Option<::prost::alloc::string::String>,
|
||||
#[prost(bytes = "bytes", tag = "5")]
|
||||
pub response_proof: ::prost::bytes::Bytes,
|
||||
#[prost(enumeration = "TierMutationFailureClass", tag = "6")]
|
||||
pub failure_class: i32,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct GetLiveEventsRequest {
|
||||
@@ -1611,6 +1613,35 @@ impl TierMutationPeerState {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum TierMutationFailureClass {
|
||||
Unspecified = 0,
|
||||
PreDispatchRejected = 1,
|
||||
Ambiguous = 2,
|
||||
}
|
||||
impl TierMutationFailureClass {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
///
|
||||
/// The values are not transformed in any way and thus are considered stable
|
||||
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||
pub fn as_str_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Unspecified => "TIER_MUTATION_FAILURE_CLASS_UNSPECIFIED",
|
||||
Self::PreDispatchRejected => "TIER_MUTATION_FAILURE_CLASS_PRE_DISPATCH_REJECTED",
|
||||
Self::Ambiguous => "TIER_MUTATION_FAILURE_CLASS_AMBIGUOUS",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"TIER_MUTATION_FAILURE_CLASS_UNSPECIFIED" => Some(Self::Unspecified),
|
||||
"TIER_MUTATION_FAILURE_CLASS_PRE_DISPATCH_REJECTED" => Some(Self::PreDispatchRejected),
|
||||
"TIER_MUTATION_FAILURE_CLASS_AMBIGUOUS" => Some(Self::Ambiguous),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated client implementations.
|
||||
pub mod node_service_client {
|
||||
#![allow(unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value)]
|
||||
|
||||
+146
-55
@@ -177,6 +177,7 @@ pub const REMOTE_VERSION_STATE_CAPABILITY_PROBE_PREFIX: &[u8] = b"rustfs-tier-re
|
||||
pub const CROSS_POOL_FENCE_CAPABILITY_PROBE_PREFIX: &[u8] = b"rustfs-cross-pool-fence-capability-v1\0";
|
||||
pub const TIER_MUTATION_RPC_MAX_PREPARE_PAYLOAD_SIZE: usize = 64 * 1024;
|
||||
pub const TIER_MUTATION_RPC_MAX_COMMIT_PAYLOAD_SIZE: usize = 1024;
|
||||
pub const TIER_MUTATION_RPC_MAX_ABORT_PAYLOAD_SIZE: usize = TIER_MUTATION_RPC_MAX_PREPARE_PAYLOAD_SIZE;
|
||||
pub const TIER_MUTATION_RPC_MAX_MESSAGE_SIZE: usize = TIER_MUTATION_RPC_MAX_PREPARE_PAYLOAD_SIZE + 4096;
|
||||
|
||||
pub fn heal_control_coordinator_epoch(topology_fingerprint: &str) -> Result<u64, &'static str> {
|
||||
@@ -342,7 +343,16 @@ impl TierMutationRpcPhase {
|
||||
}
|
||||
}
|
||||
|
||||
pub const TIER_MUTATION_RPC_PROTOCOL_VERSION: u32 = 1;
|
||||
// Version 2 required peer Prepare to block new tier-reference creators and
|
||||
// drain their in-flight operation leases. Version 3 additionally binds Abort
|
||||
// to the canonical Prepare intent so a missing-record Abort can persist an
|
||||
// identity-bound tombstone and linearize against a delayed Prepare. Version 4
|
||||
// signs a typed failure classification while retaining the exact v3 proof
|
||||
// bytes for rolling compatibility.
|
||||
pub const TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION: u32 = 3;
|
||||
pub const TIER_MUTATION_RPC_PROTOCOL_VERSION: u32 = 4;
|
||||
pub const TIER_MUTATION_RPC_MAX_ERROR_INFO_SIZE: usize = 1024;
|
||||
pub const TIER_MUTATION_RPC_MAX_RESPONSE_PROOF_SIZE: usize = 4096;
|
||||
|
||||
pub fn canonical_tier_mutation_rpc_body(
|
||||
version: u32,
|
||||
@@ -374,19 +384,23 @@ pub struct TierMutationRpcResponseProofInput<'a> {
|
||||
pub state: i32,
|
||||
pub applied: bool,
|
||||
pub error_info: Option<&'a str>,
|
||||
pub failure_class: i32,
|
||||
}
|
||||
|
||||
pub fn canonical_tier_mutation_rpc_response_body(
|
||||
input: TierMutationRpcResponseProofInput<'_>,
|
||||
) -> Result<Vec<u8>, std::num::TryFromIntError> {
|
||||
const DOMAIN: &[u8] = b"rustfs-tier-mutation-rpc-response-v1\0";
|
||||
const V3_DOMAIN: &[u8] = b"rustfs-tier-mutation-rpc-response-v1\0";
|
||||
const V4_DOMAIN: &[u8] = b"rustfs-tier-mutation-rpc-response-v2\0";
|
||||
|
||||
let phase = input.phase.as_wire_str().as_bytes();
|
||||
let mutation_id = input.mutation_id.as_bytes();
|
||||
let error_info = input.error_info.map(str::as_bytes);
|
||||
let error_info_len = error_info.map_or(0, <[u8]>::len);
|
||||
let is_v4 = input.version >= TIER_MUTATION_RPC_PROTOCOL_VERSION;
|
||||
let domain = if is_v4 { V4_DOMAIN } else { V3_DOMAIN };
|
||||
let mut body = Vec::with_capacity(
|
||||
DOMAIN.len()
|
||||
domain.len()
|
||||
+ 4
|
||||
+ 8
|
||||
+ phase.len()
|
||||
@@ -398,9 +412,10 @@ pub fn canonical_tier_mutation_rpc_response_body(
|
||||
+ 1
|
||||
+ 1
|
||||
+ 8
|
||||
+ error_info_len,
|
||||
+ error_info_len
|
||||
+ if is_v4 { 4 } else { 0 },
|
||||
);
|
||||
body.extend_from_slice(DOMAIN);
|
||||
body.extend_from_slice(domain);
|
||||
body.extend_from_slice(&input.version.to_be_bytes());
|
||||
body.extend_from_slice(&u64::try_from(phase.len())?.to_be_bytes());
|
||||
body.extend_from_slice(phase);
|
||||
@@ -415,6 +430,9 @@ pub fn canonical_tier_mutation_rpc_response_body(
|
||||
if let Some(error_info) = error_info {
|
||||
body.extend_from_slice(error_info);
|
||||
}
|
||||
if is_v4 {
|
||||
body.extend_from_slice(&input.failure_class.to_be_bytes());
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
@@ -2137,10 +2155,10 @@ mod heal_control_tests {
|
||||
#[cfg(test)]
|
||||
mod tier_mutation_rpc_tests {
|
||||
use super::{
|
||||
TIER_MUTATION_RPC_PROTOCOL_VERSION, TierMutationRpcPhase, TierMutationRpcResponseProofInput,
|
||||
canonical_tier_mutation_rpc_body, canonical_tier_mutation_rpc_response_body,
|
||||
TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION, TIER_MUTATION_RPC_PROTOCOL_VERSION, TierMutationRpcPhase,
|
||||
TierMutationRpcResponseProofInput, canonical_tier_mutation_rpc_body, canonical_tier_mutation_rpc_response_body,
|
||||
};
|
||||
use crate::proto_gen::node_service::TierMutationPeerState;
|
||||
use crate::proto_gen::node_service::{TierMutationFailureClass, TierMutationPeerState};
|
||||
use uuid::uuid;
|
||||
|
||||
#[test]
|
||||
@@ -2155,7 +2173,7 @@ mod tier_mutation_rpc_tests {
|
||||
)
|
||||
.expect("small mutation body should encode");
|
||||
let mut golden = b"rustfs-tier-mutation-rpc-v1\0".to_vec();
|
||||
golden.extend_from_slice(&1_u32.to_be_bytes());
|
||||
golden.extend_from_slice(&TIER_MUTATION_RPC_PROTOCOL_VERSION.to_be_bytes());
|
||||
golden.extend_from_slice(&7_u64.to_be_bytes());
|
||||
golden.extend_from_slice(b"prepare");
|
||||
golden.extend_from_slice(mutation_id.as_bytes());
|
||||
@@ -2165,8 +2183,13 @@ mod tier_mutation_rpc_tests {
|
||||
|
||||
assert_ne!(
|
||||
baseline,
|
||||
canonical_tier_mutation_rpc_body(2, TierMutationRpcPhase::Prepare, mutation_id, payload)
|
||||
.expect("small mutation body should encode")
|
||||
canonical_tier_mutation_rpc_body(
|
||||
TIER_MUTATION_RPC_PROTOCOL_VERSION + 1,
|
||||
TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
payload,
|
||||
)
|
||||
.expect("small mutation body should encode")
|
||||
);
|
||||
assert_ne!(
|
||||
baseline,
|
||||
@@ -2201,11 +2224,27 @@ mod tier_mutation_rpc_tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_tier_mutation_response_binds_request_state_and_error() {
|
||||
fn tier_mutation_v3_request_and_response_golden_bytes_are_unchanged() {
|
||||
let mutation_id = uuid!("12345678-1234-5678-9abc-def012345678");
|
||||
let payload = b"canonical-intent-record";
|
||||
let baseline = canonical_tier_mutation_rpc_response_body(TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
let request = canonical_tier_mutation_rpc_body(
|
||||
TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION,
|
||||
TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
payload,
|
||||
)
|
||||
.expect("v3 request should encode");
|
||||
let mut request_golden = b"rustfs-tier-mutation-rpc-v1\0".to_vec();
|
||||
request_golden.extend_from_slice(&TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION.to_be_bytes());
|
||||
request_golden.extend_from_slice(&7_u64.to_be_bytes());
|
||||
request_golden.extend_from_slice(b"prepare");
|
||||
request_golden.extend_from_slice(mutation_id.as_bytes());
|
||||
request_golden.extend_from_slice(&u64::try_from(payload.len()).expect("payload length should fit").to_be_bytes());
|
||||
request_golden.extend_from_slice(payload);
|
||||
assert_eq!(request, request_golden);
|
||||
|
||||
let response = canonical_tier_mutation_rpc_response_body(TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
@@ -2213,49 +2252,97 @@ mod tier_mutation_rpc_tests {
|
||||
state: TierMutationPeerState::Prepared as i32,
|
||||
applied: true,
|
||||
error_info: None,
|
||||
// v3 must ignore the field so its authenticated bytes stay exact.
|
||||
failure_class: TierMutationFailureClass::PreDispatchRejected as i32,
|
||||
})
|
||||
.expect("small mutation response should encode");
|
||||
.expect("v3 response should encode");
|
||||
let mut response_golden = b"rustfs-tier-mutation-rpc-response-v1\0".to_vec();
|
||||
response_golden.extend_from_slice(&TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION.to_be_bytes());
|
||||
response_golden.extend_from_slice(&7_u64.to_be_bytes());
|
||||
response_golden.extend_from_slice(b"prepare");
|
||||
response_golden.extend_from_slice(mutation_id.as_bytes());
|
||||
response_golden.extend_from_slice(&u64::try_from(payload.len()).expect("payload length should fit").to_be_bytes());
|
||||
response_golden.extend_from_slice(payload);
|
||||
response_golden.push(1);
|
||||
response_golden.extend_from_slice(&(TierMutationPeerState::Prepared as i32).to_be_bytes());
|
||||
response_golden.push(1);
|
||||
response_golden.push(0);
|
||||
response_golden.extend_from_slice(&0_u64.to_be_bytes());
|
||||
assert_eq!(response, response_golden);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_tier_mutation_v4_response_binds_request_result_and_failure_class() {
|
||||
let mutation_id = uuid!("12345678-1234-5678-9abc-def012345678");
|
||||
let payload = b"canonical-intent-record";
|
||||
let baseline = canonical_tier_mutation_rpc_response_body(TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: false,
|
||||
state: TierMutationPeerState::Unspecified as i32,
|
||||
applied: false,
|
||||
error_info: Some("failure"),
|
||||
failure_class: TierMutationFailureClass::Ambiguous as i32,
|
||||
})
|
||||
.expect("small v4 mutation response should encode");
|
||||
|
||||
let cases = [
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: 2,
|
||||
version: TIER_MUTATION_RPC_PREVIOUS_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: true,
|
||||
state: TierMutationPeerState::Prepared as i32,
|
||||
applied: true,
|
||||
error_info: None,
|
||||
success: false,
|
||||
state: TierMutationPeerState::Unspecified as i32,
|
||||
applied: false,
|
||||
error_info: Some("failure"),
|
||||
failure_class: TierMutationFailureClass::Ambiguous as i32,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Commit,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: true,
|
||||
state: TierMutationPeerState::Prepared as i32,
|
||||
applied: true,
|
||||
error_info: None,
|
||||
success: false,
|
||||
state: TierMutationPeerState::Unspecified as i32,
|
||||
applied: false,
|
||||
error_info: Some("failure"),
|
||||
failure_class: TierMutationFailureClass::Ambiguous as i32,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id: uuid!("22345678-1234-5678-9abc-def012345678"),
|
||||
canonical_payload: payload,
|
||||
success: true,
|
||||
state: TierMutationPeerState::Prepared as i32,
|
||||
applied: true,
|
||||
error_info: None,
|
||||
success: false,
|
||||
state: TierMutationPeerState::Unspecified as i32,
|
||||
applied: false,
|
||||
error_info: Some("failure"),
|
||||
failure_class: TierMutationFailureClass::Ambiguous as i32,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: b"tampered-intent-record",
|
||||
success: false,
|
||||
state: TierMutationPeerState::Unspecified as i32,
|
||||
applied: false,
|
||||
error_info: Some("failure"),
|
||||
failure_class: TierMutationFailureClass::Ambiguous as i32,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: true,
|
||||
state: TierMutationPeerState::Prepared as i32,
|
||||
applied: true,
|
||||
error_info: None,
|
||||
state: TierMutationPeerState::Unspecified as i32,
|
||||
applied: false,
|
||||
error_info: Some("failure"),
|
||||
failure_class: TierMutationFailureClass::Ambiguous as i32,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
@@ -2263,39 +2350,43 @@ mod tier_mutation_rpc_tests {
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: false,
|
||||
state: TierMutationPeerState::Prepared as i32,
|
||||
applied: true,
|
||||
error_info: None,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: true,
|
||||
state: TierMutationPeerState::Committed as i32,
|
||||
applied: true,
|
||||
error_info: None,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: true,
|
||||
state: TierMutationPeerState::Prepared as i32,
|
||||
applied: false,
|
||||
error_info: None,
|
||||
error_info: Some("failure"),
|
||||
failure_class: TierMutationFailureClass::Ambiguous as i32,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: true,
|
||||
state: TierMutationPeerState::Prepared as i32,
|
||||
success: false,
|
||||
state: TierMutationPeerState::Unspecified as i32,
|
||||
applied: true,
|
||||
error_info: Some("error"),
|
||||
error_info: Some("failure"),
|
||||
failure_class: TierMutationFailureClass::Ambiguous as i32,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: false,
|
||||
state: TierMutationPeerState::Unspecified as i32,
|
||||
applied: false,
|
||||
error_info: Some("other failure"),
|
||||
failure_class: TierMutationFailureClass::Ambiguous as i32,
|
||||
},
|
||||
TierMutationRpcResponseProofInput {
|
||||
version: TIER_MUTATION_RPC_PROTOCOL_VERSION,
|
||||
phase: TierMutationRpcPhase::Prepare,
|
||||
mutation_id,
|
||||
canonical_payload: payload,
|
||||
success: false,
|
||||
state: TierMutationPeerState::Unspecified as i32,
|
||||
applied: false,
|
||||
error_info: Some("failure"),
|
||||
failure_class: TierMutationFailureClass::PreDispatchRejected as i32,
|
||||
},
|
||||
];
|
||||
for case in cases {
|
||||
|
||||
@@ -1068,12 +1068,19 @@ enum TierMutationPeerState {
|
||||
TIER_MUTATION_PEER_STATE_ABORTED = 3;
|
||||
}
|
||||
|
||||
enum TierMutationFailureClass {
|
||||
TIER_MUTATION_FAILURE_CLASS_UNSPECIFIED = 0;
|
||||
TIER_MUTATION_FAILURE_CLASS_PRE_DISPATCH_REJECTED = 1;
|
||||
TIER_MUTATION_FAILURE_CLASS_AMBIGUOUS = 2;
|
||||
}
|
||||
|
||||
message TierMutationControlResponse {
|
||||
bool success = 1;
|
||||
TierMutationPeerState state = 2;
|
||||
bool applied = 3;
|
||||
optional string error_info = 4;
|
||||
bytes response_proof = 5;
|
||||
TierMutationFailureClass failure_class = 6;
|
||||
}
|
||||
|
||||
message GetLiveEventsRequest {
|
||||
|
||||
Reference in New Issue
Block a user