Files
rustfs/rustfs/src/storage/rpc/node_service/disk.rs
T
2026-07-26 23:03:30 +08:00

1631 lines
70 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::NodeService;
use crate::storage::storage_api::rpc_consumer::node_service::{
BatchReadVersionReq, BatchReadVersionResp, DeleteOptions, DiskError, DiskInfoOptions, FileInfoVersions, ReadMultipleReq,
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 bytes::Bytes;
use rustfs_filemeta::FileInfo;
use rustfs_io_metrics::internode_metrics::{
INTERNODE_MSGPACK_CODEC_JSON, INTERNODE_MSGPACK_CODEC_MSGPACK, INTERNODE_MSGPACK_DIRECTION_REQUEST,
INTERNODE_OPERATION_GRPC_READ_ALL, INTERNODE_OPERATION_GRPC_WRITE_ALL, INTERNODE_TRANSPORT_BACKEND_GRPC,
global_internode_metrics,
};
use rustfs_protos::proto_gen::node_service::*;
use serde::de::DeserializeOwned;
use std::io::Cursor;
use tonic::{Request, Response, Status};
use tracing::debug;
/// Initial capacity hint (bytes) for msgpack encode buffers, sized to cover a typical single-
/// version `FileInfo` without repeated growth reallocations. Larger payloads still grow as needed.
const MSGPACK_ENCODE_CAPACITY_HINT: usize = 512;
fn decode_msgpack_or_json<T: DeserializeOwned>(
binary: &[u8],
json: &str,
value_name: &'static str,
) -> std::result::Result<T, DiskError> {
if !binary.is_empty() {
let mut deserializer = rmp_serde::Deserializer::new(Cursor::new(binary));
return match T::deserialize(&mut deserializer) {
Ok(value) => {
global_internode_metrics().record_msgpack_json_decode(
INTERNODE_MSGPACK_DIRECTION_REQUEST,
value_name,
INTERNODE_MSGPACK_CODEC_MSGPACK,
);
Ok(value)
}
Err(err) => {
global_internode_metrics().record_msgpack_json_decode_error(
INTERNODE_MSGPACK_DIRECTION_REQUEST,
value_name,
INTERNODE_MSGPACK_CODEC_MSGPACK,
);
Err(DiskError::other(format!("decode {value_name} msgpack failed: {err}")))
}
};
}
// The msgpack payload was absent, so fall back to the JSON compatibility field. This branch
// must read zero across a release window before the redundant JSON fields can be dropped (P2).
global_internode_metrics().record_msgpack_json_fallback(INTERNODE_MSGPACK_DIRECTION_REQUEST, value_name);
match serde_json::from_str(json) {
Ok(value) => {
global_internode_metrics().record_msgpack_json_decode(
INTERNODE_MSGPACK_DIRECTION_REQUEST,
value_name,
INTERNODE_MSGPACK_CODEC_JSON,
);
Ok(value)
}
Err(err) => {
global_internode_metrics().record_msgpack_json_decode_error(
INTERNODE_MSGPACK_DIRECTION_REQUEST,
value_name,
INTERNODE_MSGPACK_CODEC_JSON,
);
Err(DiskError::other(format!("decode {value_name} failed: {err}")))
}
}
}
fn encode_msgpack<T: serde::Serialize>(value: &T, value_name: &str) -> std::result::Result<Vec<u8>, DiskError> {
let mut serializer = rmp_serde::Serializer::new(Vec::with_capacity(MSGPACK_ENCODE_CAPACITY_HINT));
value
.serialize(&mut serializer)
.map_err(|err| DiskError::other(format!("encode {value_name} msgpack failed: {err}")))?;
Ok(serializer.into_inner())
}
fn encode_msgpack_named<T: serde::Serialize>(value: &T, value_name: &str) -> std::result::Result<Vec<u8>, DiskError> {
let mut serializer = rmp_serde::Serializer::new(Vec::with_capacity(MSGPACK_ENCODE_CAPACITY_HINT)).with_struct_map();
value
.serialize(&mut serializer)
.map_err(|err| DiskError::other(format!("encode {value_name} named msgpack failed: {err}")))?;
Ok(serializer.into_inner())
}
/// Enforce the signature-bound canonical body digest on a mutating disk RPC (backlog#1327).
///
/// Digest-bearing requests are verified against the canonical bytes rebuilt from the received
/// wire fields — which cover both the msgpack `_bin` payloads and their JSON compatibility
/// copies, so tampering with either encoding (or stripping `_bin` to force the JSON fallback
/// decode) is rejected. Digestless requests fall back per
/// `RUSTFS_INTERNODE_RPC_BODY_DIGEST_STRICT` (default: accept + convergence counter).
fn verify_disk_mutation_digest<T>(
request: &Request<T>,
canonical_body: std::result::Result<Vec<u8>, std::num::TryFromIntError>,
op: &'static str,
) -> std::result::Result<(), Status> {
let canonical_body =
canonical_body.map_err(|_| Status::invalid_argument(format!("{op} request length cannot be represented")))?;
verify_tonic_mutation_body_digest(request, &canonical_body)
.map_err(|err| Status::permission_denied(format!("{op} authentication failed: {err}")))
}
/// JSON compatibility string for a dual-encoded response field. Returns an empty string only when
/// msgpack-only mode and its explicit fleet confirmation guard are both enabled; otherwise the
/// legacy JSON encoding is retained for old peers. The paired `_bin` field is always sent.
fn compat_response_json<T: serde::Serialize>(value: &T) -> std::result::Result<String, serde_json::Error> {
if rustfs_protos::internode_rpc_msgpack_only() {
return Ok(String::new());
}
serde_json::to_string(value)
}
fn encode_read_multiple_response_payloads(
read_multiple_resps: &[ReadMultipleResp],
) -> std::result::Result<(Vec<String>, Vec<Bytes>), DiskError> {
let mut read_multiple_resps_json = Vec::with_capacity(read_multiple_resps.len());
let mut read_multiple_resps_bin = Vec::with_capacity(read_multiple_resps.len());
for read_multiple_resp in read_multiple_resps {
read_multiple_resps_json.push(
compat_response_json(read_multiple_resp)
.map_err(|err| DiskError::other(format!("encode ReadMultipleResp json failed: {err}")))?,
);
read_multiple_resps_bin.push(Bytes::from(encode_msgpack(read_multiple_resp, "ReadMultipleResp")?));
}
Ok((read_multiple_resps_json, read_multiple_resps_bin))
}
fn encode_batch_read_version_response_payloads(
batch_read_version_resps: &[BatchReadVersionResp],
) -> std::result::Result<(Vec<String>, Vec<Bytes>), DiskError> {
let mut batch_read_version_resps_json = Vec::with_capacity(batch_read_version_resps.len());
let mut batch_read_version_resps_bin = Vec::with_capacity(batch_read_version_resps.len());
for batch_read_version_resp in batch_read_version_resps {
batch_read_version_resps_json.push(
compat_response_json(batch_read_version_resp)
.map_err(|err| DiskError::other(format!("encode BatchReadVersionResp json failed: {err}")))?,
);
batch_read_version_resps_bin.push(Bytes::from(encode_msgpack(batch_read_version_resp, "BatchReadVersionResp")?));
}
Ok((batch_read_version_resps_json, batch_read_version_resps_bin))
}
impl NodeService {
pub(super) async fn handle_disk_info(&self, request: Request<DiskInfoRequest>) -> Result<Response<DiskInfoResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let opts = match serde_json::from_str::<DiskInfoOptions>(&request.opts) {
Ok(opts) => opts,
Err(err) => {
return Ok(Response::new(DiskInfoResponse {
success: false,
disk_info: "".to_string(),
error: Some(DiskError::other(format!("decode DiskInfoOptions failed: {err}")).into()),
}));
}
};
match disk.disk_info(&opts).await {
Ok(disk_info) => match serde_json::to_string(&disk_info) {
Ok(disk_info) => Ok(Response::new(DiskInfoResponse {
success: true,
disk_info,
error: None,
})),
Err(err) => Ok(Response::new(DiskInfoResponse {
success: false,
disk_info: "".to_string(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
})),
},
Err(err) => Ok(Response::new(DiskInfoResponse {
success: false,
disk_info: "".to_string(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(DiskInfoResponse {
success: false,
disk_info: "".to_string(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_delete_volume(
&self,
request: Request<DeleteVolumeRequest>,
) -> Result<Response<DeleteVolumeResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_delete_volume_request_body(request.get_ref()),
"delete_volume",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.delete_volume(&request.volume, request.force).await {
Ok(_) => Ok(Response::new(DeleteVolumeResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(DeleteVolumeResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(DeleteVolumeResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_read_multiple(
&self,
request: Request<ReadMultipleRequest>,
) -> Result<Response<ReadMultipleResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let read_multiple_req = match decode_msgpack_or_json::<ReadMultipleReq>(
&request.read_multiple_req_bin,
&request.read_multiple_req,
"ReadMultipleReq",
) {
Ok(read_multiple_req) => read_multiple_req,
Err(err) => {
return Ok(Response::new(ReadMultipleResponse {
success: false,
read_multiple_resps: Vec::new(),
read_multiple_resps_bin: Vec::new(),
error: Some(DiskError::other(format!("decode ReadMultipleReq failed: {err}")).into()),
}));
}
};
match disk.read_multiple(read_multiple_req).await {
Ok(read_multiple_resps) => {
let (read_multiple_resps, read_multiple_resps_bin) =
match encode_read_multiple_response_payloads(&read_multiple_resps) {
Ok(payloads) => payloads,
Err(err) => {
return Ok(Response::new(ReadMultipleResponse {
success: false,
read_multiple_resps: Vec::new(),
read_multiple_resps_bin: Vec::new(),
error: Some(err.into()),
}));
}
};
Ok(Response::new(ReadMultipleResponse {
success: true,
read_multiple_resps,
read_multiple_resps_bin,
error: None,
}))
}
Err(err) => Ok(Response::new(ReadMultipleResponse {
success: false,
read_multiple_resps: Vec::new(),
read_multiple_resps_bin: Vec::new(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(ReadMultipleResponse {
success: false,
read_multiple_resps: Vec::new(),
read_multiple_resps_bin: Vec::new(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_batch_read_version(
&self,
request: Request<BatchReadVersionRequest>,
) -> Result<Response<BatchReadVersionResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let batch_read_version_req: BatchReadVersionReq = match decode_msgpack_or_json(
&request.batch_read_version_req_bin,
&request.batch_read_version_req,
"BatchReadVersionReq",
) {
Ok(batch_read_version_req) => batch_read_version_req,
Err(err) => {
return Ok(Response::new(BatchReadVersionResponse {
success: false,
batch_read_version_resps: Vec::new(),
batch_read_version_resps_bin: Vec::new(),
error: Some(DiskError::other(format!("decode BatchReadVersionReq failed: {err}")).into()),
}));
}
};
if let Err(err) = validate_batch_read_version_item_count(batch_read_version_req.items.len()) {
return Ok(Response::new(BatchReadVersionResponse {
success: false,
batch_read_version_resps: Vec::new(),
batch_read_version_resps_bin: Vec::new(),
error: Some(err.into()),
}));
}
match disk.batch_read_version(batch_read_version_req).await {
Ok(batch_read_version_resps) => {
let (batch_read_version_resps, batch_read_version_resps_bin) =
match encode_batch_read_version_response_payloads(&batch_read_version_resps) {
Ok(payloads) => payloads,
Err(err) => {
return Ok(Response::new(BatchReadVersionResponse {
success: false,
batch_read_version_resps: Vec::new(),
batch_read_version_resps_bin: Vec::new(),
error: Some(err.into()),
}));
}
};
Ok(Response::new(BatchReadVersionResponse {
success: true,
batch_read_version_resps,
batch_read_version_resps_bin,
error: None,
}))
}
Err(err) => Ok(Response::new(BatchReadVersionResponse {
success: false,
batch_read_version_resps: Vec::new(),
batch_read_version_resps_bin: Vec::new(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(BatchReadVersionResponse {
success: false,
batch_read_version_resps: Vec::new(),
batch_read_version_resps_bin: Vec::new(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_delete_versions(
&self,
request: Request<DeleteVersionsRequest>,
) -> Result<Response<DeleteVersionsResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_delete_versions_request_body(request.get_ref()),
"delete_versions",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let mut versions = Vec::with_capacity(request.versions.len());
for (index, version) in request.versions.iter().enumerate() {
let version_bin = request.versions_bin.get(index).map(|b| b.as_ref()).unwrap_or(&[]);
match decode_msgpack_or_json::<FileInfoVersions>(version_bin, version, "FileInfoVersions") {
Ok(version) => versions.push(version),
Err(err) => {
return Ok(Response::new(DeleteVersionsResponse {
success: false,
errors: Vec::new(),
error: Some(DiskError::other(format!("decode FileInfoVersions failed: {err}")).into()),
}));
}
};
}
let opts = match decode_msgpack_or_json::<DeleteOptions>(&request.opts_bin, &request.opts, "DeleteOptions") {
Ok(opts) => opts,
Err(err) => {
return Ok(Response::new(DeleteVersionsResponse {
success: false,
errors: Vec::new(),
error: Some(DiskError::other(format!("decode DeleteOptions failed: {err}")).into()),
}));
}
};
let errors = disk
.delete_versions(&request.volume, versions, opts)
.await
.into_iter()
.map(|error| match error {
Some(e) => e.to_string(),
None => "".to_string(),
})
.collect();
Ok(Response::new(DeleteVersionsResponse {
success: true,
errors,
error: None,
}))
} else {
Ok(Response::new(DeleteVersionsResponse {
success: false,
errors: Vec::new(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_delete_version(
&self,
request: Request<DeleteVersionRequest>,
) -> Result<Response<DeleteVersionResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_delete_version_request_body(request.get_ref()),
"delete_version",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let file_info = match decode_msgpack_or_json::<FileInfo>(&request.file_info_bin, &request.file_info, "FileInfo") {
Ok(file_info) => file_info,
Err(err) => {
return Ok(Response::new(DeleteVersionResponse {
success: false,
raw_file_info: "".to_string(),
error: Some(DiskError::other(format!("decode FileInfo failed: {err}")).into()),
}));
}
};
let opts = match decode_msgpack_or_json::<DeleteOptions>(&request.opts_bin, &request.opts, "DeleteOptions") {
Ok(opts) => opts,
Err(err) => {
return Ok(Response::new(DeleteVersionResponse {
success: false,
raw_file_info: "".to_string(),
error: Some(DiskError::other(format!("decode DeleteOptions failed: {err}")).into()),
}));
}
};
match disk
.delete_version(&request.volume, &request.path, file_info, request.force_del_marker, opts)
.await
{
Ok(raw_file_info) => match serde_json::to_string(&raw_file_info) {
Ok(raw_file_info) => Ok(Response::new(DeleteVersionResponse {
success: true,
raw_file_info,
error: None,
})),
Err(err) => Ok(Response::new(DeleteVersionResponse {
success: false,
raw_file_info: "".to_string(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
})),
},
Err(err) => Ok(Response::new(DeleteVersionResponse {
success: false,
raw_file_info: "".to_string(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(DeleteVersionResponse {
success: false,
raw_file_info: "".to_string(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_read_xl(&self, request: Request<ReadXlRequest>) -> Result<Response<ReadXlResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.read_xl(&request.volume, &request.path, request.read_data).await {
Ok(raw_file_info) => {
let raw_file_info_json = compat_response_json(&raw_file_info);
let raw_file_info_bin = encode_msgpack(&raw_file_info, "RawFileInfo");
match (raw_file_info_json, raw_file_info_bin) {
(Ok(raw_file_info), Ok(raw_file_info_bin)) => Ok(Response::new(ReadXlResponse {
success: true,
raw_file_info,
raw_file_info_bin: raw_file_info_bin.into(),
error: None,
})),
(Err(err), _) => Ok(Response::new(ReadXlResponse {
success: false,
raw_file_info: String::new(),
raw_file_info_bin: Vec::new().into(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
})),
(_, Err(err)) => Ok(Response::new(ReadXlResponse {
success: false,
raw_file_info: String::new(),
raw_file_info_bin: Vec::new().into(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
})),
}
}
Err(err) => Ok(Response::new(ReadXlResponse {
success: false,
raw_file_info: String::new(),
raw_file_info_bin: Vec::new().into(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(ReadXlResponse {
success: false,
raw_file_info: String::new(),
raw_file_info_bin: Vec::new().into(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_read_version(
&self,
request: Request<ReadVersionRequest>,
) -> Result<Response<ReadVersionResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let opts = match decode_msgpack_or_json::<ReadOptions>(&request.opts_bin, &request.opts, "ReadOptions") {
Ok(options) => options,
Err(err) => {
return Ok(Response::new(ReadVersionResponse {
success: false,
file_info: String::new(),
file_info_bin: Vec::new().into(),
error: Some(DiskError::other(format!("decode ReadOptions failed: {err}")).into()),
}));
}
};
match disk
.read_version("", &request.volume, &request.path, &request.version_id, &opts)
.await
{
Ok(file_info) => {
let file_info_json = compat_response_json(&file_info);
let file_info_bin = encode_msgpack(&file_info, "FileInfo");
match (file_info_json, file_info_bin) {
(Ok(file_info), Ok(file_info_bin)) => Ok(Response::new(ReadVersionResponse {
success: true,
file_info,
file_info_bin: file_info_bin.into(),
error: None,
})),
(Err(err), _) => Ok(Response::new(ReadVersionResponse {
success: false,
file_info: String::new(),
file_info_bin: Vec::new().into(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
})),
(_, Err(err)) => Ok(Response::new(ReadVersionResponse {
success: false,
file_info: String::new(),
file_info_bin: Vec::new().into(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
})),
}
}
Err(err) => Ok(Response::new(ReadVersionResponse {
success: false,
file_info: String::new(),
file_info_bin: Vec::new().into(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(ReadVersionResponse {
success: false,
file_info: String::new(),
file_info_bin: Vec::new().into(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_write_metadata(
&self,
request: Request<WriteMetadataRequest>,
) -> Result<Response<WriteMetadataResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_write_metadata_request_body(request.get_ref()),
"write_metadata",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let file_info = match decode_msgpack_or_json::<FileInfo>(&request.file_info_bin, &request.file_info, "FileInfo") {
Ok(file_info) => file_info,
Err(err) => {
return Ok(Response::new(WriteMetadataResponse {
success: false,
error: Some(DiskError::other(format!("decode FileInfo failed: {err}")).into()),
}));
}
};
match disk.write_metadata("", &request.volume, &request.path, file_info).await {
Ok(_) => Ok(Response::new(WriteMetadataResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(WriteMetadataResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(WriteMetadataResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_update_metadata(
&self,
request: Request<UpdateMetadataRequest>,
) -> Result<Response<UpdateMetadataResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_update_metadata_request_body(request.get_ref()),
"update_metadata",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let file_info = match decode_msgpack_or_json::<FileInfo>(&request.file_info_bin, &request.file_info, "FileInfo") {
Ok(file_info) => file_info,
Err(err) => {
return Ok(Response::new(UpdateMetadataResponse {
success: false,
error: Some(DiskError::other(format!("decode FileInfo failed: {err}")).into()),
}));
}
};
let opts = match decode_msgpack_or_json::<UpdateMetadataOpts>(&request.opts_bin, &request.opts, "UpdateMetadataOpts")
{
Ok(opts) => opts,
Err(err) => {
return Ok(Response::new(UpdateMetadataResponse {
success: false,
error: Some(DiskError::other(format!("decode UpdateMetadataOpts failed: {err}")).into()),
}));
}
};
match disk.update_metadata(&request.volume, &request.path, file_info, &opts).await {
Ok(_) => Ok(Response::new(UpdateMetadataResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(UpdateMetadataResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(UpdateMetadataResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_read_metadata(
&self,
request: Request<ReadMetadataRequest>,
) -> Result<Response<ReadMetadataResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.read_metadata(&request.volume, &request.path).await {
Ok(data) => Ok(Response::new(ReadMetadataResponse {
success: true,
data,
error: None,
})),
Err(err) => Ok(Response::new(ReadMetadataResponse {
success: false,
data: Bytes::new(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(ReadMetadataResponse {
success: false,
data: Bytes::new(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_delete_paths(
&self,
request: Request<DeletePathsRequest>,
) -> Result<Response<DeletePathsResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_delete_paths_request_body(request.get_ref()),
"delete_paths",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.delete_paths(&request.volume, &request.paths).await {
Ok(_) => Ok(Response::new(DeletePathsResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(DeletePathsResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(DeletePathsResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_stat_volume(
&self,
request: Request<StatVolumeRequest>,
) -> Result<Response<StatVolumeResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.stat_volume(&request.volume).await {
Ok(volume_info) => match serde_json::to_string(&volume_info) {
Ok(volume_info) => Ok(Response::new(StatVolumeResponse {
success: true,
volume_info,
error: None,
})),
Err(err) => Ok(Response::new(StatVolumeResponse {
success: false,
volume_info: String::new(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
})),
},
Err(err) => Ok(Response::new(StatVolumeResponse {
success: false,
volume_info: String::new(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(StatVolumeResponse {
success: false,
volume_info: String::new(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_list_volumes(
&self,
request: Request<ListVolumesRequest>,
) -> Result<Response<ListVolumesResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.list_volumes().await {
Ok(volume_infos) => {
let volume_infos = volume_infos
.into_iter()
.enumerate()
.map(|(index, volume_info)| {
serde_json::to_string(&volume_info)
.map_err(|err| DiskError::other(format!("encode list volumes entry {index} failed: {err}")))
})
.collect::<std::result::Result<Vec<_>, DiskError>>();
match volume_infos {
Ok(volume_infos) => Ok(Response::new(ListVolumesResponse {
success: true,
volume_infos,
error: None,
})),
Err(err) => Ok(Response::new(ListVolumesResponse {
success: false,
volume_infos: Vec::new(),
error: Some(err.into()),
})),
}
}
Err(err) => Ok(Response::new(ListVolumesResponse {
success: false,
volume_infos: Vec::new(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(ListVolumesResponse {
success: false,
volume_infos: Vec::new(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_make_volume(
&self,
request: Request<MakeVolumeRequest>,
) -> Result<Response<MakeVolumeResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_make_volume_request_body(request.get_ref()),
"make_volume",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.make_volume(&request.volume).await {
Ok(_) => Ok(Response::new(MakeVolumeResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(MakeVolumeResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(MakeVolumeResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_make_volumes(
&self,
request: Request<MakeVolumesRequest>,
) -> Result<Response<MakeVolumesResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_make_volumes_request_body(request.get_ref()),
"make_volumes",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.make_volumes(request.volumes.iter().map(|s| &**s).collect()).await {
Ok(_) => Ok(Response::new(MakeVolumesResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(MakeVolumesResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(MakeVolumesResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_rename_data(
&self,
request: Request<RenameDataRequest>,
) -> Result<Response<RenameDataResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_rename_data_request_body(request.get_ref()),
"rename_data",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let file_info = match decode_msgpack_or_json::<FileInfo>(&request.file_info_bin, &request.file_info, "FileInfo") {
Ok(file_info) => file_info,
Err(err) => {
return Ok(Response::new(RenameDataResponse {
success: false,
rename_data_resp: String::new(),
rename_data_resp_bin: Vec::new().into(),
error: Some(DiskError::other(format!("decode FileInfo failed: {err}")).into()),
}));
}
};
match disk
.rename_data(&request.src_volume, &request.src_path, file_info, &request.dst_volume, &request.dst_path)
.await
{
Ok(rename_data_resp) => {
let rename_data_resp_json = compat_response_json(&rename_data_resp);
let rename_data_resp_bin = encode_msgpack_named(&rename_data_resp, "RenameDataResp");
match (rename_data_resp_json, rename_data_resp_bin) {
(Ok(rename_data_resp), Ok(rename_data_resp_bin)) => Ok(Response::new(RenameDataResponse {
success: true,
rename_data_resp,
rename_data_resp_bin: rename_data_resp_bin.into(),
error: None,
})),
(Err(err), _) => Ok(Response::new(RenameDataResponse {
success: false,
rename_data_resp: String::new(),
rename_data_resp_bin: Vec::new().into(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
})),
(_, Err(err)) => Ok(Response::new(RenameDataResponse {
success: false,
rename_data_resp: String::new(),
rename_data_resp_bin: Vec::new().into(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
})),
}
}
Err(err) => Ok(Response::new(RenameDataResponse {
success: false,
rename_data_resp: String::new(),
rename_data_resp_bin: Vec::new().into(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(RenameDataResponse {
success: false,
rename_data_resp: String::new(),
rename_data_resp_bin: Vec::new().into(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_list_dir(&self, request: Request<ListDirRequest>) -> Result<Response<ListDirResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.list_dir("", &request.volume, &request.dir_path, request.count).await {
Ok(volumes) => Ok(Response::new(ListDirResponse {
success: true,
volumes,
error: None,
})),
Err(err) => Ok(Response::new(ListDirResponse {
success: false,
volumes: Vec::new(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(ListDirResponse {
success: false,
volumes: Vec::new(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_write(&self, _request: Request<WriteRequest>) -> Result<Response<WriteResponse>, Status> {
unimplemented!("write");
}
pub(super) async fn handle_rename_file(
&self,
request: Request<RenameFileRequest>,
) -> Result<Response<RenameFileResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_rename_file_request_body(request.get_ref()),
"rename_file",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk
.rename_file(&request.src_volume, &request.src_path, &request.dst_volume, &request.dst_path)
.await
{
Ok(_) => Ok(Response::new(RenameFileResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(RenameFileResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(RenameFileResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_rename_part(
&self,
request: Request<RenamePartRequest>,
) -> Result<Response<RenamePartResponse>, Status> {
verify_disk_mutation_digest(
&request,
rustfs_protos::canonical_rename_part_request_body(request.get_ref()),
"rename_part",
)?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk
.rename_part(
&request.src_volume,
&request.src_path,
&request.dst_volume,
&request.dst_path,
request.meta,
)
.await
{
Ok(_) => Ok(Response::new(RenamePartResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(RenamePartResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(RenamePartResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_check_parts(
&self,
request: Request<CheckPartsRequest>,
) -> Result<Response<CheckPartsResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let file_info = match serde_json::from_str::<FileInfo>(&request.file_info) {
Ok(file_info) => file_info,
Err(err) => {
return Ok(Response::new(CheckPartsResponse {
success: false,
check_parts_resp: "".to_string(),
error: Some(DiskError::other(format!("decode FileInfo failed: {err}")).into()),
}));
}
};
match disk.check_parts(&request.volume, &request.path, &file_info).await {
Ok(check_parts_resp) => {
let check_parts_resp = match serde_json::to_string(&check_parts_resp) {
Ok(check_parts_resp) => check_parts_resp,
Err(err) => {
return Ok(Response::new(CheckPartsResponse {
success: false,
check_parts_resp: String::new(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
}));
}
};
Ok(Response::new(CheckPartsResponse {
success: true,
check_parts_resp,
error: None,
}))
}
Err(err) => Ok(Response::new(CheckPartsResponse {
success: false,
check_parts_resp: "".to_string(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(CheckPartsResponse {
success: false,
check_parts_resp: "".to_string(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_read_parts(
&self,
request: Request<ReadPartsRequest>,
) -> Result<Response<ReadPartsResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.read_parts(&request.bucket, &request.paths).await {
Ok(data) => {
let data = match rmp_serde::to_vec(&data) {
Ok(data) => data,
Err(err) => {
return Ok(Response::new(ReadPartsResponse {
success: false,
object_part_infos: Bytes::new(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
}));
}
};
Ok(Response::new(ReadPartsResponse {
success: true,
object_part_infos: Bytes::copy_from_slice(&data),
error: None,
}))
}
Err(err) => Ok(Response::new(ReadPartsResponse {
success: false,
object_part_infos: Bytes::new(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(ReadPartsResponse {
success: false,
object_part_infos: Bytes::new(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_verify_file(
&self,
request: Request<VerifyFileRequest>,
) -> Result<Response<VerifyFileResponse>, Status> {
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let file_info = match serde_json::from_str::<FileInfo>(&request.file_info) {
Ok(file_info) => file_info,
Err(err) => {
return Ok(Response::new(VerifyFileResponse {
success: false,
check_parts_resp: "".to_string(),
error: Some(DiskError::other(format!("decode FileInfo failed: {err}")).into()),
}));
}
};
match disk.verify_file(&request.volume, &request.path, &file_info).await {
Ok(check_parts_resp) => {
let check_parts_resp = match serde_json::to_string(&check_parts_resp) {
Ok(check_parts_resp) => check_parts_resp,
Err(err) => {
return Ok(Response::new(VerifyFileResponse {
success: false,
check_parts_resp: String::new(),
error: Some(DiskError::other(format!("encode data failed: {err}")).into()),
}));
}
};
Ok(Response::new(VerifyFileResponse {
success: true,
check_parts_resp,
error: None,
}))
}
Err(err) => Ok(Response::new(VerifyFileResponse {
success: false,
check_parts_resp: "".to_string(),
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(VerifyFileResponse {
success: false,
check_parts_resp: "".to_string(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_delete(&self, request: Request<DeleteRequest>) -> Result<Response<DeleteResponse>, Status> {
verify_disk_mutation_digest(&request, rustfs_protos::canonical_delete_request_body(request.get_ref()), "delete")?;
let request = request.into_inner();
if let Some(disk) = self.find_disk(&request.disk).await {
let options = match serde_json::from_str::<DeleteOptions>(&request.options) {
Ok(options) => options,
Err(err) => {
return Ok(Response::new(DeleteResponse {
success: false,
error: Some(DiskError::other(format!("decode DeleteOptions failed: {err}")).into()),
}));
}
};
match disk.delete(&request.volume, &request.path, options).await {
Ok(_) => Ok(Response::new(DeleteResponse {
success: true,
error: None,
})),
Err(err) => Ok(Response::new(DeleteResponse {
success: false,
error: Some(err.into()),
})),
}
} else {
Ok(Response::new(DeleteResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_write_all(&self, request: Request<WriteAllRequest>) -> Result<Response<WriteAllResponse>, Status> {
verify_disk_mutation_digest(&request, rustfs_protos::canonical_write_all_request_body(request.get_ref()), "write_all")?;
let request = request.into_inner();
let data_len = request.data.len();
let metrics = runtime_sources::current_internode_metrics();
metrics.record_incoming_request_for_operation_and_backend(
INTERNODE_OPERATION_GRPC_WRITE_ALL,
INTERNODE_TRANSPORT_BACKEND_GRPC,
);
metrics.record_recv_bytes_for_operation_and_backend(
INTERNODE_OPERATION_GRPC_WRITE_ALL,
INTERNODE_TRANSPORT_BACKEND_GRPC,
data_len,
);
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.write_all(&request.volume, &request.path, request.data).await {
Ok(_) => Ok(Response::new(WriteAllResponse {
success: true,
error: None,
})),
Err(err) => {
metrics.record_error_for_operation_and_backend(
INTERNODE_OPERATION_GRPC_WRITE_ALL,
INTERNODE_TRANSPORT_BACKEND_GRPC,
);
Ok(Response::new(WriteAllResponse {
success: false,
error: Some(err.into()),
}))
}
}
} else {
metrics.record_error_for_operation_and_backend(INTERNODE_OPERATION_GRPC_WRITE_ALL, INTERNODE_TRANSPORT_BACKEND_GRPC);
Ok(Response::new(WriteAllResponse {
success: false,
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
pub(super) async fn handle_read_all(&self, request: Request<ReadAllRequest>) -> Result<Response<ReadAllResponse>, Status> {
debug!("read all");
let request = request.into_inner();
let metrics = runtime_sources::current_internode_metrics();
metrics.record_incoming_request_for_operation_and_backend(
INTERNODE_OPERATION_GRPC_READ_ALL,
INTERNODE_TRANSPORT_BACKEND_GRPC,
);
if let Some(disk) = self.find_disk(&request.disk).await {
match disk.read_all(&request.volume, &request.path).await {
Ok(data) => {
metrics.record_sent_bytes_for_operation_and_backend(
INTERNODE_OPERATION_GRPC_READ_ALL,
INTERNODE_TRANSPORT_BACKEND_GRPC,
data.len(),
);
Ok(Response::new(ReadAllResponse {
success: true,
data,
error: None,
}))
}
Err(err) => {
metrics.record_error_for_operation_and_backend(
INTERNODE_OPERATION_GRPC_READ_ALL,
INTERNODE_TRANSPORT_BACKEND_GRPC,
);
Ok(Response::new(ReadAllResponse {
success: false,
data: Bytes::new(),
error: Some(err.into()),
}))
}
}
} else {
metrics.record_error_for_operation_and_backend(INTERNODE_OPERATION_GRPC_READ_ALL, INTERNODE_TRANSPORT_BACKEND_GRPC);
Ok(Response::new(ReadAllResponse {
success: false,
data: Bytes::new(),
error: Some(DiskError::other("cannot find disk".to_string()).into()),
}))
}
}
}
#[cfg(test)]
mod tests {
use super::{
compat_response_json, decode_msgpack_or_json, encode_batch_read_version_response_payloads, encode_msgpack,
encode_msgpack_named, encode_read_multiple_response_payloads,
};
use crate::storage::storage_api::ReadMultipleResp;
use crate::storage::storage_api::rpc_consumer::node_service::BatchReadVersionResp;
use rustfs_io_metrics::internode_metrics::global_internode_metrics;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct SamplePayload {
name: String,
count: u32,
}
#[test]
fn decode_msgpack_or_json_prefers_binary_payload() {
let payload = SamplePayload {
name: "rustfs".to_string(),
count: 3,
};
let binary = encode_msgpack(&payload, "SamplePayload").unwrap();
let before = global_internode_metrics().msgpack_json_decode_total_for_test();
let decoded =
decode_msgpack_or_json::<SamplePayload>(&binary, r#"{"name":"ignored","count":1}"#, "SamplePayload").unwrap();
let after = global_internode_metrics().msgpack_json_decode_total_for_test();
assert_eq!(decoded, payload);
assert!(after > before, "successful request msgpack decode should increment traffic metrics");
}
#[test]
fn decode_msgpack_or_json_falls_back_to_json() {
let before = global_internode_metrics().msgpack_json_decode_total_for_test();
let decoded = decode_msgpack_or_json::<SamplePayload>(&[], r#"{"name":"compat","count":7}"#, "SamplePayload").unwrap();
let after = global_internode_metrics().msgpack_json_decode_total_for_test();
assert_eq!(
decoded,
SamplePayload {
name: "compat".to_string(),
count: 7,
}
);
assert!(after > before, "successful request JSON fallback decode should increment traffic metrics");
}
fn with_internode_msgpack_env<R>(vars: [(&'static str, Option<&'static str>); 2], f: impl FnOnce() -> R) -> R {
temp_env::with_vars(vars, || {
rustfs_protos::reset_internode_rpc_msgpack_only_cache();
let result = f();
rustfs_protos::reset_internode_rpc_msgpack_only_cache();
result
})
}
#[test]
fn compat_response_json_keeps_json_when_msgpack_only_lacks_fleet_confirmation() {
with_internode_msgpack_env(
[
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("true")),
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, None::<&str>),
],
|| {
let payload = SamplePayload {
name: "legacy-json".to_string(),
count: 9,
};
let json = compat_response_json(&payload).expect("compat response json should encode");
assert!(!json.is_empty(), "old JSON clients must remain compatible without fleet confirmation");
assert_eq!(json, serde_json::to_string(&payload).expect("json should encode"));
},
);
}
#[test]
fn compat_response_json_omits_json_only_after_fleet_confirmation() {
with_internode_msgpack_env(
[
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("true")),
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, Some("true")),
],
|| {
let payload = SamplePayload {
name: "msgpack-only".to_string(),
count: 10,
};
let json = compat_response_json(&payload).expect("compat response json should encode");
assert!(json.is_empty(), "msgpack-only may empty JSON only after explicit fleet confirmation");
},
);
}
#[test]
fn compat_response_json_restores_json_when_either_msgpack_only_gate_is_removed() {
let payload = SamplePayload {
name: "rollback".to_string(),
count: 12,
};
with_internode_msgpack_env(
[
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("true")),
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, Some("true")),
],
|| {
let json = compat_response_json(&payload).expect("compat response json should encode");
assert!(json.is_empty(), "both gates should enter msgpack-only response mode");
},
);
for vars in [
[
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("true")),
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, Some("false")),
],
[
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("false")),
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, Some("true")),
],
] {
with_internode_msgpack_env(vars, || {
let json = compat_response_json(&payload).expect("compat response json should encode");
assert!(!json.is_empty(), "removing either gate should restore old-peer JSON compatibility");
assert_eq!(json, serde_json::to_string(&payload).expect("json should encode"));
});
}
}
#[test]
fn decode_msgpack_or_json_fails_closed_on_corrupt_non_empty_msgpack() {
let before = global_internode_metrics().msgpack_json_decode_error_total_for_test();
let err = decode_msgpack_or_json::<SamplePayload>(b"not-msgpack", r#"{"name":"json","count":1}"#, "SamplePayload")
.expect_err("corrupt non-empty msgpack must not fall back to JSON");
let after = global_internode_metrics().msgpack_json_decode_error_total_for_test();
assert!(err.to_string().contains("decode SamplePayload msgpack failed"), "unexpected error: {err}");
assert!(after > before, "corrupt msgpack should increment decode-error metrics");
}
#[test]
fn decode_msgpack_or_json_reports_corrupt_json_item_when_msgpack_absent() {
let before = global_internode_metrics().msgpack_json_decode_error_total_for_test();
let err = decode_msgpack_or_json::<SamplePayload>(&[], "{not-json", "SamplePayload")
.expect_err("corrupt json item should fail in fallback branch");
let after = global_internode_metrics().msgpack_json_decode_error_total_for_test();
assert!(err.to_string().contains("decode SamplePayload failed"), "unexpected error: {err}");
assert!(after > before, "corrupt fallback JSON should increment decode-error metrics");
}
#[test]
fn decode_msgpack_or_json_accepts_named_msgpack_and_legacy_json() {
let payload = SamplePayload {
name: "named".to_string(),
count: 11,
};
let named_msgpack = encode_msgpack_named(&payload, "SamplePayload").expect("named msgpack should encode");
let decoded_msgpack =
decode_msgpack_or_json::<SamplePayload>(&named_msgpack, "", "SamplePayload").expect("named msgpack should decode");
let decoded_json = decode_msgpack_or_json::<SamplePayload>(&[], r#"{"name":"legacy-json","count":12}"#, "SamplePayload")
.expect("legacy json should decode");
assert_eq!(decoded_msgpack, payload);
assert_eq!(
decoded_json,
SamplePayload {
name: "legacy-json".to_string(),
count: 12
}
);
}
#[test]
fn encode_read_multiple_response_payloads_keeps_json_and_msgpack_in_sync() {
let responses = vec![
ReadMultipleResp {
bucket: "bucket".to_string(),
prefix: "prefix".to_string(),
file: "a".to_string(),
exists: true,
data: b"alpha".to_vec(),
..Default::default()
},
ReadMultipleResp {
bucket: "bucket".to_string(),
prefix: "prefix".to_string(),
file: "b".to_string(),
exists: true,
data: b"beta".to_vec(),
..Default::default()
},
];
let (json_payloads, msgpack_payloads) =
encode_read_multiple_response_payloads(&responses).expect("read multiple responses should encode");
assert_eq!(json_payloads.len(), responses.len());
assert_eq!(msgpack_payloads.len(), responses.len());
let json_decoded: ReadMultipleResp =
serde_json::from_str(&json_payloads[0]).expect("json read multiple response should decode");
let msgpack_decoded = decode_msgpack_or_json::<ReadMultipleResp>(&msgpack_payloads[0], "", "ReadMultipleResp")
.expect("msgpack read multiple response should decode");
assert_eq!(json_decoded.file, responses[0].file);
assert_eq!(msgpack_decoded.file, responses[0].file);
assert_eq!(msgpack_decoded.data, responses[0].data);
}
#[test]
fn encode_read_multiple_response_payloads_respects_msgpack_only_gate_and_rollback() {
let responses = vec![ReadMultipleResp {
bucket: "bucket".to_string(),
prefix: "prefix".to_string(),
file: "gate".to_string(),
exists: true,
data: b"payload".to_vec(),
..Default::default()
}];
with_internode_msgpack_env(
[
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("true")),
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, Some("true")),
],
|| {
let (json_payloads, msgpack_payloads) =
encode_read_multiple_response_payloads(&responses).expect("read multiple responses should encode");
assert_eq!(json_payloads, vec![String::new()]);
assert_eq!(msgpack_payloads.len(), responses.len());
let decoded = decode_msgpack_or_json::<ReadMultipleResp>(&msgpack_payloads[0], "", "ReadMultipleResp")
.expect("msgpack read multiple response should decode");
assert_eq!(decoded.file, responses[0].file);
},
);
with_internode_msgpack_env(
[
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("true")),
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, Some("false")),
],
|| {
let (json_payloads, msgpack_payloads) =
encode_read_multiple_response_payloads(&responses).expect("read multiple responses should encode");
assert!(!json_payloads[0].is_empty(), "rollback should restore response JSON");
assert_eq!(msgpack_payloads.len(), responses.len());
let json_decoded: ReadMultipleResp =
serde_json::from_str(&json_payloads[0]).expect("json read multiple response should decode");
assert_eq!(json_decoded.file, responses[0].file);
},
);
}
#[test]
fn encode_batch_read_version_response_payloads_keeps_json_and_msgpack_in_sync() {
let responses = vec![BatchReadVersionResp {
index: 3,
path: "object-a".to_string(),
version_id: "version-a".to_string(),
success: false,
error: "file version not found".to_string(),
..Default::default()
}];
let (json_payloads, msgpack_payloads) =
encode_batch_read_version_response_payloads(&responses).expect("batch read version responses should encode");
assert_eq!(json_payloads.len(), responses.len());
assert_eq!(msgpack_payloads.len(), responses.len());
let json_decoded: BatchReadVersionResp =
serde_json::from_str(&json_payloads[0]).expect("json batch read version response should decode");
let msgpack_decoded = decode_msgpack_or_json::<BatchReadVersionResp>(&msgpack_payloads[0], "", "BatchReadVersionResp")
.expect("msgpack batch read version response should decode");
assert_eq!(json_decoded.index, responses[0].index);
assert_eq!(msgpack_decoded.path, responses[0].path);
assert_eq!(msgpack_decoded.error, responses[0].error);
}
#[test]
fn encode_batch_read_version_response_payloads_respects_msgpack_only_gate_and_rollback() {
let responses = vec![BatchReadVersionResp {
index: 4,
path: "object-b".to_string(),
version_id: "version-b".to_string(),
success: true,
..Default::default()
}];
with_internode_msgpack_env(
[
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("true")),
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, Some("true")),
],
|| {
let (json_payloads, msgpack_payloads) =
encode_batch_read_version_response_payloads(&responses).expect("batch read version responses should encode");
assert_eq!(json_payloads, vec![String::new()]);
assert_eq!(msgpack_payloads.len(), responses.len());
let decoded = decode_msgpack_or_json::<BatchReadVersionResp>(&msgpack_payloads[0], "", "BatchReadVersionResp")
.expect("msgpack batch read version response should decode");
assert_eq!(decoded.path, responses[0].path);
},
);
with_internode_msgpack_env(
[
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, Some("false")),
(rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, Some("true")),
],
|| {
let (json_payloads, msgpack_payloads) =
encode_batch_read_version_response_payloads(&responses).expect("batch read version responses should encode");
assert!(!json_payloads[0].is_empty(), "rollback should restore response JSON");
assert_eq!(msgpack_payloads.len(), responses.len());
let json_decoded: BatchReadVersionResp =
serde_json::from_str(&json_payloads[0]).expect("json batch read version response should decode");
assert_eq!(json_decoded.path, responses[0].path);
},
);
}
}