fix(multipart): preserve retried parts on quorum failure (#5363)

* fix(multipart): preserve retried parts on quorum failure

* style(multipart): format transaction rollback

* fix(proto): regenerate multipart transaction RPCs

* fix(multipart): import rollback marker constant

* fix(multipart): export transaction action

* fix: import multipart transaction test requests
This commit is contained in:
cxymds
2026-07-28 17:04:46 +08:00
committed by GitHub
parent 6e0640444e
commit b432f31c2c
14 changed files with 1296 additions and 61 deletions
+75 -6
View File
@@ -926,10 +926,24 @@ impl Node for NodeService {
self.handle_check_parts(request).await
}
async fn prepare_part_transaction(
&self,
request: Request<PreparePartTransactionRequest>,
) -> Result<Response<PreparePartTransactionResponse>, Status> {
self.handle_prepare_part_transaction(request).await
}
async fn rename_part(&self, request: Request<RenamePartRequest>) -> Result<Response<RenamePartResponse>, Status> {
self.handle_rename_part(request).await
}
async fn settle_part_transaction(
&self,
request: Request<SettlePartTransactionRequest>,
) -> Result<Response<SettlePartTransactionResponse>, Status> {
self.handle_settle_part_transaction(request).await
}
async fn rename_file(&self, request: Request<RenameFileRequest>) -> Result<Response<RenameFileResponse>, Status> {
self.handle_rename_file(request).await
}
@@ -2046,12 +2060,12 @@ mod tests {
HealBucketRequest, HealControlRequest, ListBucketRequest, ListDirRequest, ListVolumesRequest, LoadBucketMetadataRequest,
LoadGroupRequest, LoadPolicyMappingRequest, LoadPolicyRequest, LoadRebalanceMetaRequest, LoadServiceAccountRequest,
LoadTransitionTierConfigRequest, LoadUserRequest, LocalStorageInfoRequest, MakeBucketRequest, MakeVolumeRequest,
MakeVolumesRequest, Mss, PingRequest, ReadAllRequest, ReadAtRequest, ReadMultipleRequest, ReadVersionRequest,
ReadXlRequest, ReloadPoolMetaRequest, ReloadSiteReplicationConfigRequest, RenameDataRequest, RenameFileRequest,
RenamePartRequest, ScannerActivityRequest, ServerInfoRequest, SignalServiceRequest, StartProfilingRequest,
StatVolumeRequest, StopRebalanceRequest, TierMutationPeerState, TierMutationPrepareRequest,
UpdateMetacacheListingRequest, UpdateMetadataRequest, VerifyFileRequest, WriteAllRequest, WriteMetadataRequest,
WriteRequest,
MakeVolumesRequest, Mss, PingRequest, PreparePartTransactionRequest, ReadAllRequest, ReadAtRequest, ReadMultipleRequest,
ReadVersionRequest, ReadXlRequest, ReloadPoolMetaRequest, ReloadSiteReplicationConfigRequest, RenameDataRequest,
RenameFileRequest, RenamePartRequest, ScannerActivityRequest, ServerInfoRequest, SettlePartTransactionRequest,
SignalServiceRequest, StartProfilingRequest, StatVolumeRequest, StopRebalanceRequest, TierMutationPeerState,
TierMutationPrepareRequest, UpdateMetacacheListingRequest, UpdateMetadataRequest, VerifyFileRequest, WriteAllRequest,
WriteMetadataRequest, WriteRequest,
heal_control_service_client::HealControlServiceClient,
heal_control_service_server::{HealControlService as _, HealControlServiceServer},
node_service_client::NodeServiceClient,
@@ -2598,6 +2612,28 @@ mod tests {
},
rustfs_protos::canonical_rename_part_request_body
);
assert_gated!(
prepare_part_transaction,
PreparePartTransactionRequest {
disk: disk.clone(),
src_volume: "src".into(),
src_path: "sp".into(),
dst_volume: "dst".into(),
dst_path: "dp".into(),
meta: vec![0x04].into(),
},
rustfs_protos::canonical_prepare_part_transaction_request_body
);
assert_gated!(
settle_part_transaction,
SettlePartTransactionRequest {
disk: disk.clone(),
volume: "dst".into(),
path: "dp".into(),
rollback: true,
},
rustfs_protos::canonical_settle_part_transaction_request_body
);
assert_gated!(
delete_volume,
DeleteVolumeRequest {
@@ -3316,6 +3352,39 @@ mod tests {
assert!(rename_response.error.is_some());
}
#[tokio::test]
async fn test_part_transaction_invalid_disk() {
let service = create_test_node_service();
let prepare = service
.prepare_part_transaction(Request::new(PreparePartTransactionRequest {
disk: "invalid-disk-path".to_string(),
src_volume: "src-volume".to_string(),
src_path: "src-path".to_string(),
dst_volume: "dst-volume".to_string(),
dst_path: "dst-path".to_string(),
meta: Bytes::new(),
}))
.await
.expect("prepare RPC should return a structured disk error")
.into_inner();
assert!(!prepare.success);
assert!(prepare.error.is_some());
let settle = service
.settle_part_transaction(Request::new(SettlePartTransactionRequest {
disk: "invalid-disk-path".to_string(),
volume: "dst-volume".to_string(),
path: "dst-path".to_string(),
rollback: true,
}))
.await
.expect("settle RPC should return a structured disk error")
.into_inner();
assert!(!settle.success);
assert!(settle.error.is_some());
}
#[tokio::test]
async fn test_rename_file_invalid_disk() {
let service = create_test_node_service();
+73 -1
View File
@@ -18,7 +18,7 @@ use crate::storage::storage_api::rpc_consumer::node_service::{
ReadMultipleResp, ReadOptions, StorageDiskRpcExt as _, UpdateMetadataOpts, validate_batch_read_version_item_count,
};
use crate::storage::storage_api::runtime_sources_consumer::runtime_sources;
use crate::storage::storage_api::verify_tonic_mutation_body_digest;
use crate::storage::storage_api::{PartTransactionAction, verify_tonic_mutation_body_digest};
use bytes::Bytes;
use rustfs_filemeta::FileInfo;
use rustfs_io_metrics::internode_metrics::{
@@ -1038,6 +1038,78 @@ impl NodeService {
}
}
pub(super) async fn handle_prepare_part_transaction(
&self,
request: Request<PreparePartTransactionRequest>,
) -> Result<Response<PreparePartTransactionResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_prepare_part_transaction_request_body(request.get_ref()),
"prepare_part_transaction",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk
.prepare_part_transaction(
&request.src_volume,
&request.src_path,
&request.dst_volume,
&request.dst_path,
request.meta,
)
.await
{
Ok(()) => Ok(Response::new(PreparePartTransactionResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(PreparePartTransactionResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(PreparePartTransactionResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_settle_part_transaction(
&self,
request: Request<SettlePartTransactionRequest>,
) -> Result<Response<SettlePartTransactionResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_settle_part_transaction_request_body(request.get_ref()),
"settle_part_transaction",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let action = if request.rollback {
PartTransactionAction::Rollback
} else {
PartTransactionAction::Commit
};
match disk.settle_part_transaction(&request.volume, &request.path, action).await {
Ok(()) => Ok(Response::new(SettlePartTransactionResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(SettlePartTransactionResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(SettlePartTransactionResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_check_parts(
&self,
request: Request<CheckPartsRequest>,
+28 -3
View File
@@ -430,9 +430,9 @@ pub(crate) mod ecstore_data_usage {
pub(crate) mod ecstore_disk {
pub(crate) use rustfs_ecstore::api::disk::{
BatchReadVersionReq, BatchReadVersionResp, CheckPartsResp, DeleteOptions, DiskAPI, DiskInfo, DiskInfoOptions, DiskStore,
FileInfoVersions, FileReader, FileWriter, OldCurrentSize, RUSTFS_META_BUCKET, ReadMultipleReq, ReadMultipleResp,
ReadOptions, RenameDataResp, UpdateMetadataOpts, VolumeInfo, WalkDirOptions, get_object_disk_read_timeout,
validate_batch_read_version_item_count,
FileInfoVersions, FileReader, FileWriter, OldCurrentSize, PartTransactionAction, RUSTFS_META_BUCKET, ReadMultipleReq,
ReadMultipleResp, ReadOptions, RenameDataResp, UpdateMetadataOpts, VolumeInfo, WalkDirOptions,
get_object_disk_read_timeout, validate_batch_read_version_item_count,
};
pub(crate) use rustfs_ecstore::api::disk::{endpoint, error, error_reduce};
}
@@ -629,6 +629,7 @@ pub(crate) type ReadMultipleReq = ecstore_disk::ReadMultipleReq;
pub(crate) type ReadMultipleResp = ecstore_disk::ReadMultipleResp;
pub(crate) type ReadOptions = ecstore_disk::ReadOptions;
pub(crate) type OldCurrentSize = ecstore_disk::OldCurrentSize;
pub(crate) type PartTransactionAction = ecstore_disk::PartTransactionAction;
pub(crate) type RenameDataResp = ecstore_disk::RenameDataResp;
pub(crate) type ReplicationStatusType = ecstore_bucket::replication::ReplicationStatusType;
pub(crate) type ReplicationStats = StorageReplicationStatsHandle;
@@ -1097,6 +1098,15 @@ pub(crate) trait StorageDiskRpcExt {
dst_path: &str,
meta: bytes::Bytes,
) -> DiskResult<()>;
async fn prepare_part_transaction(
&self,
src_volume: &str,
src_path: &str,
dst_volume: &str,
dst_path: &str,
meta: bytes::Bytes,
) -> DiskResult<()>;
async fn settle_part_transaction(&self, volume: &str, path: &str, action: PartTransactionAction) -> DiskResult<()>;
async fn delete(&self, volume: &str, path: &str, options: DeleteOptions) -> DiskResult<()>;
async fn verify_file(&self, volume: &str, path: &str, file_info: &rustfs_filemeta::FileInfo) -> DiskResult<CheckPartsResp>;
async fn check_parts(&self, volume: &str, path: &str, file_info: &rustfs_filemeta::FileInfo) -> DiskResult<CheckPartsResp>;
@@ -1241,6 +1251,21 @@ where
ecstore_disk::DiskAPI::rename_part(self, src_volume, src_path, dst_volume, dst_path, meta).await
}
async fn prepare_part_transaction(
&self,
src_volume: &str,
src_path: &str,
dst_volume: &str,
dst_path: &str,
meta: bytes::Bytes,
) -> DiskResult<()> {
ecstore_disk::DiskAPI::prepare_part_transaction(self, src_volume, src_path, dst_volume, dst_path, meta).await
}
async fn settle_part_transaction(&self, volume: &str, path: &str, action: PartTransactionAction) -> DiskResult<()> {
ecstore_disk::DiskAPI::settle_part_transaction(self, volume, path, action).await
}
async fn delete(&self, volume: &str, path: &str, options: DeleteOptions) -> DiskResult<()> {
ecstore_disk::DiskAPI::delete(self, volume, path, options).await
}