test(tier): cover mixed-version committed replay (#5119)

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-07-22 17:02:32 +08:00
committed by GitHub
parent 92ae19b340
commit 65ba138c27
2 changed files with 101 additions and 0 deletions
+74
View File
@@ -5611,6 +5611,80 @@ mod tests {
assert_eq!(lock_unpoisoned(&calls).as_slice(), &[format!("peer-a:commit:{mutation_id}:etag-new")]);
}
#[tokio::test]
async fn committed_replay_mixed_version_peer_matrix_fails_before_local_publish() {
for (case, peer_error, expected_error) in [
(
"old_unimplemented",
"peer tier mutation commit RPC failed: status: Unimplemented, message: \"old peer has no tier mutation control service\"",
"old peer",
),
(
"deadline_exceeded",
"peer tier mutation commit RPC failed: status: DeadlineExceeded, message: \"peer tier mutation control timed out\"",
"timed out",
),
(
"unavailable",
"peer tier mutation commit RPC failed: status: Unavailable, message: \"peer tier mutation control unavailable\"",
"unavailable",
),
] {
let store = Arc::new(CasConfigStore::default());
let mut persisted = empty_mgr();
persisted.tiers.insert("COLD-A".to_string(), build_rustfs_tier("COLD-A"));
persisted
.save_tiering_config_if_current(store.clone(), None)
.await
.expect("tier config fixture should persist");
let mutation_id = uuid::Uuid::from_u128(match case {
"old_unimplemented" => 31,
"deadline_exceeded" => 32,
"unavailable" => 33,
_ => unreachable!("matrix cases are enumerated above"),
});
let intent = committed_remove_intent("COLD-A", mutation_id, "etag-new");
crate::services::tier::tier_mutation_intent::save_tier_mutation_intent_record(store.clone(), &intent)
.await
.expect("committed intent fixture should persist");
let writes_after_fixture = store.next_etag.load(Ordering::SeqCst);
let calls = Arc::new(Mutex::new(Vec::new()));
let handle = TierConfigMgr::new();
let err = TIER_MUTATION_TEST_COMMIT_PEERS
.scope(vec![FakeTierMutationCommitPeer::boxed("peer-a", calls.clone(), Err(peer_error))], async {
TierConfigMgr::reload_handle_with(&handle, store.clone()).await
})
.await
.expect_err("mixed-version peer failure must abort committed replay");
assert!(err.to_string().contains(expected_error), "{case} returned {err}");
assert_eq!(lock_unpoisoned(&calls).as_slice(), &[format!("peer-a:commit:{mutation_id}:etag-new")]);
assert!(
!handle.read().await.tiers.contains_key("COLD-A"),
"{case} must not publish the local tier config after peer replay fails"
);
assert_eq!(
store.next_etag.load(Ordering::SeqCst),
writes_after_fixture,
"{case} must not perform a second tier-config CAS write after replay fails"
);
let reloaded = TierConfigMgr::load_tier_mutation_intents(store)
.await
.expect("intent scan should succeed after failed mixed-version replay");
assert!(
reloaded.iter().any(|intent| intent.mutation_id == mutation_id),
"{case} must keep the committed intent for retry"
);
let blocked = match TierConfigMgr::acquire_operation_lease(&handle, "COLD-A").await {
Ok(_) => panic!("{case} must keep committed replay blocking old tier operations"),
Err(err) => err,
};
assert!(blocked.message.contains("being replaced"), "{case} returned {blocked}");
}
}
#[tokio::test]
async fn committed_mutation_recovery_requires_every_peer_to_commit() {
let mutation_id = uuid::Uuid::from_u128(17);
+27
View File
@@ -2318,6 +2318,33 @@ mod tests {
assert_eq!(error.code(), tonic::Code::InvalidArgument);
}
#[tokio::test]
async fn tier_mutation_control_rejects_old_protocol_version_before_store_lookup() {
let service = make_tier_mutation_control_server_for_context(None);
let mutation_id = uuid::Uuid::new_v4();
let payload = Bytes::from_static(b"intent");
let mut request = Request::new(TierMutationPrepareRequest {
version: rustfs_protos::TIER_MUTATION_RPC_PROTOCOL_VERSION - 1,
mutation_id: mutation_id.to_string(),
canonical_payload: payload,
});
let body = rustfs_protos::canonical_tier_mutation_rpc_body(
request.get_ref().version,
rustfs_protos::TierMutationRpcPhase::Prepare,
mutation_id,
&request.get_ref().canonical_payload,
)
.expect("old-version request should encode for rejection test");
set_tonic_canonical_body_digest(&mut request, &body).expect("digest metadata should encode");
mark_v2_authenticated(&mut request);
let error = service
.prepare_tier_mutation(request)
.await
.expect_err("old tier mutation protocol version must fail closed");
assert_eq!(error.code(), tonic::Code::FailedPrecondition);
}
#[tokio::test]
async fn tier_mutation_control_rejects_oversized_prepare_before_auth_and_store_lookup() {
let service = make_tier_mutation_control_server_for_context(None);