// 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 crate::cluster::rpc::client::{
    AuthenticatedChannel, TonicInterceptor, gen_tonic_signature_interceptor, is_network_like_disk_error,
    node_service_time_out_client, node_service_time_out_client_for_class, node_service_time_out_client_no_auth,
};
use crate::cluster::rpc::http_auth::set_tonic_canonical_body_digest;
use crate::cluster::rpc::internode_data_transport::{
    InternodeDataTransport, NsScannerCapabilityRequest, NsScannerStreamRequest, ReadStreamRequest, WalkDirStreamRequest,
    WriteStreamRequest,
};
use crate::disk::error::{Error, Result};
use crate::disk::{
    BatchReadVersionReq, BatchReadVersionResp, CheckPartsResp, DeleteOptions, DiskAPI, DiskInfo, DiskInfoOptions, DiskLocation,
    DiskOption, FileInfoVersions, FileReader, FileWriter, PartTransactionAction, ReadMultipleReq, ReadMultipleResp, ReadOptions,
    RenameDataResp, SnapshotLeaseToken, UpdateMetadataOpts, VolumeInfo, WalkDirOptions, batch_read_version_one_by_one,
    disk_store::{
        DEFAULT_RUSTFS_DRIVE_ACTIVE_MONITORING, ENV_RUSTFS_DRIVE_ACTIVE_MONITORING, SKIP_IF_SUCCESS_BEFORE,
        get_drive_active_check_interval, get_drive_active_check_timeout, get_drive_disk_info_timeout, get_drive_list_dir_timeout,
        get_drive_metadata_timeout, get_drive_walkdir_stall_timeout, get_drive_walkdir_timeout, get_max_timeout_duration,
        get_object_disk_read_timeout,
    },
    endpoint::Endpoint,
    health_state::{RuntimeDriveHealthState, get_drive_returning_probe_interval, record_drive_runtime_state},
    validate_batch_read_version_item_count,
};
use crate::disk::{disk_store::DiskHealthTracker, error::DiskError, local::ScanGuard};
use crate::set_disk::DEFAULT_READ_BUFFER_SIZE;
use bytes::Bytes;
use futures::lock::Mutex;
use metrics::counter;
use rustfs_filemeta::{FileInfo, ObjectPartInfo, RawFileInfo};
use rustfs_protos::ChannelClass;
use rustfs_protos::evict_failed_connection;
use rustfs_protos::proto_gen::node_service::RenamePartRequest;
use rustfs_protos::proto_gen::node_service::{
    BatchReadVersionRequest, BatchReadVersionResponse, CheckPartsRequest, DeletePathsRequest, DeleteRequest,
    DeleteVersionRequest, DeleteVersionsRequest, DeleteVolumeRequest, DiskInfoRequest, ListDirRequest, ListVolumesRequest,
    MakeVolumeRequest, MakeVolumesRequest, PreparePartTransactionRequest, ReadAllRequest, ReadMetadataRequest,
    ReadMultipleRequest, ReadMultipleResponse, ReadPartsRequest, ReadVersionRequest, ReadXlRequest, RenameDataRequest,
    RenameFileRequest, SettlePartTransactionRequest, SnapshotLeaseReleaseRequest, SnapshotLeaseRenewRequest,
    SnapshotLeaseRequest, SnapshotLeaseResponse, StatVolumeRequest, UpdateMetadataRequest, VerifyFileRequest, WriteAllRequest,
    WriteMetadataRequest, node_service_client::NodeServiceClient,
};
use serde::{Serialize, de::DeserializeOwned};
use std::{
    io::Cursor,
    path::PathBuf,
    sync::{
        Arc,
        atomic::{AtomicU32, Ordering},
    },
    time::Duration,
};
use tokio::time;
use tokio::{
    io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
    net::TcpStream,
    time::timeout,
};
use tokio_util::sync::CancellationToken;
use tonic::{Code, Request, service::interceptor::InterceptedService};
use tracing::{debug, trace, warn};
use uuid::Uuid;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FailureHealthAction {
    MarkFailure,
    IgnoreFailure,
}

const REMOTE_DISK_OPEN_WRITE_MAX_ATTEMPTS: usize = 2;
const REMOTE_DISK_OPEN_WRITE_RETRY_BACKOFF: Duration = Duration::from_millis(20);
const REMOTE_DISK_OPEN_READ_MAX_ATTEMPTS: usize = 2;
const REMOTE_DISK_OPEN_READ_RETRY_BACKOFF: Duration = Duration::from_millis(20);
const NS_SCANNER_CAPABILITY_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
/// Base backoff for idempotent read-only RPC retries (grpc-optimization P3-3); doubles per attempt.
const REMOTE_DISK_READ_RETRY_BASE_BACKOFF: Duration = Duration::from_millis(50);
const ENV_RUSTFS_METADATA_BATCH_READ: &str = "RUSTFS_METADATA_BATCH_READ";
const LEGACY_ENV_RUSTFS_BATCH_METADATA_RPC: &str = "RUSTFS_BATCH_METADATA_RPC";
const BATCH_METADATA_RPC_OFF: &str = "off";
const BATCH_METADATA_RPC_AUTO: &str = "auto";
const BATCH_METADATA_RPC_ON: &str = "on";
const BATCH_READ_VERSION_GATE_ATTEMPT: &str = "attempt";
const BATCH_READ_VERSION_GATE_OFF_UNARY: &str = "off_unary";
const BATCH_READ_VERSION_GATE_FALLBACK_UNIMPLEMENTED: &str = "fallback_unimplemented";
const BATCH_READ_VERSION_GATE_UNSUPPORTED_NO_FALLBACK: &str = "unsupported_no_fallback";
const LOG_COMPONENT_ECSTORE: &str = "ecstore";
const LOG_SUBSYSTEM_REMOTE_DISK: &str = "remote_disk";
const EVENT_REMOTE_DISK_HEALTH: &str = "remote_disk_health";
const EVENT_REMOTE_DISK_RPC: &str = "remote_disk_rpc";
const SNAPSHOT_LEASE_PROTOCOL_VERSION: u32 = 1;
pub const REMOTE_SNAPSHOT_LEASE_TTL: Duration = Duration::from_secs(60);

fn snapshot_lease_token_from_response(response: SnapshotLeaseResponse) -> Result<SnapshotLeaseToken> {
    if !response.success {
        return Err(response.error.unwrap_or_default().into());
    }
    if response.protocol_version != SNAPSHOT_LEASE_PROTOCOL_VERSION {
        return Err(Error::other("remote snapshot lease protocol is incompatible"));
    }
    SnapshotLeaseToken::from_slice(&response.token)
}

/// Bind a mutating disk RPC to its canonical body: the digest lands in the request metadata, and
/// the signing interceptor folds it (plus a replay-protected nonce) into the v2 signature scope
/// (backlog#1327).
fn attach_mutation_body_digest<T>(
    request: &mut Request<T>,
    canonical_body: std::result::Result<Vec<u8>, std::num::TryFromIntError>,
    op: &'static str,
) -> Result<()> {
    let canonical_body = canonical_body.map_err(|_| Error::other(format!("{op} request length cannot be represented")))?;
    set_tonic_canonical_body_digest(request, &canonical_body).map_err(Error::other)
}

fn decode_volume_infos(volume_infos: Vec<String>) -> Result<Vec<VolumeInfo>> {
    volume_infos
        .into_iter()
        .enumerate()
        .map(|(index, json)| {
            serde_json::from_str::<VolumeInfo>(&json)
                .map_err(|err| Error::other(format!("decode list volumes entry {index} failed: {err}")))
        })
        .collect()
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum BatchMetadataRpcMode {
    Off,
    Auto,
    On,
}

impl BatchMetadataRpcMode {
    fn as_str(self) -> &'static str {
        match self {
            Self::Off => BATCH_METADATA_RPC_OFF,
            Self::Auto => BATCH_METADATA_RPC_AUTO,
            Self::On => BATCH_METADATA_RPC_ON,
        }
    }

    fn should_attempt(self) -> bool {
        matches!(self, Self::Auto | Self::On)
    }

    fn should_fallback_on_unimplemented(self) -> bool {
        matches!(self, Self::Auto)
    }
}

fn parse_batch_metadata_rpc_mode(raw: &str) -> BatchMetadataRpcMode {
    match raw.trim() {
        value if value.eq_ignore_ascii_case(BATCH_METADATA_RPC_AUTO) => BatchMetadataRpcMode::Auto,
        value if value.eq_ignore_ascii_case(BATCH_METADATA_RPC_ON) => BatchMetadataRpcMode::On,
        value if value.eq_ignore_ascii_case(BATCH_METADATA_RPC_OFF) => BatchMetadataRpcMode::Off,
        _ => BatchMetadataRpcMode::Off,
    }
}

fn batch_metadata_rpc_mode_from_env() -> BatchMetadataRpcMode {
    rustfs_utils::get_env_opt_str(ENV_RUSTFS_METADATA_BATCH_READ)
        .or_else(|| rustfs_utils::get_env_opt_str(LEGACY_ENV_RUSTFS_BATCH_METADATA_RPC))
        .as_deref()
        .map(parse_batch_metadata_rpc_mode)
        .unwrap_or(BatchMetadataRpcMode::Off)
}

fn batch_metadata_rpc_mode() -> BatchMetadataRpcMode {
    // The gate cannot change at runtime; parse it once instead of re-reading
    // the environment on every batch RPC.
    static MODE: std::sync::LazyLock<BatchMetadataRpcMode> = std::sync::LazyLock::new(batch_metadata_rpc_mode_from_env);
    *MODE
}

fn record_batch_read_version_gate_decision(mode: BatchMetadataRpcMode, decision: &'static str) {
    counter!(
        "rustfs_remote_disk_batch_read_version_gate_total",
        "mode" => mode.as_str(),
        "decision" => decision
    )
    .increment(1);
}

async fn copy_stream_with_buffer<R, W>(reader: &mut R, writer: &mut W, buffer_size: usize) -> io::Result<u64>
where
    R: AsyncRead + Unpin,
    W: AsyncWrite + Unpin,
{
    let mut copied = 0_u64;
    let mut buffer = vec![0_u8; buffer_size];

    loop {
        let bytes_read = reader.read(&mut buffer).await?;
        if bytes_read == 0 {
            writer.flush().await?;
            return Ok(copied);
        }

        writer.write_all(&buffer[..bytes_read]).await?;
        copied += bytes_read as u64;
    }
}

#[derive(Debug)]
pub struct RemoteDisk {
    pub id: Mutex<Option<Uuid>>,
    pub addr: String,
    endpoint: Endpoint,
    pub scanning: Arc<AtomicU32>,
    /// Whether health checking is enabled
    health_check: bool,
    /// Health tracker for connection monitoring
    health: Arc<DiskHealthTracker>,
    /// Cancellation token for monitoring tasks
    cancel_token: CancellationToken,
    data_transport: Arc<dyn InternodeDataTransport>,
}

// ── Connection lifecycle (grpc-optimization P3) ──

/// Whether to prewarm the internode control channel in the background at construction (default off).
fn internode_prewarm_enabled() -> bool {
    rustfs_utils::get_env_bool(rustfs_config::ENV_INTERNODE_PREWARM, rustfs_config::DEFAULT_INTERNODE_PREWARM)
}

/// Whether to fast-fail RPCs to peers already marked offline (default off).
fn internode_offline_bypass_enabled() -> bool {
    rustfs_utils::get_env_bool(
        rustfs_config::ENV_INTERNODE_OFFLINE_BYPASS,
        rustfs_config::DEFAULT_INTERNODE_OFFLINE_BYPASS,
    )
}

/// Re-probe interval for the offline bypass (>= 1s).
fn internode_offline_reprobe_interval() -> Duration {
    Duration::from_secs(
        rustfs_utils::get_env_u64(
            rustfs_config::ENV_INTERNODE_OFFLINE_REPROBE_SECS,
            rustfs_config::DEFAULT_INTERNODE_OFFLINE_REPROBE_SECS,
        )
        .max(1),
    )
}

/// If the offline bypass is enabled and `addr` is marked offline, return a reason string to
/// fast-fail with instead of paying the connect timeout (grpc-optimization P3-2). Self-healing:
/// one request per re-probe interval is let through so the peer can recover. Shared by the data
/// path (`remote_disk`) and the lock path (`remote_locker`).
pub(crate) fn internode_offline_bypass_reason(addr: &str) -> Option<String> {
    if internode_offline_bypass_enabled()
        && rustfs_io_metrics::internode_metrics::cluster_peer_should_bypass(addr, internode_offline_reprobe_interval())
    {
        return Some(format!("internode peer {addr} offline; fast-fail bypass (P3)"));
    }
    None
}

/// Number of extra attempts for idempotent read-only control-plane and object-read RPCs on
/// transient network failures (grpc-optimization P3-3). Defaults to `1` so a single reset-by-peer
/// during the read-after-write window does not erode read quorum (#2761). Write/lock RPCs never retry.
fn internode_idempotent_read_retries() -> usize {
    rustfs_utils::get_env_usize(
        rustfs_config::ENV_INTERNODE_IDEMPOTENT_READ_RETRIES,
        rustfs_config::DEFAULT_INTERNODE_IDEMPOTENT_READ_RETRIES,
    )
}

/// Peers for which a control-channel prewarm has already been triggered, to dedup the N remote
/// disks that map to a single peer address.
static PREWARMED_PEERS: std::sync::LazyLock<std::sync::Mutex<std::collections::HashSet<String>>> =
    std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new()));

/// Best-effort background prewarm of a peer's control channel (grpc-optimization P3-1). Deduped per
/// peer; a dial failure just falls through to the existing lazy connect and recovery monitor.
fn spawn_control_channel_prewarm(addr: String) {
    {
        let Ok(mut prewarmed) = PREWARMED_PEERS.lock() else {
            return;
        };
        if !prewarmed.insert(addr.clone()) {
            return;
        }
    }
    tokio::spawn(async move {
        match node_service_time_out_client_no_auth(&addr).await {
            Ok(_) => debug!(addr = %addr, "internode control channel prewarmed"),
            Err(err) => debug!(addr = %addr, error = %err, "internode control channel prewarm failed (best-effort)"),
        }
    });
}

impl RemoteDisk {
    pub(crate) async fn ns_scanner_server_epoch(&self) -> Result<Option<Uuid>> {
        if self.health.is_faulty() {
            return Err(DiskError::FaultyDisk);
        }
        let probe = self.data_transport.probe_ns_scanner(NsScannerCapabilityRequest {
            endpoint: self.endpoint.grid_host(),
        });
        let result = timeout(NS_SCANNER_CAPABILITY_PROBE_TIMEOUT, probe)
            .await
            .map_err(|_| DiskError::other("remote namespace scanner capability probe timed out"))?;
        match result {
            Ok(server_epoch) => Ok(Some(server_epoch)),
            // RUSTFS_COMPAT_TODO(ns-scanner-rpc-v3): old peers and legacy transports lack the authenticated startup-epoch handshake. Remove after every supported peer implements namespace scanner protocol v3.
            Err(DiskError::MethodNotAllowed) => Ok(None),
            Err(err)
                if matches!(
                    err.internode_http_error_kind(),
                    Some(rustfs_rio::InternodeHttpErrorKind::HttpStatus(status))
                        if matches!(status.as_u16(), 404 | 405)
                ) =>
            {
                Ok(None)
            }
            Err(err)
                if matches!(
                    err.internode_http_error_kind(),
                    Some(rustfs_rio::InternodeHttpErrorKind::HttpStatus(status)) if status.as_u16() == 426
                ) =>
            {
                Ok(None)
            }
            Err(err) => Err(err),
        }
    }

    pub(crate) async fn open_ns_scanner_stream(&self, request: crate::disk::NsScannerOpenRequest) -> Result<FileReader> {
        if self.health.is_faulty() {
            return Err(DiskError::FaultyDisk);
        }
        let crate::disk::NsScannerOpenRequest {
            request_id,
            server_epoch,
            session_id,
            session_sequence,
            next_cycle,
            leader_epoch,
            body,
            stall_timeout,
        } = request;
        self.data_transport
            .open_ns_scanner(NsScannerStreamRequest {
                endpoint: self.endpoint.grid_host(),
                disk: self.disk_ref().await,
                request_id,
                server_epoch,
                session_id,
                session_sequence,
                next_cycle,
                leader_epoch,
                body,
                stall_timeout,
            })
            .await
    }

    fn recovery_monitor_span(addr: &str, endpoint: &Endpoint) -> tracing::Span {
        tracing::info_span!(
            "recovery-monitor",
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            kind = "remote_disk",
            endpoint = %endpoint,
            addr = %addr
        )
    }

    fn is_retryable_walk_dir_error(err: &DiskError) -> bool {
        if is_network_like_disk_error(err) {
            return true;
        }

        let err_text = err.to_string().to_ascii_lowercase();
        err_text.contains("httpreader stream error") || err_text.contains("error decoding response body")
    }

    fn is_retryable_open_write_error(err: &DiskError) -> bool {
        err.is_retryable_internode_write_failure()
    }

    fn is_retryable_open_read_error(err: &DiskError) -> bool {
        // Opening a read stream is idempotent (no data has been consumed yet), so any transient
        // internode transport failure classified as retryable can be safely re-dialed. The
        // classifier is direction-agnostic — it inspects the InternodeHttpError kind — so it is
        // reused here from the write path.
        err.is_retryable_internode_write_failure()
    }

    pub(crate) async fn new(ep: &Endpoint, opt: &DiskOption, data_transport: Arc<dyn InternodeDataTransport>) -> Result<Self> {
        let addr = if let Some(port) = ep.url.port() {
            format!("{}://{}:{}", ep.url.scheme(), ep.url.host_str().expect("operation should succeed"), port)
        } else {
            format!("{}://{}", ep.url.scheme(), ep.url.host_str().expect("operation should succeed"))
        };

        let env_health_check =
            rustfs_utils::get_env_bool(ENV_RUSTFS_DRIVE_ACTIVE_MONITORING, DEFAULT_RUSTFS_DRIVE_ACTIVE_MONITORING);

        let disk = Self {
            id: Mutex::new(None),
            addr,
            endpoint: ep.clone(),
            scanning: Arc::new(AtomicU32::new(0)),
            health_check: opt.health_check && env_health_check,
            health: Arc::new(DiskHealthTracker::new()),
            cancel_token: CancellationToken::new(),
            data_transport,
        };
        record_drive_runtime_state(ep, RuntimeDriveHealthState::Online);

        // P3-1: move the connect cost off the first RPC by prewarming the control channel in the
        // background. Deduped per peer, best-effort, opt-in.
        if internode_prewarm_enabled() {
            spawn_control_channel_prewarm(disk.addr.clone());
        }

        Ok(disk)
    }

    pub fn runtime_state(&self) -> RuntimeDriveHealthState {
        self.health.runtime_state()
    }

    pub fn offline_duration_secs(&self) -> Option<u64> {
        self.health.offline_duration().map(|duration| duration.as_secs())
    }

    pub fn last_capacity_snapshot(&self) -> Option<(u64, u64, u64, u64)> {
        self.health.last_capacity_snapshot()
    }

    async fn open_write_with_retry(&self, request: WriteStreamRequest) -> Result<FileWriter> {
        let mut attempt = 1;
        let mut last_retry_classification = None;
        loop {
            match self.data_transport.open_write(request.clone()).await {
                Ok(writer) => {
                    if attempt > 1
                        && let Some(classification) = last_retry_classification
                    {
                        crate::cluster::rpc::runtime_sources::record_remote_disk_open_write_retry_success(classification);
                    }
                    return Ok(writer);
                }
                Err(err) if attempt < REMOTE_DISK_OPEN_WRITE_MAX_ATTEMPTS && Self::is_retryable_open_write_error(&err) => {
                    if let Some(classification) = err.internode_http_error_kind() {
                        let classification = classification.metric_label();
                        crate::cluster::rpc::runtime_sources::record_remote_disk_open_write_retry(classification);
                        last_retry_classification = Some(classification);
                    }
                    debug!(
                        endpoint = %request.endpoint,
                        volume = %request.volume,
                        path = %request.path,
                        append = request.append,
                        size = request.size,
                        attempt,
                        "retrying remote open_write after retryable transport error"
                    );
                    tokio::time::sleep(REMOTE_DISK_OPEN_WRITE_RETRY_BACKOFF).await;
                    attempt += 1;
                }
                Err(err) => return Err(err),
            }
        }
    }

    /// Open a remote read stream with a bounded retry on transient transport failures, mirroring
    /// [`Self::open_write_with_retry`]. A freshly-committed object exists on only `write_quorum`
    /// disks until background heal reconstructs the rest, so a single transient network error
    /// (BrokenPipe/ConnectionReset/reset-by-peer) on a shard read during that read-after-write
    /// window can drop the readable-shard count below `data_shards` and surface as a spurious
    /// `InsufficientReadQuorum`. Opening the stream is idempotent, so re-dialing once absorbs the
    /// transient failure instead of eroding quorum. See issue #2761.
    async fn open_read_with_retry(&self, request: ReadStreamRequest) -> Result<FileReader> {
        let mut attempt = 1;
        let mut last_retry_classification = None;
        loop {
            match self.data_transport.open_read(request.clone()).await {
                Ok(reader) => {
                    if attempt > 1
                        && let Some(classification) = last_retry_classification
                    {
                        crate::cluster::rpc::runtime_sources::record_remote_disk_open_read_retry_success(classification);
                    }
                    return Ok(reader);
                }
                Err(err) if attempt < REMOTE_DISK_OPEN_READ_MAX_ATTEMPTS && Self::is_retryable_open_read_error(&err) => {
                    if let Some(classification) = err.internode_http_error_kind() {
                        let classification = classification.metric_label();
                        crate::cluster::rpc::runtime_sources::record_remote_disk_open_read_retry(classification);
                        last_retry_classification = Some(classification);
                    }
                    debug!(
                        endpoint = %request.endpoint,
                        volume = %request.volume,
                        path = %request.path,
                        offset = request.offset,
                        length = request.length,
                        attempt,
                        "retrying remote open_read after retryable transport error"
                    );
                    tokio::time::sleep(REMOTE_DISK_OPEN_READ_RETRY_BACKOFF).await;
                    attempt += 1;
                }
                Err(err) => return Err(err),
            }
        }
    }

    pub fn record_capacity_probe(&self, total: u64, used: u64, free: u64) {
        self.health.record_capacity_probe(total, used, free);
    }

    #[cfg(test)]
    pub fn force_runtime_state_for_test(&self, state: RuntimeDriveHealthState) {
        self.health.force_runtime_state_for_test(state);
    }

    /// Same as [`DiskHealthTracker::reset_for_store_init_retry`]: undo a transient faulty mark before another format load attempt.
    pub fn reset_health_for_store_init_retry(&self) {
        self.health.reset_for_store_init_retry(&self.endpoint);
    }

    #[cfg(test)]
    pub fn health_check_enabled_for_test(&self) -> bool {
        self.health_check
    }

    fn spawn_recovery_monitor_if_needed(&self) {
        if !self.health_check {
            return;
        }

        let addr = self.addr.clone();
        let endpoint = self.endpoint.clone();
        let health = Arc::clone(&self.health);
        let cancel_token = self.cancel_token.clone();
        let span = Self::recovery_monitor_span(&addr, &endpoint);
        super::spawn_background_monitor(span, async move {
            Self::monitor_remote_disk_recovery(addr, endpoint, health, cancel_token).await;
        });
    }

    #[cfg(test)]
    fn spawn_recovery_monitor_log_probe_for_test(&self) -> tokio::sync::oneshot::Receiver<()> {
        let (tx, rx) = tokio::sync::oneshot::channel();
        let endpoint = self.endpoint.clone();
        let addr = self.addr.clone();
        let span = Self::recovery_monitor_span(&addr, &endpoint);
        super::spawn_background_monitor(span, async move {
            warn!(
                event = EVENT_REMOTE_DISK_HEALTH,
                component = LOG_COMPONENT_ECSTORE,
                subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                endpoint = %endpoint,
                addr,
                state = "probe",
                "remote disk recovery monitor log probe"
            );
            let _ = tx.send(());
        });
        rx
    }

    fn mark_suspect_or_offline(&self, reason: &'static str) -> bool {
        self.health.mark_failure(&self.endpoint, reason)
    }

    /// Enable health monitoring after disk creation.
    /// Used to defer health checks until after startup format loading completes,
    /// so that remote peers have time to come online.
    pub fn enable_health_check(&self) {
        if !self.health_check {
            return;
        }
        let health = Arc::clone(&self.health);
        let cancel_token = self.cancel_token.clone();
        let addr = self.addr.clone();
        let endpoint = self.endpoint.clone();

        tokio::spawn(async move {
            Self::monitor_remote_disk_health(addr, endpoint, health, cancel_token).await;
        });
    }

    /// Monitor remote disk health periodically
    async fn monitor_remote_disk_health(
        addr: String,
        endpoint: Endpoint,
        health: Arc<DiskHealthTracker>,
        cancel_token: CancellationToken,
    ) {
        let mut interval = time::interval(get_drive_active_check_interval());

        // Perform basic connectivity check
        let initial_probe_ok = Self::perform_connectivity_check(&addr).await.is_ok();
        if initial_probe_ok {
            health.record_operation_success(&endpoint, "connectivity_probe_success");
        } else if health.mark_failure(&endpoint, "connectivity_probe_failed") {
            warn!(
                event = EVENT_REMOTE_DISK_HEALTH,
                component = LOG_COMPONENT_ECSTORE,
                subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                endpoint = %endpoint,
                addr,
                state = "initial_probe_failed",
                result = "mark_faulty",
                "Remote disk initial health probe failed"
            );

            // Start recovery monitoring
            let health_clone = Arc::clone(&health);
            let addr_clone = addr.clone();
            let endpoint_clone = endpoint.clone();
            let cancel_clone = cancel_token.clone();
            let span = Self::recovery_monitor_span(&addr_clone, &endpoint_clone);

            super::spawn_background_monitor(span, async move {
                Self::monitor_remote_disk_recovery(addr_clone, endpoint_clone, health_clone, cancel_clone).await;
            });
        }

        loop {
            tokio::select! {
                _ = cancel_token.cancelled() => {
                    debug!(
                        event = EVENT_REMOTE_DISK_HEALTH,
                        component = LOG_COMPONENT_ECSTORE,
                        subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                        endpoint = %endpoint,
                        addr,
                        state = "monitor_cancelled",
                        "Remote disk health monitor cancelled"
                    );
                    return;
                }
                _ = interval.tick() => {
                    if cancel_token.is_cancelled() {
                        return;
                    }

                    // Skip health check if disk is already marked as faulty
                    if health.is_faulty() {
                        continue;
                    }

                    let last_success_nanos = health.last_success.load(Ordering::Relaxed);
                    let elapsed = Duration::from_nanos(
                        (std::time::SystemTime::now()
                            .duration_since(std::time::UNIX_EPOCH)
                            .expect("operation should succeed")
                            .as_nanos() as i64 - last_success_nanos) as u64
                    );

                    if elapsed < SKIP_IF_SUCCESS_BEFORE {
                        continue;
                    }

                    // Perform basic connectivity check
                    if Self::perform_connectivity_check(&addr).await.is_ok() {
                        health.record_operation_success(&endpoint, "connectivity_probe_success");
                    } else if health.mark_failure(&endpoint, "connectivity_probe_failed") {
                        warn!(
                            event = EVENT_REMOTE_DISK_HEALTH,
                            component = LOG_COMPONENT_ECSTORE,
                            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                            endpoint = %endpoint,
                            addr,
                            state = "probe_failed",
                            result = "mark_faulty",
                            "Remote disk health probe failed"
                        );

                        // Start recovery monitoring
                        let health_clone = Arc::clone(&health);
                        let addr_clone = addr.clone();
                        let endpoint_clone = endpoint.clone();
                        let cancel_clone = cancel_token.clone();
                        let span = Self::recovery_monitor_span(&addr_clone, &endpoint_clone);

                        super::spawn_background_monitor(span, async move {
                            Self::monitor_remote_disk_recovery(addr_clone, endpoint_clone, health_clone, cancel_clone).await;
                        });
                    }
                }
            }
        }
    }

    /// Monitor remote disk recovery and mark as healthy when recovered
    async fn monitor_remote_disk_recovery(
        addr: String,
        endpoint: Endpoint,
        health: Arc<DiskHealthTracker>,
        cancel_token: CancellationToken,
    ) {
        let mut interval = time::interval(get_drive_returning_probe_interval());
        debug!(
            event = EVENT_REMOTE_DISK_HEALTH,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %endpoint,
            addr,
            state = "recovery_monitor_started",
            "Remote disk recovery monitor started"
        );

        loop {
            tokio::select! {
                _ = cancel_token.cancelled() => {
                    debug!(
                        event = EVENT_REMOTE_DISK_HEALTH,
                        component = LOG_COMPONENT_ECSTORE,
                        subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                        endpoint = %endpoint,
                        addr,
                        state = "recovery_monitor_cancelled",
                        "Remote disk recovery monitor cancelled"
                    );
                    return;
                }
                _ = interval.tick() => {
                    if Self::perform_recovery_probe(&addr, &endpoint).await.is_ok() {
                        let became_online = health.mark_recovery_success(&endpoint, "disk_info_probe_success");
                        debug!(
                            event = EVENT_REMOTE_DISK_HEALTH,
                            component = LOG_COMPONENT_ECSTORE,
                            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                            endpoint = %endpoint,
                            addr,
                            state = "recovery_probe_succeeded",
                            "Remote disk recovery probe succeeded"
                        );
                        if became_online {
                            debug!(
                                event = EVENT_REMOTE_DISK_HEALTH,
                                component = LOG_COMPONENT_ECSTORE,
                                subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                                endpoint = %endpoint,
                                addr,
                                state = "recovered",
                                "Remote disk recovered"
                            );
                            return;
                        }
                    } else {
                        health.mark_failure(&endpoint, "disk_info_probe_failed");
                    }
                }
            }
        }
    }

    async fn perform_recovery_probe(addr: &str, endpoint: &Endpoint) -> Result<()> {
        let mut evict_cached_connection = false;
        let result = match timeout(get_drive_active_check_timeout(), async {
            let opts = serde_json::to_string(&DiskInfoOptions {
                noop: true,
                ..Default::default()
            })
            .map_err(|err| (Error::other(format!("encode DiskInfoOptions failed: {err}")), false))?;
            let addr = addr.to_string();
            let mut client = node_service_time_out_client(&addr, TonicInterceptor::Signature(gen_tonic_signature_interceptor()))
                .await
                .map_err(|err| (Error::other(format!("can not get client, err: {err}")), true))?;
            let request = Request::new(DiskInfoRequest {
                disk: endpoint.to_string(),
                opts,
            });
            let response = client
                .disk_info(request)
                .await
                .map_err(|err| (Error::from(err), true))?
                .into_inner();
            if !response.success {
                return Err((response.error.unwrap_or_default().into(), false));
            }
            Ok(())
        })
        .await
        {
            Ok(Ok(())) => Ok(()),
            Ok(Err((err, should_evict))) => {
                evict_cached_connection = should_evict;
                Err(err)
            }
            Err(_) => {
                evict_cached_connection = true;
                Err(DiskError::Timeout)
            }
        };

        if evict_cached_connection {
            evict_failed_connection(addr).await;
        }

        result
    }

    /// Perform basic connectivity check for remote disk
    async fn perform_connectivity_check(addr: &str) -> Result<()> {
        let url = url::Url::parse(addr).map_err(|e| Error::other(format!("Invalid URL: {e}")))?;

        let Some(host) = url.host_str() else {
            return Err(Error::other("No host in URL".to_string()));
        };

        let port = url.port_or_known_default().unwrap_or(80);

        // Try to establish TCP connection
        match timeout(get_drive_active_check_timeout(), TcpStream::connect((host, port))).await {
            Ok(Ok(stream)) => {
                drop(stream);
                Ok(())
            }
            _ => Err(Error::other(format!("Cannot connect to {host}:{port}"))),
        }
    }

    /// Execute operation with timeout and health tracking
    async fn execute_with_timeout<T, F, Fut>(&self, operation: F, timeout_duration: Duration) -> Result<T>
    where
        F: FnOnce() -> Fut,
        Fut: std::future::Future<Output = Result<T>>,
    {
        self.execute_with_timeout_for_op("unknown", operation, timeout_duration).await
    }

    /// Execute an **idempotent, read-only/reentrant** RPC with a bounded number of retries on
    /// transient network failures, with exponential backoff (grpc-optimization P3-3). Retries
    /// default to 1 (see [`internode_idempotent_read_retries`]). MUST NOT be used for write/lock
    /// RPCs — those must never auto-retry (quorum/idempotency safety). The `operation` closure is
    /// re-invoked per attempt, so it must be `Fn` (rebuild the request from borrowed inputs, do not
    /// move captured state out).
    async fn execute_read_with_retry<T, F, Fut>(&self, op: &'static str, operation: F, timeout_duration: Duration) -> Result<T>
    where
        F: Fn() -> Fut,
        Fut: std::future::Future<Output = Result<T>>,
    {
        let max_retries = internode_idempotent_read_retries();
        let mut attempt = 0usize;
        loop {
            // Only the final attempt marks the disk faulty / evicts the channel. Earlier retries
            // ignore the failure, so a transient error cannot flip the disk into a faulty
            // short-circuit (which would defeat the retry) or over-count failures.
            let health_action = if attempt >= max_retries {
                FailureHealthAction::MarkFailure
            } else {
                FailureHealthAction::IgnoreFailure
            };
            match self
                .execute_with_timeout_for_op_and_health_action(op, &operation, timeout_duration, health_action)
                .await
            {
                Err(err) if attempt < max_retries && is_network_like_disk_error(&err) => {
                    attempt += 1;
                    let backoff = REMOTE_DISK_READ_RETRY_BASE_BACKOFF
                        .saturating_mul(1u32 << u32::try_from(attempt - 1).unwrap_or(4).min(4));
                    debug!(
                        endpoint = %self.endpoint,
                        addr = %self.addr,
                        op,
                        attempt,
                        "retrying idempotent read-only RPC after transient network error"
                    );
                    tokio::time::sleep(backoff).await;
                }
                other => return other,
            }
        }
    }

    async fn execute_with_timeout_for_op<T, F, Fut>(
        &self,
        op: &'static str,
        operation: F,
        timeout_duration: Duration,
    ) -> Result<T>
    where
        F: FnOnce() -> Fut,
        Fut: std::future::Future<Output = Result<T>>,
    {
        self.execute_with_timeout_for_op_and_health_action(op, operation, timeout_duration, FailureHealthAction::MarkFailure)
            .await
    }

    async fn execute_with_timeout_for_op_and_health_action<T, F, Fut>(
        &self,
        op: &'static str,
        operation: F,
        timeout_duration: Duration,
        failure_health_action: FailureHealthAction,
    ) -> Result<T>
    where
        F: FnOnce() -> Fut,
        Fut: std::future::Future<Output = Result<T>>,
    {
        // Check if disk is faulty
        if self.health.is_faulty() {
            debug!(
                event = EVENT_REMOTE_DISK_HEALTH,
                component = LOG_COMPONENT_ECSTORE,
                subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                endpoint = %self.endpoint,
                addr = %self.addr,
                op,
                state = "faulty_short_circuit",
                "Remote disk operation short-circuited by faulty state"
            );
            return Err(DiskError::FaultyDisk);
        }

        // Record operation start
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("operation should succeed")
            .as_nanos() as i64;
        self.health.last_started.store(now, std::sync::atomic::Ordering::Relaxed);
        let _waiting_guard = self.health.waiting_guard();

        if timeout_duration == Duration::ZERO {
            let operation_result = operation().await;
            if operation_result.is_ok() {
                self.health.log_success();
            }
            self.handle_network_like_error(op, timeout_duration, &operation_result, failure_health_action)
                .await;
            return operation_result;
        }

        // Execute operation with timeout
        let result = time::timeout(timeout_duration, operation()).await;

        match result {
            Ok(operation_result) => {
                // Log success; the waiting guard balances every exit path.
                if operation_result.is_ok() {
                    self.health.log_success();
                }
                self.handle_network_like_error(op, timeout_duration, &operation_result, failure_health_action)
                    .await;
                operation_result
            }
            Err(_) => {
                // Timeout occurred, mark disk as potentially faulty
                counter!(
                    "rustfs_drive_op_timeout_total",
                    "endpoint" => self.endpoint.to_string(),
                    "op" => op.to_string()
                )
                .increment(1);
                if failure_health_action == FailureHealthAction::MarkFailure {
                    self.mark_faulty_and_evict("operation_timeout").await;
                }
                warn!(
                    event = EVENT_REMOTE_DISK_RPC,
                    component = LOG_COMPONENT_ECSTORE,
                    subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                    endpoint = %self.endpoint,
                    addr = %self.addr,
                    op,
                    timeout_ms = timeout_duration.as_millis(),
                    state = "timeout",
                    "Remote disk operation timed out"
                );
                Err(DiskError::Timeout)
            }
        }
    }

    async fn handle_network_like_error<T>(
        &self,
        op: &'static str,
        timeout_duration: Duration,
        operation_result: &Result<T>,
        failure_health_action: FailureHealthAction,
    ) {
        if let Err(err) = operation_result
            && is_network_like_disk_error(err)
        {
            counter!(
                "rustfs_drive_op_network_error_total",
                "endpoint" => self.endpoint.to_string(),
                "op" => op.to_string()
            )
            .increment(1);
            warn!(
                event = EVENT_REMOTE_DISK_RPC,
                component = LOG_COMPONENT_ECSTORE,
                subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                endpoint = %self.endpoint,
                addr = %self.addr,
                op,
                timeout_ms = timeout_duration.as_millis(),
                state = "network_like_error",
                "Remote disk operation returned network-like error"
            );
            if failure_health_action == FailureHealthAction::MarkFailure {
                self.mark_faulty_and_evict("operation_network_error").await;
            }
        }
    }

    async fn mark_faulty_and_evict(&self, reason: &'static str) {
        let previous_state = self.runtime_state();
        let transitioned_to_offline = self.mark_suspect_or_offline(reason);
        let state = self.runtime_state();

        if state != previous_state {
            self.spawn_recovery_monitor_if_needed();
            counter!(
                "rustfs_drive_faulty_mark_total",
                "endpoint" => self.endpoint.to_string(),
                "reason" => reason.to_string()
            )
            .increment(1);
            if transitioned_to_offline {
                warn!(
                    event = EVENT_REMOTE_DISK_HEALTH,
                    component = LOG_COMPONENT_ECSTORE,
                    subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                    endpoint = %self.endpoint,
                    addr = %self.addr,
                    reason,
                    state = "marked_faulty",
                    "Remote disk marked faulty"
                );
            } else {
                warn!(
                    event = EVENT_REMOTE_DISK_HEALTH,
                    component = LOG_COMPONENT_ECSTORE,
                    subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                    endpoint = %self.endpoint,
                    addr = %self.addr,
                    reason,
                    runtime_state = ?state,
                    state = "marked_suspect",
                    "Remote disk marked suspect"
                );
            }
            counter!(
                "rustfs_drive_connection_evict_total",
                "endpoint" => self.endpoint.to_string(),
                "reason" => reason.to_string()
            )
            .increment(1);
            debug!(
                event = EVENT_REMOTE_DISK_HEALTH,
                component = LOG_COMPONENT_ECSTORE,
                subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                endpoint = %self.endpoint,
                addr = %self.addr,
                reason,
                state = "evict_cached_connection",
                "Remote disk cached connection evicted"
            );
            evict_failed_connection(&self.addr).await;
        }
    }

    /// P3-2 offline bypass: when enabled and this peer is marked offline, fast-fail instead of
    /// paying the connect timeout, so the erasure layer proceeds on quorum sooner. This does not
    /// change quorum. Self-healing — one request per re-probe interval is let through so the peer
    /// recovers even without a background monitor. The recovery monitor's own probe path calls the
    /// client directly and is unaffected.
    fn offline_bypass_error(&self) -> Option<Error> {
        internode_offline_bypass_reason(&self.addr).map(Error::other)
    }

    async fn get_client(&self) -> Result<NodeServiceClient<InterceptedService<AuthenticatedChannel, TonicInterceptor>>> {
        if let Some(err) = self.offline_bypass_error() {
            return Err(err);
        }
        node_service_time_out_client(&self.addr, TonicInterceptor::Signature(gen_tonic_signature_interceptor()))
            .await
            .map_err(|err| Error::other(format!("can not get client, err: {err}")))
    }

    /// Client for large `bytes`-carrying RPCs (ReadAll/WriteAll/ReadMultiple/BatchReadVersion).
    /// Routes onto the isolated bulk channel pool so large transfers cannot head-of-line block
    /// lock/health RPCs (grpc-optimization P1). Falls back to the control channel when isolation
    /// is disabled.
    async fn get_bulk_client(&self) -> Result<NodeServiceClient<InterceptedService<AuthenticatedChannel, TonicInterceptor>>> {
        if let Some(err) = self.offline_bypass_error() {
            return Err(err);
        }
        node_service_time_out_client_for_class(
            &self.addr,
            TonicInterceptor::Signature(gen_tonic_signature_interceptor()),
            ChannelClass::Bulk,
        )
        .await
        .map_err(|err| Error::other(format!("can not get client, err: {err}")))
    }

    async fn disk_ref(&self) -> String {
        (*self.id.lock().await)
            .map(|id| id.to_string())
            .unwrap_or_else(|| self.endpoint.to_string())
    }
}

/// 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 encode_msgpack<T: Serialize>(value: &T) -> Result<Vec<u8>> {
    let mut serializer = rmp_serde::Serializer::new(Vec::with_capacity(MSGPACK_ENCODE_CAPACITY_HINT));
    value.serialize(&mut serializer)?;
    Ok(serializer.into_inner())
}

/// JSON compatibility string for a dual-encoded (`_bin` + text) request 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.
fn compat_json<T: Serialize>(value: &T) -> Result<String> {
    if rustfs_protos::internode_rpc_msgpack_only() {
        return Ok(String::new());
    }
    Ok(serde_json::to_string(value)?)
}

fn encode_msgpack_named<T: Serialize>(value: &T) -> Result<Vec<u8>> {
    let mut serializer = rmp_serde::Serializer::new(Vec::with_capacity(MSGPACK_ENCODE_CAPACITY_HINT)).with_struct_map();
    value.serialize(&mut serializer)?;
    Ok(serializer.into_inner())
}

fn decode_msgpack_or_json<T: DeserializeOwned>(binary: &[u8], json: &str, value_name: &'static str) -> Result<T> {
    if !binary.is_empty() {
        let mut deserializer = rmp_serde::Deserializer::new(Cursor::new(binary));
        return match T::deserialize(&mut deserializer) {
            Ok(value) => {
                crate::cluster::rpc::runtime_sources::record_response_msgpack_decode(value_name);
                Ok(value)
            }
            Err(err) => {
                crate::cluster::rpc::runtime_sources::record_response_msgpack_decode_error(value_name);
                Err(Error::from(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).
    crate::cluster::rpc::runtime_sources::record_response_json_fallback(value_name);
    match serde_json::from_str(json) {
        Ok(value) => {
            crate::cluster::rpc::runtime_sources::record_response_json_decode(value_name);
            Ok(value)
        }
        Err(err) => {
            crate::cluster::rpc::runtime_sources::record_response_json_decode_error(value_name);
            Err(Error::from(err))
        }
    }
}

/// Aggregate encoded size (bytes) of a `ReadMultiple` response, preferring the msgpack payloads
/// and falling back to the JSON compatibility strings. Used to size the RPC for the payload
/// histogram / large-payload alerting (grpc-optimization P0 instrumentation).
fn read_multiple_response_payload_len(response: &ReadMultipleResponse) -> usize {
    if !response.read_multiple_resps_bin.is_empty() {
        response.read_multiple_resps_bin.iter().map(|buf| buf.len()).sum()
    } else {
        response.read_multiple_resps.iter().map(|item| item.len()).sum()
    }
}

fn decode_read_multiple_response_items(response: ReadMultipleResponse, endpoint: &Endpoint) -> Result<Vec<ReadMultipleResp>> {
    if !response.read_multiple_resps_bin.is_empty() {
        if !response.read_multiple_resps.is_empty()
            && response.read_multiple_resps.len() != response.read_multiple_resps_bin.len()
        {
            warn!(
                event = EVENT_REMOTE_DISK_RPC,
                component = LOG_COMPONENT_ECSTORE,
                subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                endpoint = %endpoint,
                json_count = response.read_multiple_resps.len(),
                msgpack_count = response.read_multiple_resps_bin.len(),
                op = "read_multiple",
                state = "response_count_mismatch",
                "Remote disk ReadMultiple compatibility payload counts differ"
            );
        }

        let mut read_multiple_resps = Vec::with_capacity(response.read_multiple_resps_bin.len());
        for (index, buf) in response.read_multiple_resps_bin.iter().enumerate() {
            let resp = decode_msgpack_or_json::<ReadMultipleResp>(buf, "", "ReadMultipleResp").map_err(|err| {
                Error::other(format!("decode ReadMultipleResp msgpack item {index} from {endpoint} failed: {err}"))
            })?;
            read_multiple_resps.push(resp);
        }
        return Ok(read_multiple_resps);
    }

    // No msgpack payloads present: the whole list fell back to the JSON compatibility field (P2).
    if !response.read_multiple_resps.is_empty() {
        crate::cluster::rpc::runtime_sources::record_response_json_fallback("ReadMultipleResp");
    }
    let mut read_multiple_resps = Vec::with_capacity(response.read_multiple_resps.len());
    for (index, json_str) in response.read_multiple_resps.iter().enumerate() {
        let resp = serde_json::from_str::<ReadMultipleResp>(json_str).map_err(|err| {
            crate::cluster::rpc::runtime_sources::record_response_json_decode_error("ReadMultipleResp");
            Error::other(format!("decode ReadMultipleResp json item {index} from {endpoint} failed: {err}"))
        })?;
        crate::cluster::rpc::runtime_sources::record_response_json_decode("ReadMultipleResp");
        read_multiple_resps.push(resp);
    }

    Ok(read_multiple_resps)
}

fn decode_batch_read_version_response_items(
    response: BatchReadVersionResponse,
    endpoint: &Endpoint,
) -> Result<Vec<BatchReadVersionResp>> {
    if !response.batch_read_version_resps_bin.is_empty() {
        if !response.batch_read_version_resps.is_empty()
            && response.batch_read_version_resps.len() != response.batch_read_version_resps_bin.len()
        {
            warn!(
                event = EVENT_REMOTE_DISK_RPC,
                component = LOG_COMPONENT_ECSTORE,
                subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                endpoint = %endpoint,
                json_count = response.batch_read_version_resps.len(),
                msgpack_count = response.batch_read_version_resps_bin.len(),
                op = "batch_read_version",
                state = "response_count_mismatch",
                "Remote disk BatchReadVersion compatibility payload counts differ"
            );
        }

        let mut batch_read_version_resps = Vec::with_capacity(response.batch_read_version_resps_bin.len());
        for (index, buf) in response.batch_read_version_resps_bin.iter().enumerate() {
            let resp = decode_msgpack_or_json::<BatchReadVersionResp>(buf, "", "BatchReadVersionResp").map_err(|err| {
                Error::other(format!("decode BatchReadVersionResp msgpack item {index} from {endpoint} failed: {err}"))
            })?;
            if resp.success {
                validate_decoded_file_info(&resp.file_info)?;
            }
            batch_read_version_resps.push(resp);
        }
        return Ok(batch_read_version_resps);
    }

    // No msgpack payloads present: the whole list fell back to the JSON compatibility field (P2).
    if !response.batch_read_version_resps.is_empty() {
        crate::cluster::rpc::runtime_sources::record_response_json_fallback("BatchReadVersionResp");
    }
    let mut batch_read_version_resps = Vec::with_capacity(response.batch_read_version_resps.len());
    for (index, json_str) in response.batch_read_version_resps.iter().enumerate() {
        let resp = serde_json::from_str::<BatchReadVersionResp>(json_str).map_err(|err| {
            crate::cluster::rpc::runtime_sources::record_response_json_decode_error("BatchReadVersionResp");
            Error::other(format!("decode BatchReadVersionResp json item {index} from {endpoint} failed: {err}"))
        })?;
        crate::cluster::rpc::runtime_sources::record_response_json_decode("BatchReadVersionResp");
        if resp.success {
            validate_decoded_file_info(&resp.file_info)?;
        }
        batch_read_version_resps.push(resp);
    }

    Ok(batch_read_version_resps)
}

fn validate_decoded_file_info(file_info: &FileInfo) -> Result<()> {
    file_info.validate_for_metadata_read().map_err(Into::into)
}

#[async_trait::async_trait]
impl DiskAPI for RemoteDisk {
    #[tracing::instrument(level = "trace", skip_all)]
    fn to_string(&self) -> String {
        self.endpoint.to_string()
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn is_online(&self) -> bool {
        // If disk is marked as faulty, consider it offline
        !self.health.is_faulty()
    }

    #[tracing::instrument(level = "trace", skip_all)]
    fn is_local(&self) -> bool {
        false
    }
    #[tracing::instrument(level = "trace", skip_all)]
    fn host_name(&self) -> String {
        self.endpoint.host_port()
    }
    #[tracing::instrument(level = "trace", skip_all)]
    fn endpoint(&self) -> Endpoint {
        self.endpoint.clone()
    }
    #[tracing::instrument(level = "trace", skip_all)]
    async fn close(&self) -> Result<()> {
        self.cancel_token.cancel();
        Ok(())
    }
    #[tracing::instrument(level = "trace", skip_all)]
    async fn get_disk_id(&self) -> Result<Option<Uuid>> {
        Ok(*self.id.lock().await)
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn set_disk_id(&self, id: Option<Uuid>) -> Result<()> {
        let mut lock = self.id.lock().await;
        *lock = id;

        Ok(())
    }

    #[tracing::instrument(level = "trace", skip_all)]
    fn path(&self) -> PathBuf {
        PathBuf::from(self.endpoint.get_file_path())
    }

    #[tracing::instrument(level = "trace", skip_all)]
    fn get_disk_location(&self) -> DiskLocation {
        DiskLocation {
            pool_idx: {
                if self.endpoint.pool_idx < 0 {
                    None
                } else {
                    Some(self.endpoint.pool_idx as usize)
                }
            },
            set_idx: {
                if self.endpoint.set_idx < 0 {
                    None
                } else {
                    Some(self.endpoint.set_idx as usize)
                }
            },
            disk_idx: {
                if self.endpoint.disk_idx < 0 {
                    None
                } else {
                    Some(self.endpoint.disk_idx as usize)
                }
            },
        }
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn make_volume(&self, volume: &str) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            op = "make_volume",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(MakeVolumeRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                });
                let canonical_body = rustfs_protos::canonical_make_volume_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "make_volume")?;

                let response = client.make_volume(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn make_volumes(&self, volumes: Vec<&str>) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume_count = volumes.len(),
            op = "make_volumes",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(MakeVolumesRequest {
                    disk: self.endpoint.to_string(),
                    volumes: volumes.iter().map(|s| (*s).to_string()).collect(),
                });
                let canonical_body = rustfs_protos::canonical_make_volumes_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "make_volumes")?;

                let response = client.make_volumes(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn list_volumes(&self) -> Result<Vec<VolumeInfo>> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            op = "list_volumes",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(ListVolumesRequest {
                    disk: self.endpoint.to_string(),
                });

                let response = client.list_volumes(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                let infos = decode_volume_infos(response.volume_infos)?;

                Ok(infos)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn stat_volume(&self, volume: &str) -> Result<VolumeInfo> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            op = "stat_volume",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(StatVolumeRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                });

                let response = client.stat_volume(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                let volume_info = serde_json::from_str::<VolumeInfo>(&response.volume_info)?;

                Ok(volume_info)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn delete_volume(&self, volume: &str, force_delete: bool) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            op = "delete_volume",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(DeleteVolumeRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    force: force_delete,
                });
                let canonical_body = rustfs_protos::canonical_delete_volume_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "delete_volume")?;

                let response = client.delete_volume(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn delete_version(
        &self,
        volume: &str,
        path: &str,
        fi: FileInfo,
        force_del_marker: bool,
        opts: DeleteOptions,
    ) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            op = "delete_version",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                // `_bin` support for DeleteVersion is new (grpc-optimization P2); always dual-write
                // JSON + msgpack until its fallback counter has read zero across a release window.
                let file_info_bin = encode_msgpack(&fi)?;
                let opts_bin = encode_msgpack(&opts)?;
                let file_info = serde_json::to_string(&fi)?;
                let opts = serde_json::to_string(&opts)?;

                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(DeleteVersionRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    path: path.to_string(),
                    file_info,
                    force_del_marker,
                    opts,
                    file_info_bin: file_info_bin.into(),
                    opts_bin: opts_bin.into(),
                });
                let canonical_body = rustfs_protos::canonical_delete_version_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "delete_version")?;

                let response = client.delete_version(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                // let raw_file_info = serde_json::from_str::<RawFileInfo>(&response.raw_file_info)?;

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn delete_versions(&self, volume: &str, versions: Vec<FileInfoVersions>, opts: DeleteOptions) -> Vec<Option<Error>> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            version_count = versions.len(),
            op = "delete_versions",
            state = "started",
            "Remote disk RPC started"
        );

        if self.health.is_faulty() {
            return vec![Some(DiskError::FaultyDisk); versions.len()];
        }

        // `_bin` support for DeleteVersions is new (grpc-optimization P2); always dual-write JSON +
        // msgpack until its fallback counter has read zero across a release window.
        let opts_bin = match encode_msgpack(&opts) {
            Ok(opts_bin) => opts_bin,
            Err(err) => {
                let mut errors = Vec::with_capacity(versions.len());
                for _ in 0..versions.len() {
                    errors.push(Some(Error::other(err.to_string())));
                }
                return errors;
            }
        };
        let opts = match serde_json::to_string(&opts) {
            Ok(opts) => opts,
            Err(err) => {
                let mut errors = Vec::with_capacity(versions.len());
                for _ in 0..versions.len() {
                    errors.push(Some(Error::other(err.to_string())));
                }
                return errors;
            }
        };
        let mut versions_str = Vec::with_capacity(versions.len());
        let mut versions_bin = Vec::with_capacity(versions.len());
        for file_info_versions in versions.iter() {
            versions_str.push(match serde_json::to_string(file_info_versions) {
                Ok(versions_str) => versions_str,
                Err(err) => {
                    let mut errors = Vec::with_capacity(versions.len());
                    for _ in 0..versions.len() {
                        errors.push(Some(Error::other(err.to_string())));
                    }
                    return errors;
                }
            });
            versions_bin.push(match encode_msgpack(file_info_versions) {
                Ok(versions_bin) => Bytes::from(versions_bin),
                Err(err) => {
                    let mut errors = Vec::with_capacity(versions.len());
                    for _ in 0..versions.len() {
                        errors.push(Some(Error::other(err.to_string())));
                    }
                    return errors;
                }
            });
        }
        let mut client = match self.get_client().await {
            Ok(client) => client,
            Err(err) => {
                let mut errors = Vec::with_capacity(versions.len());
                for _ in 0..versions.len() {
                    errors.push(Some(Error::other(err.to_string())));
                }
                return errors;
            }
        };

        let mut request = Request::new(DeleteVersionsRequest {
            disk: self.endpoint.to_string(),
            volume: volume.to_string(),
            versions: versions_str,
            opts,
            versions_bin,
            opts_bin: opts_bin.into(),
        });
        let canonical_body = rustfs_protos::canonical_delete_versions_request_body(request.get_ref());
        if let Err(err) = attach_mutation_body_digest(&mut request, canonical_body, "delete_versions") {
            let mut errors = Vec::with_capacity(versions.len());
            for _ in 0..versions.len() {
                errors.push(Some(err.clone()));
            }
            return errors;
        }

        // TODO: use Error not string

        let result = self
            .execute_with_timeout(
                || async {
                    client
                        .delete_versions(request)
                        .await
                        .map_err(|err| Error::other(format!("delete_versions failed: {err}")))
                },
                get_max_timeout_duration(),
            )
            .await;

        let response = match result {
            Ok(response) => response,
            Err(err) => {
                let mut errors = Vec::with_capacity(versions.len());
                for _ in 0..versions.len() {
                    errors.push(Some(err.clone()));
                }
                return errors;
            }
        };

        let response = response.into_inner();
        if !response.success {
            let mut errors = Vec::with_capacity(versions.len());
            for _ in 0..versions.len() {
                errors.push(Some(Error::other(response.error.clone().map(|e| e.error_info).unwrap_or_default())));
            }
            return errors;
        }
        response
            .errors
            .iter()
            .map(|error| {
                if error.is_empty() {
                    None
                } else {
                    Some(Error::other(error.to_string()))
                }
            })
            .collect()
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn delete_paths(&self, volume: &str, paths: &[String]) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path_count = paths.len(),
            op = "delete_paths",
            state = "started",
            "Remote disk RPC started"
        );
        let paths = paths.to_owned();

        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(DeletePathsRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    paths: paths.clone(),
                });
                let canonical_body = rustfs_protos::canonical_delete_paths_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "delete_paths")?;

                let response = client.delete_paths(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    async fn acquire_snapshot_lease(&self, volume: &str, path: &str) -> Result<SnapshotLeaseToken> {
        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(SnapshotLeaseRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    path: path.to_string(),
                    ttl_ms: u64::try_from(REMOTE_SNAPSHOT_LEASE_TTL.as_millis())
                        .map_err(|_| Error::other("snapshot lease TTL cannot be represented"))?,
                });
                let canonical_body = rustfs_protos::canonical_snapshot_lease_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "acquire_snapshot_lease")?;
                let response = client.acquire_snapshot_lease(request).await?.into_inner();
                snapshot_lease_token_from_response(response)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    async fn renew_snapshot_lease(&self, volume: &str, path: &str, token: SnapshotLeaseToken) -> Result<SnapshotLeaseToken> {
        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(SnapshotLeaseRenewRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    path: path.to_string(),
                    token: token.as_bytes().to_vec().into(),
                    ttl_ms: u64::try_from(REMOTE_SNAPSHOT_LEASE_TTL.as_millis())
                        .map_err(|_| Error::other("snapshot lease TTL cannot be represented"))?,
                });
                let canonical_body = rustfs_protos::canonical_snapshot_lease_renew_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "renew_snapshot_lease")?;
                let response = client.renew_snapshot_lease(request).await?.into_inner();
                snapshot_lease_token_from_response(response)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    async fn release_snapshot_lease(&self, volume: &str, path: &str, token: SnapshotLeaseToken) -> Result<()> {
        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(SnapshotLeaseReleaseRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    path: path.to_string(),
                    token: token.as_bytes().to_vec().into(),
                });
                let canonical_body = rustfs_protos::canonical_snapshot_lease_release_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "release_snapshot_lease")?;
                let response = client.release_snapshot_lease(request).await?.into_inner();
                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }
                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn write_metadata(&self, _org_volume: &str, volume: &str, path: &str, fi: FileInfo) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            op = "write_metadata",
            state = "started",
            "Remote disk RPC started"
        );
        let file_info = compat_json(&fi)?;
        let file_info_bin = encode_msgpack(&fi)?;

        self.execute_with_timeout_for_op(
            "write_metadata",
            move || async move {
                let disk = self.disk_ref().await;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(WriteMetadataRequest {
                    disk,
                    volume: volume.to_string(),
                    path: path.to_string(),
                    file_info,
                    file_info_bin: file_info_bin.into(),
                });
                let canonical_body = rustfs_protos::canonical_write_metadata_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "write_metadata")?;

                let response = client.write_metadata(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    async fn read_metadata(&self, volume: &str, path: &str) -> Result<Bytes> {
        // Idempotent metadata read: eligible for the bounded transient-network retry so a single
        // reset-by-peer during the read-after-write window does not erode the metadata read
        // quorum (see issue #2761).
        self.execute_read_with_retry(
            "read_metadata",
            || async {
                let disk = self.disk_ref().await;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(ReadMetadataRequest {
                    volume: volume.to_string(),
                    path: path.to_string(),
                    disk,
                });

                let response = client.read_metadata(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(response.data)
            },
            get_drive_metadata_timeout(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn update_metadata(&self, volume: &str, path: &str, fi: FileInfo, opts: &UpdateMetadataOpts) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            op = "update_metadata",
            state = "started",
            "Remote disk RPC started"
        );
        let file_info = compat_json(&fi)?;
        let opts_str = compat_json(&opts)?;
        let file_info_bin = encode_msgpack(&fi)?;
        let opts_bin = encode_msgpack(opts)?;

        self.execute_with_timeout_for_op(
            "update_metadata",
            move || async move {
                let disk = self.disk_ref().await;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(UpdateMetadataRequest {
                    disk,
                    volume: volume.to_string(),
                    path: path.to_string(),
                    file_info,
                    opts: opts_str,
                    file_info_bin: file_info_bin.into(),
                    opts_bin: opts_bin.into(),
                });
                let canonical_body = rustfs_protos::canonical_update_metadata_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "update_metadata")?;

                let response = client.update_metadata(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn read_version(
        &self,
        _org_volume: &str,
        volume: &str,
        path: &str,
        version_id: &str,
        opts: &ReadOptions,
    ) -> Result<FileInfo> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            version_id,
            op = "read_version",
            state = "started",
            "Remote disk RPC started"
        );
        let opts_str = compat_json(opts)?;
        let opts_bin = encode_msgpack(opts)?;

        // Idempotent version read: eligible for the bounded transient-network retry so a single
        // reset-by-peer during the read-after-write window does not erode the metadata read
        // quorum (see issue #2761). The request payload is rebuilt (cloned) per attempt so the
        // operation closure stays re-invocable (`Fn`).
        self.execute_read_with_retry(
            "read_version",
            || async {
                let opts_str = opts_str.clone();
                let opts_bin = opts_bin.clone();
                let disk = self.disk_ref().await;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(ReadVersionRequest {
                    disk,
                    volume: volume.to_string(),
                    path: path.to_string(),
                    version_id: version_id.to_string(),
                    opts: opts_str,
                    opts_bin: opts_bin.into(),
                });

                let response = client.read_version(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                let file_info = decode_msgpack_or_json::<FileInfo>(&response.file_info_bin, &response.file_info, "FileInfo")?;
                validate_decoded_file_info(&file_info)?;

                Ok(file_info)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn batch_read_version(&self, req: BatchReadVersionReq) -> Result<Vec<BatchReadVersionResp>> {
        validate_batch_read_version_item_count(req.items.len())?;

        let mode = batch_metadata_rpc_mode();
        if !mode.should_attempt() {
            record_batch_read_version_gate_decision(mode, BATCH_READ_VERSION_GATE_OFF_UNARY);
            return batch_read_version_one_by_one(self, req).await;
        }
        record_batch_read_version_gate_decision(mode, BATCH_READ_VERSION_GATE_ATTEMPT);

        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            item_count = req.items.len(),
            batch_metadata_rpc_mode = mode.as_str(),
            op = "batch_read_version",
            state = "started",
            "Remote disk RPC started"
        );
        let batch_read_version_req = compat_json(&req)?;
        let batch_read_version_req_bin = encode_msgpack(&req)?;

        let batch_result = self
            .execute_with_timeout_for_op(
                "batch_read_version",
                move || async move {
                    let disk = self.disk_ref().await;
                    let mut client = self
                        .get_bulk_client()
                        .await
                        .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                    let request = Request::new(BatchReadVersionRequest {
                        disk,
                        batch_read_version_req,
                        batch_read_version_req_bin: batch_read_version_req_bin.into(),
                    });

                    let response = match client.batch_read_version(request).await {
                        Ok(response) => response.into_inner(),
                        Err(status) if status.code() == Code::Unimplemented => {
                            if mode.should_fallback_on_unimplemented() {
                                record_batch_read_version_gate_decision(mode, BATCH_READ_VERSION_GATE_FALLBACK_UNIMPLEMENTED);
                                warn!(
                                    event = EVENT_REMOTE_DISK_RPC,
                                    component = LOG_COMPONENT_ECSTORE,
                                    subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                                    endpoint = %self.endpoint,
                                    batch_metadata_rpc_mode = mode.as_str(),
                                    op = "batch_read_version",
                                    state = "fallback_unimplemented",
                                    "Remote disk BatchReadVersion unsupported; falling back to unary read_version"
                                );
                                return Ok(None);
                            }

                            record_batch_read_version_gate_decision(mode, BATCH_READ_VERSION_GATE_UNSUPPORTED_NO_FALLBACK);
                            warn!(
                                event = EVENT_REMOTE_DISK_RPC,
                                component = LOG_COMPONENT_ECSTORE,
                                subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
                                endpoint = %self.endpoint,
                                batch_metadata_rpc_mode = mode.as_str(),
                                op = "batch_read_version",
                                state = "unsupported_no_fallback",
                                "Remote disk BatchReadVersion unsupported and explicit batch RPC mode forbids fallback"
                            );
                            return Err(Error::from(status));
                        }
                        Err(status) => return Err(Error::from(status)),
                    };

                    if !response.success {
                        return Err(response.error.unwrap_or_default().into());
                    }

                    decode_batch_read_version_response_items(response, &self.endpoint).map(Some)
                },
                get_max_timeout_duration(),
            )
            .await?;

        match batch_result {
            Some(batch_read_version_resps) => Ok(batch_read_version_resps),
            // Run the unary fallback outside the batch RPC deadline so each
            // read_version keeps its own per-op timeout and health accounting
            // instead of racing the whole batch against one drive timeout.
            None => batch_read_version_one_by_one(self, req).await,
        }
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn read_xl(&self, volume: &str, path: &str, read_data: bool) -> Result<RawFileInfo> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            read_data,
            op = "read_xl",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let disk = self.disk_ref().await;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(ReadXlRequest {
                    disk,
                    volume: volume.to_string(),
                    path: path.to_string(),
                    read_data,
                });

                let response = client.read_xl(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                let raw_file_info =
                    decode_msgpack_or_json::<RawFileInfo>(&response.raw_file_info_bin, &response.raw_file_info, "RawFileInfo")?;

                Ok(raw_file_info)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn rename_data(
        &self,
        src_volume: &str,
        src_path: &str,
        fi: FileInfo,
        dst_volume: &str,
        dst_path: &str,
    ) -> Result<RenameDataResp> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            src_volume,
            src_path,
            dst_volume,
            dst_path,
            op = "rename_data",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout_for_op(
            "rename_data",
            || async {
                let file_info = compat_json(&fi)?;
                let file_info_bin = encode_msgpack_named(&fi)?;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(RenameDataRequest {
                    disk: self.endpoint.to_string(),
                    src_volume: src_volume.to_string(),
                    src_path: src_path.to_string(),
                    file_info,
                    dst_volume: dst_volume.to_string(),
                    dst_path: dst_path.to_string(),
                    file_info_bin: file_info_bin.into(),
                });
                let canonical_body = rustfs_protos::canonical_rename_data_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "rename_data")?;

                let response = client.rename_data(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                let rename_data_resp = decode_msgpack_or_json::<RenameDataResp>(
                    &response.rename_data_resp_bin,
                    &response.rename_data_resp,
                    "RenameDataResp",
                )?;

                Ok(rename_data_resp)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn list_dir(&self, _origvolume: &str, volume: &str, dir_path: &str, count: i32) -> Result<Vec<String>> {
        trace!(volume, dir_path, "Remote disk list_dir RPC started");

        self.execute_with_timeout(
            || async {
                let disk = self.disk_ref().await;

                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(ListDirRequest {
                    disk,
                    volume: volume.to_string(),
                    dir_path: dir_path.to_string(),
                    count,
                });

                let response = client.list_dir(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(response.volumes)
            },
            get_drive_list_dir_timeout(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn walk_dir<W: AsyncWrite + Unpin + Send>(&self, opts: WalkDirOptions, wr: &mut W) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            bucket = %opts.bucket,
            base_dir = %opts.base_dir,
            op = "walk_dir",
            state = "started",
            "Remote disk RPC started"
        );

        let disk = self.disk_ref().await;
        let body = serde_json::to_vec(&opts)?;
        let stall_timeout = opts.stall_timeout_duration().unwrap_or_else(get_drive_walkdir_stall_timeout);
        let bucket = opts.bucket.clone();
        let base_dir = opts.base_dir.clone();
        let disk_for_log = disk.clone();
        let timeout_duration = if opts.skip_total_timeout {
            Duration::ZERO
        } else {
            opts.timeout_duration().unwrap_or_else(get_drive_walkdir_timeout)
        };

        self.execute_with_timeout_for_op_and_health_action(
            "walk_dir",
            || async {
                let mut last_err = None;

                for attempt in 1..=2 {
                    let mut reader = match self
                        .data_transport
                        .open_walk_dir(WalkDirStreamRequest {
                            endpoint: self.endpoint.grid_host(),
                            disk: disk.clone(),
                            body: body.clone(),
                            stall_timeout: Some(stall_timeout),
                        })
                        .await
                    {
                        Ok(reader) => reader,
                        Err(err) => {
                            if attempt == 1 && Self::is_retryable_walk_dir_error(&err) {
                                warn!(
                                    endpoint = %self.endpoint,
                                    addr = %self.addr,
                                    disk = %disk_for_log,
                                    bucket = %bucket,
                                    base_dir = %base_dir,
                                    attempt,
                                    stall_timeout_ms = stall_timeout.as_millis(),
                                    error = %err,
                                    "remote walk_dir returned retryable transport error; retrying"
                                );
                                last_err = Some(err);
                                continue;
                            }

                            return Err(err);
                        }
                    };

                    match copy_stream_with_buffer(&mut reader, wr, DEFAULT_READ_BUFFER_SIZE).await {
                        Ok(_) => return Ok(()),
                        Err(io_err) => return Err(DiskError::Io(io_err)),
                    }
                }

                Err(last_err.unwrap_or_else(|| DiskError::other("walk_dir retry exhausted without captured error")))
            },
            timeout_duration,
            FailureHealthAction::IgnoreFailure,
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn read_file(&self, volume: &str, path: &str) -> Result<FileReader> {
        self.read_file_stream(volume, path, 0, 0).await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn read_file_stream(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<FileReader> {
        // warn!(
        //     "disk remote read_file_stream {}/{}/{} offset={} length={}",
        //     self.endpoint.to_string(),
        //     volume,
        //     path,
        //     offset,
        //     length
        // );

        if self.health.is_faulty() {
            return Err(DiskError::FaultyDisk);
        }
        let disk = self.disk_ref().await;
        let stall_timeout = get_object_disk_read_timeout();
        self.open_read_with_retry(ReadStreamRequest {
            endpoint: self.endpoint.grid_host(),
            disk,
            volume: volume.to_string(),
            path: path.to_string(),
            offset,
            length,
            stall_timeout: (!stall_timeout.is_zero()).then_some(stall_timeout),
        })
        .await
    }

    /// Buffered read for remote disks.
    /// The transport stream is collected into owned Bytes for caller sharing.
    #[tracing::instrument(level = "trace", skip_all)]
    async fn read_file_mmap_copy(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<Bytes> {
        // For remote disks, use the regular reader and read into Bytes
        let reader = self.read_file_stream(volume, path, offset, length).await?;

        use tokio::io::AsyncReadExt;
        let mut reader = reader;

        // Read all data into Bytes (single allocation)
        let mut buffer = Vec::with_capacity(length);
        reader.read_to_end(&mut buffer).await?;

        Ok(Bytes::from(buffer))
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn append_file(&self, volume: &str, path: &str) -> Result<FileWriter> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            op = "append_file",
            state = "started",
            "Remote disk RPC started"
        );

        if self.health.is_faulty() {
            return Err(DiskError::FaultyDisk);
        }
        let disk = self.disk_ref().await;
        self.open_write_with_retry(WriteStreamRequest {
            endpoint: self.endpoint.grid_host(),
            disk,
            volume: volume.to_string(),
            path: path.to_string(),
            append: true,
            size: 0,
        })
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn create_file(&self, _origvolume: &str, volume: &str, path: &str, file_size: i64) -> Result<FileWriter> {
        // warn!(
        //     "disk remote create_file {}/{}/{} file_size={}",
        //     self.endpoint.to_string(),
        //     volume,
        //     path,
        //     file_size
        // );

        if self.health.is_faulty() {
            return Err(DiskError::FaultyDisk);
        }
        let disk = self.disk_ref().await;
        self.open_write_with_retry(WriteStreamRequest {
            endpoint: self.endpoint.grid_host(),
            disk,
            volume: volume.to_string(),
            path: path.to_string(),
            append: false,
            size: file_size,
        })
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn rename_file(&self, src_volume: &str, src_path: &str, dst_volume: &str, dst_path: &str) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            src_volume,
            src_path,
            dst_volume,
            dst_path,
            op = "rename_file",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(RenameFileRequest {
                    disk: self.endpoint.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(),
                });
                let canonical_body = rustfs_protos::canonical_rename_file_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "rename_file")?;

                let response = client.rename_file(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn rename_part(&self, src_volume: &str, src_path: &str, dst_volume: &str, dst_path: &str, meta: Bytes) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            src_volume,
            src_path,
            dst_volume,
            dst_path,
            op = "rename_part",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(RenamePartRequest {
                    disk: self.endpoint.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,
                });
                let canonical_body = rustfs_protos::canonical_rename_part_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "rename_part")?;

                let response = client.rename_part(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn prepare_part_transaction(
        &self,
        src_volume: &str,
        src_path: &str,
        dst_volume: &str,
        dst_path: &str,
        meta: Bytes,
    ) -> Result<()> {
        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(PreparePartTransactionRequest {
                    disk: self.endpoint.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,
                });
                let canonical_body = rustfs_protos::canonical_prepare_part_transaction_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "prepare_part_transaction")?;

                let response = client.prepare_part_transaction(request).await?.into_inner();
                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }
                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn settle_part_transaction(&self, volume: &str, path: &str, action: PartTransactionAction) -> Result<()> {
        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(SettlePartTransactionRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    path: path.to_string(),
                    rollback: action == PartTransactionAction::Rollback,
                });
                let canonical_body = rustfs_protos::canonical_settle_part_transaction_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "settle_part_transaction")?;

                let response = client.settle_part_transaction(request).await?.into_inner();
                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }
                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn delete(&self, volume: &str, path: &str, opt: DeleteOptions) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            recursive = opt.recursive,
            immediate = opt.immediate,
            op = "delete",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let options = serde_json::to_string(&opt)?;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let mut request = Request::new(DeleteRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    path: path.to_string(),
                    options,
                });
                let canonical_body = rustfs_protos::canonical_delete_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "delete")?;

                let response = client.delete(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn verify_file(&self, volume: &str, path: &str, fi: &FileInfo) -> Result<CheckPartsResp> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            op = "verify_file",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let file_info = serde_json::to_string(&fi)?;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(VerifyFileRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    path: path.to_string(),
                    file_info,
                });

                let response = client.verify_file(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                let check_parts_resp = serde_json::from_str::<CheckPartsResp>(&response.check_parts_resp)?;

                Ok(check_parts_resp)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn read_parts(&self, bucket: &str, paths: &[String]) -> Result<Vec<ObjectPartInfo>> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            bucket,
            path_count = paths.len(),
            op = "read_parts",
            state = "started",
            "Remote disk RPC started"
        );
        self.execute_with_timeout(
            || async {
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(ReadPartsRequest {
                    disk: self.endpoint.to_string(),
                    bucket: bucket.to_string(),
                    paths: paths.to_vec(),
                });

                let response = client.read_parts(request).await?.into_inner();
                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                let read_parts_resp = rmp_serde::from_slice::<Vec<ObjectPartInfo>>(&response.object_part_infos)?;

                Ok(read_parts_resp)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn check_parts(&self, volume: &str, path: &str, fi: &FileInfo) -> Result<CheckPartsResp> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            op = "check_parts",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let file_info = serde_json::to_string(&fi)?;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(CheckPartsRequest {
                    disk: self.endpoint.to_string(),
                    volume: volume.to_string(),
                    path: path.to_string(),
                    file_info,
                });

                let response = client.check_parts(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                let check_parts_resp = serde_json::from_str::<CheckPartsResp>(&response.check_parts_resp)?;

                Ok(check_parts_resp)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn read_multiple(&self, req: ReadMultipleReq) -> Result<Vec<ReadMultipleResp>> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            bucket = %req.bucket,
            prefix = %req.prefix,
            op = "read_multiple",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let read_multiple_req = compat_json(&req)?;
                let read_multiple_req_bin = encode_msgpack(&req)?;
                let disk = self.disk_ref().await;
                let mut client = self
                    .get_bulk_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(ReadMultipleRequest {
                    disk,
                    read_multiple_req,
                    read_multiple_req_bin: read_multiple_req_bin.into(),
                });

                let response = client.read_multiple(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_multiple_recv_bytes(
                    read_multiple_response_payload_len(&response),
                );

                let read_multiple_resps = decode_read_multiple_response_items(response, &self.endpoint)?;

                Ok(read_multiple_resps)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn write_all(&self, volume: &str, path: &str, data: Bytes) -> Result<()> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            bytes = data.len(),
            op = "write_all",
            state = "started",
            "Remote disk RPC started"
        );

        self.execute_with_timeout(
            || async {
                let data_len = data.len();
                let disk = self.disk_ref().await;
                let mut client = self.get_bulk_client().await.map_err(|err| {
                    crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_write_all_error();
                    Error::other(format!("can not get client, err: {err}"))
                })?;
                let mut request = Request::new(WriteAllRequest {
                    disk,
                    volume: volume.to_string(),
                    path: path.to_string(),
                    data,
                });
                let canonical_body = rustfs_protos::canonical_write_all_request_body(request.get_ref());
                attach_mutation_body_digest(&mut request, canonical_body, "write_all")?;

                crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_write_all_request();
                let response = match client.write_all(request).await {
                    Ok(response) => response.into_inner(),
                    Err(err) => {
                        crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_write_all_error();
                        return Err(err.into());
                    }
                };

                crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_write_all_sent_bytes(data_len);

                if !response.success {
                    crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_write_all_error();
                    return Err(response.error.unwrap_or_default().into());
                }

                Ok(())
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn read_all(&self, volume: &str, path: &str) -> Result<Bytes> {
        trace!(
            event = EVENT_REMOTE_DISK_RPC,
            component = LOG_COMPONENT_ECSTORE,
            subsystem = LOG_SUBSYSTEM_REMOTE_DISK,
            endpoint = %self.endpoint,
            volume,
            path,
            op = "read_all",
            state = "started",
            "Remote disk RPC started"
        );

        // Idempotent full read: eligible for the bounded transient-network retry so a single
        // reset-by-peer during the read-after-write window does not erode the read quorum
        // (see issue #2761).
        self.execute_read_with_retry(
            "read_all",
            || async {
                let disk = self.disk_ref().await;
                let mut client = self.get_bulk_client().await.map_err(|err| {
                    crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_all_error();
                    Error::other(format!("can not get client, err: {err}"))
                })?;
                let request = Request::new(ReadAllRequest {
                    disk,
                    volume: volume.to_string(),
                    path: path.to_string(),
                });

                crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_all_request();
                let response = match client.read_all(request).await {
                    Ok(response) => response.into_inner(),
                    Err(err) => {
                        crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_all_error();
                        return Err(err.into());
                    }
                };

                if !response.success {
                    crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_all_error();
                    return Err(response.error.unwrap_or_default().into());
                }

                crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_all_recv_bytes(response.data.len());
                Ok(response.data)
            },
            get_max_timeout_duration(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    async fn disk_info(&self, opts: &DiskInfoOptions) -> Result<DiskInfo> {
        // disk_info is idempotent/read-only, so it is eligible for the P3-3 bounded retry.
        self.execute_read_with_retry(
            "disk_info",
            || async {
                let opts = serde_json::to_string(&opts)?;
                let mut client = self
                    .get_client()
                    .await
                    .map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
                let request = Request::new(DiskInfoRequest {
                    disk: self.endpoint.to_string(),
                    opts,
                });

                let response = client.disk_info(request).await?.into_inner();

                if !response.success {
                    return Err(response.error.unwrap_or_default().into());
                }

                let disk_info = serde_json::from_str::<DiskInfo>(&response.disk_info)?;

                Ok(disk_info)
            },
            get_drive_disk_info_timeout(),
        )
        .await
    }

    #[tracing::instrument(level = "trace", skip_all)]
    fn start_scan(&self) -> ScanGuard {
        self.scanning.fetch_add(1, Ordering::Relaxed);
        ScanGuard(Arc::clone(&self.scanning))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cluster::rpc::internode_data_transport::{InternodeDataTransportCapabilities, TcpHttpInternodeDataTransport};
    use crate::runtime::sources as runtime_sources;
    use serde_json::Value;
    use serial_test::serial;
    use std::io::{self as std_io, Write};
    use std::pin::Pin;
    use std::sync::{Arc, Mutex, Mutex as StdMutex, Once};
    use std::task::{Context, Poll};
    use tokio::io::{ReadBuf, duplex};
    use tokio::net::TcpListener;
    use tonic::transport::Endpoint as TonicEndpoint;
    use tracing::Level;
    use tracing_subscriber::{Registry, fmt::MakeWriter, layer::SubscriberExt};
    use uuid::Uuid;

    static INIT: Once = Once::new();

    // `#[serial(internode_metrics)]` marks every test that observes
    // `global_internode_metrics()`. Those counters are a process-wide singleton:
    // some of these tests snapshot a counter, run one decode, and assert on the
    // delta, while others deliberately record decode errors or call
    // `reset_internode_metrics_for_test()`. Run concurrently in one process they
    // corrupt each other's deltas — a sibling's error bumps the "no decode error"
    // assertion off zero, and a sibling's reset can drive an `after > before`
    // assertion backwards.
    //
    // The marker only takes effect under the `cargo test` fallback; nextest
    // already isolates each test in its own process, so every test there gets its
    // own copy of the counters (see `docs/testing/README.md`). Any new test that
    // reads or mutates the global internode metrics belongs in this group.

    #[test]
    fn snapshot_lease_response_requires_current_protocol_and_valid_token() {
        let token = SnapshotLeaseToken::new();
        let response = SnapshotLeaseResponse {
            success: true,
            token: token.as_bytes().to_vec().into(),
            protocol_version: SNAPSHOT_LEASE_PROTOCOL_VERSION,
            error: None,
        };
        assert_eq!(snapshot_lease_token_from_response(response).unwrap(), token);

        let incompatible = SnapshotLeaseResponse {
            success: true,
            token: token.as_bytes().to_vec().into(),
            protocol_version: SNAPSHOT_LEASE_PROTOCOL_VERSION + 1,
            error: None,
        };
        assert!(snapshot_lease_token_from_response(incompatible).is_err());

        let malformed = SnapshotLeaseResponse {
            success: true,
            token: Bytes::from_static(b"not-a-uuid"),
            protocol_version: SNAPSHOT_LEASE_PROTOCOL_VERSION,
            error: None,
        };
        assert!(snapshot_lease_token_from_response(malformed).is_err());
    }

    #[test]
    fn list_volumes_decode_rejects_a_malformed_entry() {
        let valid = serde_json::to_string(&VolumeInfo {
            name: "bucket".to_string(),
            created: None,
        })
        .expect("volume info should serialize");
        let err = decode_volume_infos(vec![valid, "{".to_string()])
            .expect_err("a malformed volume entry must fail the complete response");

        assert!(err.to_string().contains("entry 1"));
    }

    #[test]
    fn decoded_remote_metadata_rejects_default_like_delete_marker() {
        let forged = FileInfo {
            deleted: true,
            ..Default::default()
        };
        assert!(matches!(validate_decoded_file_info(&forged), Err(DiskError::FileCorrupt)));

        let marker = FileInfo {
            volume: "bucket".to_string(),
            name: "object".to_string(),
            version_id: Some(Uuid::new_v4()),
            deleted: true,
            mod_time: Some(::time::OffsetDateTime::now_utc()),
            ..Default::default()
        };
        validate_decoded_file_info(&marker).expect("canonical remote delete marker should validate");
    }

    #[derive(Clone, Default)]
    struct CapturedLogs {
        buffer: Arc<Mutex<Vec<u8>>>,
    }

    struct CapturedLogWriter {
        buffer: Arc<Mutex<Vec<u8>>>,
    }

    impl CapturedLogs {
        fn lines(&self) -> Vec<Value> {
            let buffer = self
                .buffer
                .lock()
                .expect("captured logs mutex should not be poisoned")
                .clone();
            String::from_utf8(buffer)
                .expect("captured logs should be valid UTF-8")
                .lines()
                .map(|line| serde_json::from_str::<Value>(line).expect("captured log line should be valid JSON"))
                .collect()
        }
    }

    impl Write for CapturedLogWriter {
        fn write(&mut self, buf: &[u8]) -> std_io::Result<usize> {
            self.buffer
                .lock()
                .expect("captured logs mutex should not be poisoned")
                .extend_from_slice(buf);
            Ok(buf.len())
        }

        fn flush(&mut self) -> std_io::Result<()> {
            Ok(())
        }
    }

    impl<'a> MakeWriter<'a> for CapturedLogs {
        type Writer = CapturedLogWriter;

        fn make_writer(&'a self) -> Self::Writer {
            CapturedLogWriter {
                buffer: Arc::clone(&self.buffer),
            }
        }
    }

    #[derive(Debug, Clone)]
    enum RecordedTransportCall {
        Read(ReadStreamRequest),
        Write(WriteStreamRequest),
        WalkDir(WalkDirStreamRequest),
        NsScanner(NsScannerStreamRequest),
        NsScannerProbe(NsScannerCapabilityRequest),
    }

    #[derive(Debug, Clone, Default)]
    struct RecordingInternodeDataTransport {
        calls: Arc<StdMutex<Vec<RecordedTransportCall>>>,
        ns_scanner_probe_status: Arc<StdMutex<Option<u16>>>,
    }

    impl RecordingInternodeDataTransport {
        fn with_ns_scanner_probe_status(status: u16) -> Self {
            Self {
                calls: Arc::default(),
                ns_scanner_probe_status: Arc::new(StdMutex::new(Some(status))),
            }
        }

        fn set_ns_scanner_probe_status(&self, status: Option<u16>) {
            *self
                .ns_scanner_probe_status
                .lock()
                .expect("namespace scanner probe status lock poisoned") = status;
        }

        fn calls(&self) -> Vec<RecordedTransportCall> {
            self.calls.lock().expect("recorded transport calls lock poisoned").clone()
        }

        fn record(&self, call: RecordedTransportCall) {
            self.calls.lock().expect("recorded transport calls lock poisoned").push(call);
        }
    }

    #[derive(Debug)]
    enum WalkDirTestStep {
        Error(DiskError),
        Data(Vec<u8>),
        PartialDataThenError { data: Vec<u8>, error: io::Error },
    }

    #[derive(Debug, Clone, Default)]
    struct RetryingWalkDirInternodeDataTransport {
        calls: Arc<StdMutex<Vec<RecordedTransportCall>>>,
        steps: Arc<StdMutex<Vec<WalkDirTestStep>>>,
    }

    impl RetryingWalkDirInternodeDataTransport {
        fn with_steps(steps: Vec<WalkDirTestStep>) -> Self {
            Self {
                calls: Arc::new(StdMutex::new(Vec::new())),
                steps: Arc::new(StdMutex::new(steps)),
            }
        }

        fn calls(&self) -> Vec<RecordedTransportCall> {
            self.calls.lock().expect("recorded transport calls lock poisoned").clone()
        }

        fn record(&self, call: RecordedTransportCall) {
            self.calls.lock().expect("recorded transport calls lock poisoned").push(call);
        }
    }

    #[derive(Debug, Clone)]
    enum OpenWriteTestStep {
        Error(DiskError),
        Success,
    }

    #[derive(Debug, Clone, Default)]
    struct RetryingOpenWriteInternodeDataTransport {
        calls: Arc<StdMutex<Vec<RecordedTransportCall>>>,
        steps: Arc<StdMutex<Vec<OpenWriteTestStep>>>,
    }

    impl RetryingOpenWriteInternodeDataTransport {
        fn with_steps(steps: Vec<OpenWriteTestStep>) -> Self {
            Self {
                calls: Arc::new(StdMutex::new(Vec::new())),
                steps: Arc::new(StdMutex::new(steps)),
            }
        }

        fn calls(&self) -> Vec<RecordedTransportCall> {
            self.calls.lock().expect("recorded transport calls lock poisoned").clone()
        }

        fn record(&self, call: RecordedTransportCall) {
            self.calls.lock().expect("recorded transport calls lock poisoned").push(call);
        }
    }

    #[derive(Debug, Clone, Default)]
    struct RetryingOpenReadInternodeDataTransport {
        calls: Arc<StdMutex<Vec<RecordedTransportCall>>>,
        // Reuses OpenWriteTestStep as a transport-agnostic open outcome (Error | Success).
        steps: Arc<StdMutex<Vec<OpenWriteTestStep>>>,
    }

    impl RetryingOpenReadInternodeDataTransport {
        fn with_steps(steps: Vec<OpenWriteTestStep>) -> Self {
            Self {
                calls: Arc::new(StdMutex::new(Vec::new())),
                steps: Arc::new(StdMutex::new(steps)),
            }
        }

        fn calls(&self) -> Vec<RecordedTransportCall> {
            self.calls.lock().expect("recorded transport calls lock poisoned").clone()
        }

        fn record(&self, call: RecordedTransportCall) {
            self.calls.lock().expect("recorded transport calls lock poisoned").push(call);
        }
    }

    #[derive(Debug, Default)]
    struct EmptyTestReader;

    impl AsyncRead for EmptyTestReader {
        fn poll_read(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
            Poll::Ready(Ok(()))
        }
    }

    fn sample_rename_data_file_info() -> FileInfo {
        FileInfo {
            volume: "bucket".to_string(),
            name: "object".to_string(),
            version_id: Some(Uuid::new_v4()),
            data_dir: Some(Uuid::new_v4()),
            size: 64 * 1024,
            mod_time: Some(::time::OffsetDateTime::UNIX_EPOCH + ::time::Duration::seconds(1)),
            metadata: [
                ("etag".to_string(), "etag-value".to_string()),
                ("content-type".to_string(), "application/octet-stream".to_string()),
            ]
            .into_iter()
            .collect(),
            erasure: rustfs_filemeta::ErasureInfo {
                algorithm: rustfs_filemeta::ERASURE_ALGORITHM.to_string(),
                data_blocks: 4,
                parity_blocks: 2,
                block_size: 1024 * 1024,
                index: 1,
                distribution: vec![1, 2, 3, 4, 5, 6],
                ..Default::default()
            },
            ..Default::default()
        }
    }

    fn sample_read_multiple_resp(file: &str, data: &[u8]) -> ReadMultipleResp {
        ReadMultipleResp {
            bucket: "bucket".to_string(),
            prefix: "prefix".to_string(),
            file: file.to_string(),
            exists: true,
            data: data.to_vec(),
            ..Default::default()
        }
    }

    fn sample_remote_endpoint() -> Endpoint {
        Endpoint {
            url: url::Url::parse("http://server:9000/disk-a").expect("endpoint URL should parse"),
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        }
    }

    #[test]
    #[serial(internode_metrics)]
    fn read_multiple_response_decode_prefers_msgpack_payloads() {
        let endpoint = sample_remote_endpoint();
        let msgpack_resp = sample_read_multiple_resp("msgpack", b"binary");
        let json_resp = sample_read_multiple_resp("json", b"fallback");
        let response = ReadMultipleResponse {
            success: true,
            read_multiple_resps: vec![serde_json::to_string(&json_resp).expect("json fallback should encode")],
            read_multiple_resps_bin: vec![encode_msgpack(&msgpack_resp).expect("msgpack response should encode").into()],
            error: None,
        };
        let before = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_total_for_test();

        let decoded = decode_read_multiple_response_items(response, &endpoint).expect("msgpack response should decode");
        let after = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_total_for_test();

        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].file, "msgpack");
        assert_eq!(decoded[0].data, b"binary");
        assert!(after > before, "successful response msgpack decode should increment traffic metrics");
    }

    #[test]
    #[serial(internode_metrics)]
    fn read_multiple_response_decode_falls_back_to_json_payloads() {
        let endpoint = sample_remote_endpoint();
        let json_resp = sample_read_multiple_resp("json", b"fallback");
        let response = ReadMultipleResponse {
            success: true,
            read_multiple_resps: vec![serde_json::to_string(&json_resp).expect("json fallback should encode")],
            read_multiple_resps_bin: Vec::new(),
            error: None,
        };
        let before = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_total_for_test();

        let decoded = decode_read_multiple_response_items(response, &endpoint).expect("json response should decode");
        let after = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_total_for_test();

        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].file, "json");
        assert_eq!(decoded[0].data, b"fallback");
        assert!(after > before, "successful response JSON decode should increment traffic metrics");
    }

    #[test]
    #[serial(internode_metrics)]
    fn rename_data_response_accepts_legacy_json_without_decode_error() {
        crate::cluster::rpc::runtime_sources::reset_internode_metrics_for_test();
        let response = RenameDataResp {
            old_data_dir: Some(Uuid::new_v4()),
            sign: Some(vec![0x14, 0x35]),
            old_current_size: Some(crate::disk::OldCurrentSize::Present(64 * 1024)),
        };
        let json = serde_json::to_string(&response).expect("legacy rename_data JSON response should encode");
        let decode_before = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_total_for_test();
        let decode_errors_before = crate::cluster::rpc::runtime_sources::internode_msgpack_json_decode_error_total_for_test();

        let decoded = decode_msgpack_or_json::<RenameDataResp>(&[], &json, "RenameDataResp")
            .expect("legacy rename_data JSON response should decode");
        let decode_after = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_total_for_test();
        let decode_errors_after = crate::cluster::rpc::runtime_sources::internode_msgpack_json_decode_error_total_for_test();

        assert_eq!(decoded.old_data_dir, response.old_data_dir);
        assert_eq!(decoded.sign, response.sign);
        assert_eq!(decoded.old_current_size, response.old_current_size);
        assert!(
            decode_after > decode_before,
            "legacy JSON response should increment successful decode traffic"
        );
        assert_eq!(
            decode_errors_after, decode_errors_before,
            "legacy JSON compatibility fallback must stay observable without becoming a decode error"
        );
    }

    #[test]
    fn read_multiple_response_payload_len_prefers_msgpack_and_falls_back_to_json() {
        let bin_a = encode_msgpack(&sample_read_multiple_resp("a", b"binary")).expect("msgpack should encode");
        let bin_b = encode_msgpack(&sample_read_multiple_resp("b", b"more")).expect("msgpack should encode");
        let json = serde_json::to_string(&sample_read_multiple_resp("j", b"fallback")).expect("json should encode");

        // When msgpack bins are present, the length is their aggregate size (JSON strings ignored).
        let with_bin = ReadMultipleResponse {
            success: true,
            read_multiple_resps: vec![json.clone()],
            read_multiple_resps_bin: vec![bin_a.clone().into(), bin_b.clone().into()],
            error: None,
        };
        assert_eq!(read_multiple_response_payload_len(&with_bin), bin_a.len() + bin_b.len());

        // With no msgpack bins, the JSON compatibility strings are summed instead.
        let json_only = ReadMultipleResponse {
            success: true,
            read_multiple_resps: vec![json.clone()],
            read_multiple_resps_bin: Vec::new(),
            error: None,
        };
        assert_eq!(read_multiple_response_payload_len(&json_only), json.len());

        // An empty response has zero payload.
        assert_eq!(read_multiple_response_payload_len(&ReadMultipleResponse::default()), 0);
    }

    #[test]
    fn compat_json_dual_writes_by_default() {
        with_internode_msgpack_env(
            [
                (rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY, None::<&str>),
                (rustfs_config::ENV_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED, None::<&str>),
            ],
            || {
                let resp = sample_read_multiple_resp("file", b"data");
                let json = compat_json(&resp).expect("compat_json should encode");
                assert!(!json.is_empty());
                assert_eq!(json, serde_json::to_string(&resp).expect("json should encode"));
            },
        );
    }

    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_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 resp = sample_read_multiple_resp("file", b"data");
                let json = compat_json(&resp).expect("compat_json should encode");

                assert!(!json.is_empty(), "old JSON peers must remain compatible without fleet confirmation");
                assert_eq!(json, serde_json::to_string(&resp).expect("json should encode"));
            },
        );
    }

    #[test]
    fn compat_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 resp = sample_read_multiple_resp("file", b"data");
                let json = compat_json(&resp).expect("compat_json should encode");

                assert!(json.is_empty(), "msgpack-only may empty JSON only after explicit fleet confirmation");
            },
        );
    }

    #[test]
    fn compat_json_restores_json_when_either_msgpack_only_gate_is_removed() {
        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 resp = sample_read_multiple_resp("file", b"data");
                let json = compat_json(&resp).expect("compat_json should encode");

                assert!(json.is_empty(), "both gates should enter msgpack-only send 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 resp = sample_read_multiple_resp("file", b"data");
                let json = compat_json(&resp).expect("compat_json should encode");

                assert!(!json.is_empty(), "removing either gate should restore old-peer JSON compatibility");
                assert_eq!(json, serde_json::to_string(&resp).expect("json should encode"));
            });
        }
    }

    #[test]
    #[serial(internode_metrics)]
    fn read_multiple_response_decode_reports_corrupt_msgpack_item() {
        let endpoint = sample_remote_endpoint();
        let response = ReadMultipleResponse {
            success: true,
            read_multiple_resps: Vec::new(),
            read_multiple_resps_bin: vec![
                encode_msgpack(&sample_read_multiple_resp("ok", b"data"))
                    .expect("msgpack response should encode")
                    .into(),
                bytes::Bytes::from_static(b"not-msgpack"),
            ],
            error: None,
        };
        let before = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_error_total_for_test();

        let err = decode_read_multiple_response_items(response, &endpoint).expect_err("corrupt msgpack item should fail");
        let after = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_error_total_for_test();
        let err = err.to_string();

        assert!(err.contains("ReadMultipleResp msgpack item 1"), "unexpected error: {err}");
        assert!(err.contains("server:9000"), "unexpected error: {err}");
        assert!(after > before, "corrupt response msgpack should increment decode-error metrics");
    }

    #[test]
    #[serial(internode_metrics)]
    fn read_multiple_response_decode_reports_corrupt_json_item() {
        let endpoint = sample_remote_endpoint();
        let response = ReadMultipleResponse {
            success: true,
            read_multiple_resps: vec![
                serde_json::to_string(&sample_read_multiple_resp("good", b"ok")).expect("json response should encode"),
                "{not-json".to_string(),
            ],
            read_multiple_resps_bin: Vec::new(),
            error: None,
        };
        let before = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_error_total_for_test();

        let err = decode_read_multiple_response_items(response, &endpoint).expect_err("corrupt json item should fail");
        let after = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_error_total_for_test();
        let err = err.to_string();

        assert!(err.contains("ReadMultipleResp json item 1"), "unexpected error: {err}");
        assert!(err.contains("server:9000"), "unexpected error: {err}");
        assert!(after > before, "corrupt response JSON should increment decode-error metrics");
    }

    fn sample_batch_read_version_resp(index: usize, path: &str, success: bool) -> BatchReadVersionResp {
        let mut file_info = FileInfo::new(path, 1, 0);
        file_info.erasure.index = 1;
        BatchReadVersionResp {
            index,
            path: path.to_string(),
            version_id: "version-a".to_string(),
            success,
            file_info,
            error: if success {
                String::new()
            } else {
                "file version not found".to_string()
            },
        }
    }

    #[test]
    #[serial(internode_metrics)]
    fn batch_read_version_response_decode_prefers_msgpack_payloads() {
        let endpoint = sample_remote_endpoint();
        let msgpack_resp = sample_batch_read_version_resp(7, "msgpack-object", true);
        let json_resp = sample_batch_read_version_resp(1, "json-object", false);
        let response = BatchReadVersionResponse {
            success: true,
            batch_read_version_resps: vec![serde_json::to_string(&json_resp).expect("json fallback should encode")],
            batch_read_version_resps_bin: vec![encode_msgpack(&msgpack_resp).expect("msgpack response should encode").into()],
            error: None,
        };

        let decoded = decode_batch_read_version_response_items(response, &endpoint).expect("msgpack response should decode");

        assert_eq!(decoded.len(), 1);
        assert_eq!(decoded[0].index, 7);
        assert_eq!(decoded[0].path, "msgpack-object");
        assert!(decoded[0].success);
    }

    #[test]
    #[serial(internode_metrics)]
    fn batch_read_version_response_rejects_invalid_success_metadata() {
        let endpoint = sample_remote_endpoint();
        let mut response_item = sample_batch_read_version_resp(0, "invalid-object", true);
        response_item.file_info.erasure.data_blocks = 0;
        response_item.file_info.erasure.parity_blocks = 2;
        let response = BatchReadVersionResponse {
            success: true,
            batch_read_version_resps: Vec::new(),
            batch_read_version_resps_bin: vec![encode_msgpack(&response_item).expect("msgpack response should encode").into()],
            error: None,
        };

        let err = decode_batch_read_version_response_items(response, &endpoint)
            .expect_err("successful remote response with invalid metadata must fail closed");

        assert_eq!(err, DiskError::FileCorrupt);
    }

    #[test]
    #[serial(internode_metrics)]
    fn batch_read_version_response_decode_reports_corrupt_msgpack_item() {
        let endpoint = sample_remote_endpoint();
        let response = BatchReadVersionResponse {
            success: true,
            batch_read_version_resps: Vec::new(),
            batch_read_version_resps_bin: vec![
                encode_msgpack(&sample_batch_read_version_resp(0, "ok", true))
                    .expect("msgpack response should encode")
                    .into(),
                bytes::Bytes::from_static(b"not-msgpack"),
            ],
            error: None,
        };
        let before = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_error_total_for_test();

        let err = decode_batch_read_version_response_items(response, &endpoint)
            .expect_err("corrupt msgpack item should fail")
            .to_string();
        let after = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_error_total_for_test();

        assert!(err.contains("BatchReadVersionResp msgpack item 1"), "unexpected error: {err}");
        assert!(err.contains("server:9000"), "unexpected error: {err}");
        assert!(after > before, "corrupt batch response msgpack should increment decode-error metrics");
    }

    #[test]
    #[serial(internode_metrics)]
    fn batch_read_version_response_decode_reports_corrupt_json_item() {
        let endpoint = sample_remote_endpoint();
        let response = BatchReadVersionResponse {
            success: true,
            batch_read_version_resps: vec![
                serde_json::to_string(&sample_batch_read_version_resp(0, "ok", false)).expect("json should encode"),
                "{not-json".to_string(),
            ],
            batch_read_version_resps_bin: Vec::new(),
            error: None,
        };
        let before = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_error_total_for_test();

        let err = decode_batch_read_version_response_items(response, &endpoint)
            .expect_err("corrupt json item should fail")
            .to_string();
        let after = rustfs_io_metrics::internode_metrics::global_internode_metrics().msgpack_json_decode_error_total_for_test();

        assert!(err.contains("BatchReadVersionResp json item 1"), "unexpected error: {err}");
        assert!(err.contains("server:9000"), "unexpected error: {err}");
        assert!(after > before, "corrupt batch response JSON should increment decode-error metrics");
    }

    #[test]
    fn batch_metadata_rpc_mode_defaults_to_off_and_parses_supported_values() {
        assert_eq!(parse_batch_metadata_rpc_mode(""), BatchMetadataRpcMode::Off);
        assert_eq!(parse_batch_metadata_rpc_mode("off"), BatchMetadataRpcMode::Off);
        assert_eq!(parse_batch_metadata_rpc_mode("auto"), BatchMetadataRpcMode::Auto);
        assert_eq!(parse_batch_metadata_rpc_mode("on"), BatchMetadataRpcMode::On);
        assert_eq!(parse_batch_metadata_rpc_mode("unknown"), BatchMetadataRpcMode::Off);
        assert_eq!(BatchMetadataRpcMode::Off.as_str(), "off");
        assert_eq!(BatchMetadataRpcMode::Auto.as_str(), "auto");
        assert_eq!(BatchMetadataRpcMode::On.as_str(), "on");
        assert!(!BatchMetadataRpcMode::Off.should_attempt());
        assert!(BatchMetadataRpcMode::Auto.should_attempt());
        assert!(BatchMetadataRpcMode::On.should_attempt());
        assert_eq!(BATCH_READ_VERSION_GATE_ATTEMPT, "attempt");
        assert_eq!(BATCH_READ_VERSION_GATE_OFF_UNARY, "off_unary");
        assert_eq!(BATCH_READ_VERSION_GATE_FALLBACK_UNIMPLEMENTED, "fallback_unimplemented");
        assert_eq!(BATCH_READ_VERSION_GATE_UNSUPPORTED_NO_FALLBACK, "unsupported_no_fallback");
    }

    #[test]
    fn batch_metadata_rpc_mode_uses_documented_env_before_legacy_alias() {
        temp_env::with_vars(
            [
                (ENV_RUSTFS_METADATA_BATCH_READ, Some("auto")),
                (LEGACY_ENV_RUSTFS_BATCH_METADATA_RPC, Some("on")),
            ],
            || {
                assert_eq!(batch_metadata_rpc_mode_from_env(), BatchMetadataRpcMode::Auto);
            },
        );
    }

    #[test]
    fn batch_metadata_rpc_mode_falls_back_to_legacy_env_alias() {
        temp_env::with_vars(
            [
                (ENV_RUSTFS_METADATA_BATCH_READ, None::<&str>),
                (LEGACY_ENV_RUSTFS_BATCH_METADATA_RPC, Some("on")),
            ],
            || {
                assert_eq!(batch_metadata_rpc_mode_from_env(), BatchMetadataRpcMode::On);
            },
        );
    }

    #[test]
    fn batch_metadata_rpc_mode_only_auto_falls_back_on_unimplemented() {
        assert!(!BatchMetadataRpcMode::Off.should_fallback_on_unimplemented());
        assert!(BatchMetadataRpcMode::Auto.should_fallback_on_unimplemented());
        assert!(!BatchMetadataRpcMode::On.should_fallback_on_unimplemented());
    }

    #[test]
    fn rename_data_file_info_named_msgpack_is_smaller_than_json() {
        let file_info = sample_rename_data_file_info();
        let json = serde_json::to_vec(&file_info).expect("file info json should encode");
        let named_msgpack = encode_msgpack_named(&file_info).expect("file info named msgpack should encode");

        assert!(
            named_msgpack.len() < json.len(),
            "expected named msgpack payload to be smaller than json (msgpack={}, json={})",
            named_msgpack.len(),
            json.len()
        );
    }

    #[test]
    fn rename_data_resp_named_msgpack_is_smaller_than_json() {
        let response = RenameDataResp {
            old_data_dir: Some(Uuid::new_v4()),
            sign: Some(vec![1_u8; 32]),
            old_current_size: Some(crate::disk::OldCurrentSize::Present(4096)),
        };
        let json = serde_json::to_vec(&response).expect("rename data response json should encode");
        let named_msgpack = encode_msgpack_named(&response).expect("rename data response named msgpack should encode");

        assert!(
            named_msgpack.len() < json.len(),
            "expected named msgpack payload to be smaller than json (msgpack={}, json={})",
            named_msgpack.len(),
            json.len()
        );
    }

    #[derive(Debug, Default)]
    struct SinkTestWriter;

    impl AsyncWrite for SinkTestWriter {
        fn poll_write(self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
            Poll::Ready(Ok(buf.len()))
        }

        fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
            Poll::Ready(Ok(()))
        }

        fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
            Poll::Ready(Ok(()))
        }
    }

    #[async_trait::async_trait]
    impl InternodeDataTransport for RecordingInternodeDataTransport {
        async fn open_read(&self, request: ReadStreamRequest) -> Result<FileReader> {
            self.record(RecordedTransportCall::Read(request));
            Ok(Box::new(EmptyTestReader))
        }

        async fn open_write(&self, request: WriteStreamRequest) -> Result<FileWriter> {
            self.record(RecordedTransportCall::Write(request));
            Ok(Box::new(SinkTestWriter))
        }

        async fn open_walk_dir(&self, request: WalkDirStreamRequest) -> Result<FileReader> {
            self.record(RecordedTransportCall::WalkDir(request));
            Ok(Box::new(EmptyTestReader))
        }

        async fn open_ns_scanner(&self, request: NsScannerStreamRequest) -> Result<FileReader> {
            self.record(RecordedTransportCall::NsScanner(request));
            Ok(Box::new(EmptyTestReader))
        }

        async fn probe_ns_scanner(&self, request: NsScannerCapabilityRequest) -> Result<Uuid> {
            self.record(RecordedTransportCall::NsScannerProbe(request));
            if let Some(status) = *self
                .ns_scanner_probe_status
                .lock()
                .expect("namespace scanner probe status lock poisoned")
            {
                let status = reqwest::StatusCode::from_u16(status).expect("test status code should be valid");
                return Err(
                    rustfs_rio::new_test_internode_http_io_error(rustfs_rio::InternodeHttpErrorKind::HttpStatus(status)).into(),
                );
            }
            Ok(Uuid::from_u128(1))
        }

        fn name(&self) -> &'static str {
            "recording"
        }

        fn capabilities(&self) -> InternodeDataTransportCapabilities {
            InternodeDataTransportCapabilities::tcp_http()
        }
    }

    #[async_trait::async_trait]
    impl InternodeDataTransport for RetryingWalkDirInternodeDataTransport {
        async fn open_read(&self, _request: ReadStreamRequest) -> Result<FileReader> {
            panic!("open_read should not be used in walk_dir retry test");
        }

        async fn open_write(&self, _request: WriteStreamRequest) -> Result<FileWriter> {
            panic!("open_write should not be used in walk_dir retry test");
        }

        async fn open_walk_dir(&self, request: WalkDirStreamRequest) -> Result<FileReader> {
            self.record(RecordedTransportCall::WalkDir(request));
            let step = self.steps.lock().expect("walk_dir retry steps lock poisoned").remove(0);
            match step {
                WalkDirTestStep::Error(err) => Err(err),
                WalkDirTestStep::Data(data) => Ok(Box::new(Cursor::new(data))),
                WalkDirTestStep::PartialDataThenError { data, error } => Ok(Box::new(PartialThenErrorReader {
                    cursor: Cursor::new(data),
                    error: Some(error),
                })),
            }
        }

        async fn open_ns_scanner(&self, _request: NsScannerStreamRequest) -> Result<FileReader> {
            panic!("open_ns_scanner should not be used in walk_dir retry test");
        }

        async fn probe_ns_scanner(&self, _request: NsScannerCapabilityRequest) -> Result<Uuid> {
            Ok(Uuid::from_u128(1))
        }

        fn name(&self) -> &'static str {
            "retrying-walk-dir"
        }

        fn capabilities(&self) -> InternodeDataTransportCapabilities {
            InternodeDataTransportCapabilities::tcp_http()
        }
    }

    #[async_trait::async_trait]
    impl InternodeDataTransport for RetryingOpenWriteInternodeDataTransport {
        async fn open_read(&self, _request: ReadStreamRequest) -> Result<FileReader> {
            panic!("open_read should not be used in open_write retry test");
        }

        async fn open_write(&self, request: WriteStreamRequest) -> Result<FileWriter> {
            self.record(RecordedTransportCall::Write(request));
            let step = self.steps.lock().expect("open_write retry steps lock poisoned").remove(0);
            match step {
                OpenWriteTestStep::Error(err) => Err(err),
                OpenWriteTestStep::Success => Ok(Box::new(SinkTestWriter)),
            }
        }

        async fn open_walk_dir(&self, _request: WalkDirStreamRequest) -> Result<FileReader> {
            panic!("open_walk_dir should not be used in open_write retry test");
        }

        async fn open_ns_scanner(&self, _request: NsScannerStreamRequest) -> Result<FileReader> {
            panic!("open_ns_scanner should not be used in open_write retry test");
        }

        async fn probe_ns_scanner(&self, _request: NsScannerCapabilityRequest) -> Result<Uuid> {
            Ok(Uuid::from_u128(1))
        }

        fn name(&self) -> &'static str {
            "retrying-open-write"
        }

        fn capabilities(&self) -> InternodeDataTransportCapabilities {
            InternodeDataTransportCapabilities::tcp_http()
        }
    }

    #[async_trait::async_trait]
    impl InternodeDataTransport for RetryingOpenReadInternodeDataTransport {
        async fn open_read(&self, request: ReadStreamRequest) -> Result<FileReader> {
            self.record(RecordedTransportCall::Read(request));
            let step = self.steps.lock().expect("open_read retry steps lock poisoned").remove(0);
            match step {
                OpenWriteTestStep::Error(err) => Err(err),
                OpenWriteTestStep::Success => Ok(Box::new(EmptyTestReader)),
            }
        }

        async fn open_write(&self, _request: WriteStreamRequest) -> Result<FileWriter> {
            panic!("open_write should not be used in open_read retry test");
        }

        async fn open_walk_dir(&self, _request: WalkDirStreamRequest) -> Result<FileReader> {
            panic!("open_walk_dir should not be used in open_read retry test");
        }

        fn name(&self) -> &'static str {
            "retrying-open-read"
        }

        fn capabilities(&self) -> InternodeDataTransportCapabilities {
            InternodeDataTransportCapabilities::tcp_http()
        }
    }

    async fn new_remote_disk_with_transport(data_transport: Arc<dyn InternodeDataTransport>) -> RemoteDisk {
        let endpoint = Endpoint {
            url: url::Url::parse("http://remote-node:9000/data/rustfs0").expect("operation should succeed"),
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };
        let disk_option = DiskOption {
            cleanup: false,
            health_check: false,
        };

        RemoteDisk::new(&endpoint, &disk_option, data_transport)
            .await
            .expect("operation should succeed")
    }

    #[tokio::test]
    async fn remote_disk_health_wrapper_balances_task_cancellation() {
        let disk = Arc::new(new_remote_disk_with_transport(Arc::new(RecordingInternodeDataTransport::default())).await);
        let task_disk = Arc::clone(&disk);
        let task = tokio::spawn(async move {
            task_disk
                .execute_with_timeout_for_op(
                    "cancellation-test",
                    || async { std::future::pending::<Result<()>>().await },
                    Duration::ZERO,
                )
                .await
        });

        tokio::time::timeout(Duration::from_secs(1), async {
            while disk.health.waiting_count() == 0 {
                tokio::task::yield_now().await;
            }
        })
        .await
        .expect("operation should enter remote disk health tracking");
        task.abort();
        let _ = task.await;

        assert_eq!(disk.health.waiting_count(), 0);
    }

    #[derive(Debug)]
    struct PartialThenErrorReader {
        cursor: Cursor<Vec<u8>>,
        error: Option<io::Error>,
    }

    impl AsyncRead for PartialThenErrorReader {
        fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
            let filled_before = buf.filled().len();
            match Pin::new(&mut self.cursor).poll_read(cx, buf) {
                Poll::Ready(Ok(())) => {
                    if buf.filled().len() > filled_before {
                        return Poll::Ready(Ok(()));
                    }

                    if let Some(err) = self.error.take() {
                        return Poll::Ready(Err(err));
                    }

                    Poll::Ready(Ok(()))
                }
                other => other,
            }
        }
    }

    fn init_tracing(filter_level: Level) {
        INIT.call_once(|| {
            let _ = tracing_subscriber::fmt()
                .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
                .with_max_level(filter_level)
                .with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339())
                .with_thread_names(true)
                .try_init();
        });
    }

    #[tokio::test]
    async fn test_remote_disk_creation() {
        let url = url::Url::parse("http://example.com:9000/path").expect("operation should succeed");
        let endpoint = Endpoint {
            url: url.clone(),
            is_local: false,
            pool_idx: 0,
            set_idx: 1,
            disk_idx: 2,
        };

        let disk_option = DiskOption {
            cleanup: false,
            health_check: false,
        };

        let remote_disk = RemoteDisk::new(&endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
            .await
            .expect("operation should succeed");

        assert!(!remote_disk.is_local());
        assert_eq!(remote_disk.endpoint.url, url);
        assert_eq!(remote_disk.endpoint.pool_idx, 0);
        assert_eq!(remote_disk.endpoint.set_idx, 1);
        assert_eq!(remote_disk.endpoint.disk_idx, 2);
        assert_eq!(remote_disk.host_name(), "example.com:9000");
    }

    #[tokio::test]
    async fn test_remote_disk_basic_properties() {
        let url = url::Url::parse("http://remote-server:9000").expect("operation should succeed");
        let endpoint = Endpoint {
            url: url.clone(),
            is_local: false,
            pool_idx: -1,
            set_idx: -1,
            disk_idx: -1,
        };

        let disk_option = DiskOption {
            cleanup: false,
            health_check: false,
        };

        let remote_disk = RemoteDisk::new(&endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
            .await
            .expect("operation should succeed");

        // Test basic properties
        assert!(!remote_disk.is_local());
        assert_eq!(remote_disk.host_name(), "remote-server:9000");
        assert!(remote_disk.to_string().contains("remote-server"));
        assert!(remote_disk.to_string().contains("9000"));

        // Test disk location
        let location = remote_disk.get_disk_location();
        assert_eq!(location.pool_idx, None);
        assert_eq!(location.set_idx, None);
        assert_eq!(location.disk_idx, None);
        assert!(!location.valid()); // None values make it invalid
    }

    #[tokio::test]
    async fn test_remote_disk_path() {
        let url = url::Url::parse("http://remote-server:9000/storage").expect("operation should succeed");
        let endpoint = Endpoint {
            url: url.clone(),
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let disk_option = DiskOption {
            cleanup: false,
            health_check: false,
        };

        let remote_disk = RemoteDisk::new(&endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
            .await
            .expect("operation should succeed");
        let path = remote_disk.path();

        // Remote disk path should be based on the URL path
        assert!(path.to_string_lossy().contains("storage"));
    }

    #[tokio::test]
    async fn test_remote_disk_is_online_detects_active_listener() {
        let listener = match TcpListener::bind("127.0.0.1:0").await {
            Ok(listener) => listener,
            Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return,
            Err(err) => panic!("test listener should bind: {err}"),
        };
        let addr = listener.local_addr().expect("listener local address should be available");

        let url =
            url::Url::parse(&format!("http://{}:{}/data/rustfs0", addr.ip(), addr.port())).expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let disk_option = DiskOption {
            cleanup: false,
            health_check: false,
        };

        let remote_disk = RemoteDisk::new(&endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
            .await
            .expect("operation should succeed");
        assert!(remote_disk.is_online().await);

        drop(listener);
    }

    #[tokio::test]
    async fn test_remote_disk_is_online_detects_missing_listener() {
        init_tracing(Level::ERROR);

        let listener = match TcpListener::bind("127.0.0.1:0").await {
            Ok(listener) => listener,
            Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return,
            Err(err) => panic!("test listener should bind: {err}"),
        };
        let addr = listener.local_addr().expect("listener local address should be available");
        let ip = addr.ip();
        let port = addr.port();

        drop(listener);

        let url = url::Url::parse(&format!("http://{ip}:{port}/data/rustfs0")).expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        temp_env::async_with_vars(
            [
                (rustfs_config::ENV_DRIVE_ACTIVE_CHECK_INTERVAL_SECS, Some("1")),
                (rustfs_config::ENV_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS, Some("1")),
            ],
            async {
                let disk_option = DiskOption {
                    cleanup: false,
                    health_check: true,
                };

                let remote_disk = RemoteDisk::new(&endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
                    .await
                    .expect("operation should succeed");
                remote_disk.enable_health_check();

                // Wait out the initial success-grace window so the active probe loop
                // actually attempts a connectivity check. Under the new
                // suspect-first semantics we only need to prove that the drive
                // transitions away from a clean Online state at least once.
                tokio::time::sleep(SKIP_IF_SUCCESS_BEFORE + Duration::from_secs(2)).await;
                assert!(
                    remote_disk.offline_duration_secs().is_some(),
                    "missing listener should transition the drive through suspect/offline tracking"
                );
                assert_ne!(
                    remote_disk.runtime_state(),
                    RuntimeDriveHealthState::Online,
                    "missing listener should not remain in a clean Online state after probing"
                );
            },
        )
        .await;
    }

    #[tokio::test]
    async fn test_remote_disk_recovery_requires_disk_rpc_readiness() {
        init_tracing(Level::ERROR);
        runtime_sources::ensure_test_rpc_secret();

        let listener = match TcpListener::bind("127.0.0.1:0").await {
            Ok(listener) => listener,
            Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return,
            Err(err) => panic!("test listener should bind: {err}"),
        };
        let addr = listener.local_addr().expect("listener local address should be available");
        let accept_task = tokio::spawn(async move {
            while let Ok((stream, _)) = listener.accept().await {
                drop(stream);
            }
        });

        let base_addr = format!("http://{}:{}", addr.ip(), addr.port());
        let url = url::Url::parse(&format!("{base_addr}/data/rustfs0")).expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };
        let health = Arc::new(DiskHealthTracker::new());
        health.mark_failure(&endpoint, "test_failure");
        health.mark_failure(&endpoint, "test_failure");
        assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Offline);
        let channel = TonicEndpoint::from_shared(base_addr.clone())
            .expect("operation should succeed")
            .connect_lazy();
        runtime_sources::cache_test_node_channel(base_addr.clone(), channel).await;
        assert!(runtime_sources::test_node_channel_is_cached(&base_addr).await);

        temp_env::async_with_vars(
            [
                (rustfs_config::ENV_DRIVE_RETURNING_PROBE_INTERVAL_SECS, Some("1")),
                (rustfs_config::ENV_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS, Some("1")),
            ],
            async {
                let cancel_token = CancellationToken::new();
                let monitor = tokio::spawn(RemoteDisk::monitor_remote_disk_recovery(
                    base_addr.clone(),
                    endpoint,
                    Arc::clone(&health),
                    cancel_token.clone(),
                ));

                tokio::time::sleep(Duration::from_millis(2_500)).await;
                cancel_token.cancel();
                let _ = monitor.await;

                assert_ne!(
                    health.runtime_state(),
                    RuntimeDriveHealthState::Online,
                    "a plain TCP listener without disk_info RPC readiness must not restore the remote disk online"
                );
                assert!(
                    !runtime_sources::test_node_channel_is_cached(&base_addr).await,
                    "failed recovery probes should evict stale cached gRPC channels"
                );
            },
        )
        .await;

        accept_task.abort();
    }

    #[tokio::test]
    async fn test_copy_stream_with_buffer_copies_full_payload() {
        let payload = b"walk-dir-stream".repeat(1024);
        let expected = payload.clone();
        let (mut write_half, mut read_half) = duplex(128);

        let copy_task = tokio::spawn(async move {
            let mut cursor = Cursor::new(payload);
            copy_stream_with_buffer(&mut cursor, &mut write_half, 4 * 1024)
                .await
                .expect("operation should succeed");
        });

        let mut copied = Vec::new();
        read_half.read_to_end(&mut copied).await.expect("operation should succeed");
        copy_task.await.expect("operation should succeed");

        assert_eq!(copied, expected);
    }

    #[tokio::test]
    async fn test_remote_disk_disk_id() {
        let url = url::Url::parse("http://remote-server:9000").expect("operation should succeed");
        let endpoint = Endpoint {
            url: url.clone(),
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let disk_option = DiskOption {
            cleanup: false,
            health_check: false,
        };

        let remote_disk = RemoteDisk::new(&endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
            .await
            .expect("operation should succeed");

        // Initially, disk ID should be None
        let initial_id = remote_disk.get_disk_id().await.expect("operation should succeed");
        assert!(initial_id.is_none());

        // Set a disk ID
        let test_id = Uuid::new_v4();
        remote_disk
            .set_disk_id(Some(test_id))
            .await
            .expect("operation should succeed");

        // Verify the disk ID was set
        let retrieved_id = remote_disk.get_disk_id().await.expect("operation should succeed");
        assert_eq!(retrieved_id, Some(test_id));

        // Clear the disk ID
        remote_disk.set_disk_id(None).await.expect("operation should succeed");
        let cleared_id = remote_disk.get_disk_id().await.expect("operation should succeed");
        assert!(cleared_id.is_none());
    }

    #[tokio::test]
    async fn test_remote_disk_ref_prefers_disk_id() {
        let url = url::Url::parse("http://remote-server:9000").expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };
        let disk_option = DiskOption {
            cleanup: false,
            health_check: false,
        };

        let remote_disk = RemoteDisk::new(&endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
            .await
            .expect("operation should succeed");
        assert_eq!(remote_disk.disk_ref().await, endpoint.to_string());

        let disk_id = Uuid::new_v4();
        remote_disk
            .set_disk_id(Some(disk_id))
            .await
            .expect("operation should succeed");

        assert_eq!(remote_disk.disk_ref().await, disk_id.to_string());
    }

    #[tokio::test]
    async fn test_remote_disk_read_file_stream_uses_configured_data_transport() {
        temp_env::async_with_vars([(rustfs_config::ENV_OBJECT_DISK_READ_TIMEOUT, None::<&str>)], async {
            let transport = RecordingInternodeDataTransport::default();
            let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
            let expected_disk = remote_disk.disk_ref().await;

            let _reader = remote_disk
                .read_file_stream("bucket", "object/part.1", 7, 11)
                .await
                .expect("operation should succeed");

            let calls = transport.calls();
            assert_eq!(calls.len(), 1);
            match &calls[0] {
                RecordedTransportCall::Read(request) => {
                    assert_eq!(request.endpoint, "http://remote-node:9000");
                    assert_eq!(request.disk, expected_disk);
                    assert_eq!(request.volume, "bucket");
                    assert_eq!(request.path, "object/part.1");
                    assert_eq!(request.offset, 7);
                    assert_eq!(request.length, 11);
                    assert_eq!(request.stall_timeout, Some(get_object_disk_read_timeout()));
                }
                other => panic!("expected read transport call, got {other:?}"),
            }
        })
        .await;
    }

    #[tokio::test]
    async fn test_remote_disk_read_file_stream_disables_stall_timeout_when_configured_zero() {
        temp_env::async_with_vars([(rustfs_config::ENV_OBJECT_DISK_READ_TIMEOUT, Some("0"))], async {
            let transport = RecordingInternodeDataTransport::default();
            let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;

            let _reader = remote_disk
                .read_file_stream("bucket", "object/part.1", 7, 11)
                .await
                .expect("operation should succeed");

            let calls = transport.calls();
            assert_eq!(calls.len(), 1);
            match &calls[0] {
                RecordedTransportCall::Read(request) => assert_eq!(request.stall_timeout, None),
                other => panic!("expected read transport call, got {other:?}"),
            }
        })
        .await;
    }

    #[tokio::test]
    async fn test_remote_disk_create_and_append_file_use_configured_data_transport() {
        let transport = RecordingInternodeDataTransport::default();
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
        let expected_disk = remote_disk.disk_ref().await;

        let _created = remote_disk
            .create_file("orig-bucket", "bucket", "object/part.1", 4096)
            .await
            .expect("operation should succeed");
        let _appended = remote_disk
            .append_file("bucket", "object/part.2")
            .await
            .expect("operation should succeed");

        let calls = transport.calls();
        assert_eq!(calls.len(), 2);

        match &calls[0] {
            RecordedTransportCall::Write(request) => {
                assert_eq!(request.endpoint, "http://remote-node:9000");
                assert_eq!(request.disk, expected_disk);
                assert_eq!(request.volume, "bucket");
                assert_eq!(request.path, "object/part.1");
                assert!(!request.append);
                assert_eq!(request.size, 4096);
            }
            other => panic!("expected create write transport call, got {other:?}"),
        }

        match &calls[1] {
            RecordedTransportCall::Write(request) => {
                assert_eq!(request.endpoint, "http://remote-node:9000");
                assert_eq!(request.disk, expected_disk);
                assert_eq!(request.volume, "bucket");
                assert_eq!(request.path, "object/part.2");
                assert!(request.append);
                assert_eq!(request.size, 0);
            }
            other => panic!("expected append write transport call, got {other:?}"),
        }
    }

    #[tokio::test]
    #[serial(internode_metrics)]
    async fn test_remote_disk_create_file_retries_once_on_retryable_open_write_error() {
        let transport = RetryingOpenWriteInternodeDataTransport::with_steps(vec![
            OpenWriteTestStep::Error(DiskError::from(rustfs_rio::new_test_internode_http_io_error(
                rustfs_rio::InternodeHttpErrorKind::ConnectionReset,
            ))),
            OpenWriteTestStep::Success,
        ]);
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
        crate::cluster::rpc::runtime_sources::reset_internode_metrics_for_test();

        let _created = remote_disk
            .create_file("orig-bucket", "bucket", "object/part.1", 4096)
            .await
            .expect("retryable open_write error should recover");

        let calls = transport.calls();
        assert_eq!(calls.len(), 2, "create_file should retry exactly once");
        let snapshot = crate::cluster::rpc::runtime_sources::internode_metrics_snapshot_for_test();
        assert_eq!(snapshot.outgoing_requests_total, 0);
    }

    #[tokio::test]
    async fn test_remote_disk_append_file_does_not_retry_non_retryable_open_write_error() {
        let transport = RetryingOpenWriteInternodeDataTransport::with_steps(vec![OpenWriteTestStep::Error(DiskError::from(
            rustfs_rio::new_test_internode_http_io_error(rustfs_rio::InternodeHttpErrorKind::Unknown),
        ))]);
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;

        let err = match remote_disk.append_file("bucket", "object/part.2").await {
            Ok(_) => panic!("non-retryable open_write error should be returned directly"),
            Err(err) => err,
        };

        assert_eq!(err.internode_http_error_kind(), Some(rustfs_rio::InternodeHttpErrorKind::Unknown));
        assert_eq!(transport.calls().len(), 1, "append_file should not retry non-retryable errors");
    }

    #[tokio::test]
    #[serial(internode_metrics)]
    async fn test_remote_disk_read_file_stream_retries_once_on_retryable_open_read_error() {
        // A transient reset-by-peer on a shard read during the read-after-write window must be
        // absorbed by one re-dial rather than eroding read quorum (issue #2761).
        let transport = RetryingOpenReadInternodeDataTransport::with_steps(vec![
            OpenWriteTestStep::Error(DiskError::from(rustfs_rio::new_test_internode_http_io_error(
                rustfs_rio::InternodeHttpErrorKind::ConnectionReset,
            ))),
            OpenWriteTestStep::Success,
        ]);
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
        crate::cluster::rpc::runtime_sources::reset_internode_metrics_for_test();

        let _reader = remote_disk
            .read_file_stream("bucket", "object/part.1", 0, 4096)
            .await
            .expect("retryable open_read error should recover");

        assert_eq!(transport.calls().len(), 2, "read_file_stream should retry exactly once");
    }

    #[tokio::test]
    async fn test_remote_disk_read_file_stream_does_not_retry_non_retryable_open_read_error() {
        let transport = RetryingOpenReadInternodeDataTransport::with_steps(vec![OpenWriteTestStep::Error(DiskError::from(
            rustfs_rio::new_test_internode_http_io_error(rustfs_rio::InternodeHttpErrorKind::Unknown),
        ))]);
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;

        let err = match remote_disk.read_file_stream("bucket", "object/part.2", 0, 4096).await {
            Ok(_) => panic!("non-retryable open_read error should be returned directly"),
            Err(err) => err,
        };

        assert_eq!(err.internode_http_error_kind(), Some(rustfs_rio::InternodeHttpErrorKind::Unknown));
        assert_eq!(transport.calls().len(), 1, "read_file_stream should not retry non-retryable errors");
    }

    #[tokio::test]
    async fn test_remote_disk_walk_dir_uses_configured_data_transport() {
        let transport = RecordingInternodeDataTransport::default();
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
        let expected_disk = remote_disk.disk_ref().await;
        let opts = WalkDirOptions {
            bucket: "bucket".to_string(),
            base_dir: "prefix".to_string(),
            recursive: true,
            report_notfound: false,
            filter_prefix: Some("part".to_string()),
            forward_to: None,
            limit: 10,
            disk_id: String::new(),
            ..Default::default()
        };
        let expected_body = serde_json::to_vec(&opts).expect("operation should succeed");
        let mut writer = Vec::new();

        remote_disk
            .walk_dir(opts, &mut writer)
            .await
            .expect("operation should succeed");

        let calls = transport.calls();
        assert_eq!(calls.len(), 1);
        match &calls[0] {
            RecordedTransportCall::WalkDir(request) => {
                assert_eq!(request.endpoint, "http://remote-node:9000");
                assert_eq!(request.disk, expected_disk);
                assert_eq!(request.body, expected_body);
                assert_eq!(request.stall_timeout, Some(get_drive_walkdir_stall_timeout()));
            }
            other => panic!("expected walk-dir transport call, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn test_remote_disk_namespace_scanner_uses_configured_data_transport() {
        let transport = RecordingInternodeDataTransport::default();
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
        let expected_disk = remote_disk.disk_ref().await;
        let expected_body = b"namespace-scanner-request".to_vec();
        let expected_request_id = Uuid::new_v4();
        let expected_server_epoch = Uuid::new_v4();
        let expected_session_id = Uuid::new_v4();

        let _reader = remote_disk
            .open_ns_scanner_stream(crate::disk::NsScannerOpenRequest {
                request_id: expected_request_id,
                server_epoch: expected_server_epoch,
                session_id: expected_session_id,
                session_sequence: 3,
                next_cycle: 7,
                leader_epoch: 9,
                body: expected_body.clone(),
                stall_timeout: Some(Duration::from_secs(15)),
            })
            .await
            .expect("namespace scanner stream should open");

        let calls = transport.calls();
        assert_eq!(calls.len(), 1);
        match &calls[0] {
            RecordedTransportCall::NsScanner(request) => {
                assert_eq!(request.endpoint, "http://remote-node:9000");
                assert_eq!(request.disk, expected_disk);
                assert_eq!(request.request_id, expected_request_id);
                assert_eq!(request.server_epoch, expected_server_epoch);
                assert_eq!(request.session_id, expected_session_id);
                assert_eq!(request.session_sequence, 3);
                assert_eq!(request.next_cycle, 7);
                assert_eq!(request.leader_epoch, 9);
                assert_eq!(request.body, expected_body);
                assert_eq!(request.stall_timeout, Some(Duration::from_secs(15)));
            }
            other => panic!("expected namespace scanner transport call, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn test_remote_disk_namespace_scanner_capability_uses_configured_data_transport() {
        let transport = RecordingInternodeDataTransport::default();
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;

        assert_eq!(
            remote_disk
                .ns_scanner_server_epoch()
                .await
                .expect("namespace scanner capability probe should succeed"),
            Some(Uuid::from_u128(1))
        );

        let calls = transport.calls();
        assert_eq!(calls.len(), 1);
        match &calls[0] {
            RecordedTransportCall::NsScannerProbe(request) => {
                assert_eq!(request.endpoint, "http://remote-node:9000");
            }
            other => panic!("expected namespace scanner capability probe, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn test_remote_disk_namespace_scanner_capability_rejects_old_and_incompatible_peers() {
        for status in [404, 405, 426] {
            let transport = RecordingInternodeDataTransport::with_ns_scanner_probe_status(status);
            let remote_disk = new_remote_disk_with_transport(Arc::new(transport)).await;

            assert_eq!(
                remote_disk
                    .ns_scanner_server_epoch()
                    .await
                    .expect("unsupported namespace scanner response should be classified"),
                None
            );
        }
    }

    #[tokio::test]
    async fn test_remote_disk_namespace_scanner_capability_rejects_legacy_transport() {
        let remote_disk = new_remote_disk_with_transport(Arc::new(RetryingOpenReadInternodeDataTransport::default())).await;

        assert_eq!(
            remote_disk
                .ns_scanner_server_epoch()
                .await
                .expect("legacy transport should be classified as unsupported"),
            None
        );
    }

    #[tokio::test]
    async fn test_remote_disk_namespace_scanner_capability_reprobes_after_peer_upgrade() {
        let transport = RecordingInternodeDataTransport::with_ns_scanner_probe_status(404);
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;

        assert_eq!(
            remote_disk
                .ns_scanner_server_epoch()
                .await
                .expect("old peer should be classified as unsupported"),
            None
        );
        transport.set_ns_scanner_probe_status(None);
        assert_eq!(
            remote_disk
                .ns_scanner_server_epoch()
                .await
                .expect("upgraded peer should be re-probed"),
            Some(Uuid::from_u128(1))
        );
        assert_eq!(transport.calls().len(), 2);
    }

    #[tokio::test]
    async fn test_remote_disk_namespace_scanner_capability_propagates_transient_failure() {
        let transport = RecordingInternodeDataTransport::with_ns_scanner_probe_status(503);
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport)).await;

        let err = remote_disk
            .ns_scanner_server_epoch()
            .await
            .expect_err("transient capability failure must not be reported as unsupported");
        assert!(matches!(
            err.internode_http_error_kind(),
            Some(rustfs_rio::InternodeHttpErrorKind::HttpStatus(status)) if status.as_u16() == 503
        ));
    }

    #[tokio::test]
    async fn test_remote_disk_walk_dir_preserves_skip_total_timeout_option() {
        let transport = RecordingInternodeDataTransport::default();
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
        let opts = WalkDirOptions {
            bucket: "bucket".to_string(),
            base_dir: "prefix".to_string(),
            recursive: true,
            skip_total_timeout: true,
            ..Default::default()
        };
        let mut writer = Vec::new();

        remote_disk
            .walk_dir(opts, &mut writer)
            .await
            .expect("walk_dir should be sent through configured data transport");

        let calls = transport.calls();
        assert_eq!(calls.len(), 1);
        match &calls[0] {
            RecordedTransportCall::WalkDir(request) => {
                let sent_opts: WalkDirOptions =
                    serde_json::from_slice(&request.body).expect("walk_dir request body should deserialize");
                assert!(sent_opts.skip_total_timeout);
                assert_eq!(request.stall_timeout, Some(get_drive_walkdir_stall_timeout()));
            }
            other => panic!("expected walk-dir transport call, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn test_remote_disk_walk_dir_uses_per_request_stall_timeout() {
        let transport = RecordingInternodeDataTransport::default();
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
        let opts = WalkDirOptions {
            bucket: "bucket".to_string(),
            base_dir: "prefix".to_string(),
            recursive: true,
            stall_timeout_ms: Some(60_000),
            ..Default::default()
        };
        let mut writer = Vec::new();

        remote_disk
            .walk_dir(opts, &mut writer)
            .await
            .expect("walk_dir should be sent through configured data transport");

        let calls = transport.calls();
        assert_eq!(calls.len(), 1);
        match &calls[0] {
            RecordedTransportCall::WalkDir(request) => {
                assert_eq!(request.stall_timeout, Some(Duration::from_secs(60)));
            }
            other => panic!("expected walk-dir transport call, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn test_remote_disk_walk_dir_retries_once_on_retryable_transport_error() {
        let transport = RetryingWalkDirInternodeDataTransport::with_steps(vec![
            WalkDirTestStep::Error(DiskError::other("HttpReader stream error: error decoding response body")),
            WalkDirTestStep::Data(b"walk-dir-retry-ok".to_vec()),
        ]);
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
        let opts = WalkDirOptions {
            bucket: "bucket".to_string(),
            base_dir: "config/iam".to_string(),
            recursive: true,
            report_notfound: false,
            filter_prefix: None,
            forward_to: None,
            limit: 10,
            disk_id: String::new(),
            ..Default::default()
        };
        let mut writer = Vec::new();

        remote_disk
            .walk_dir(opts, &mut writer)
            .await
            .expect("retryable walk_dir error should recover");

        assert_eq!(writer, b"walk-dir-retry-ok");
        assert_eq!(transport.calls().len(), 2, "walk_dir should retry exactly once");
    }

    #[tokio::test]
    async fn test_remote_disk_walk_dir_does_not_retry_after_partial_stream_failure() {
        let transport = RetryingWalkDirInternodeDataTransport::with_steps(vec![
            WalkDirTestStep::PartialDataThenError {
                data: b"partial-walk-dir".to_vec(),
                error: io::Error::new(io::ErrorKind::ConnectionReset, "connection reset"),
            },
            WalkDirTestStep::Data(b"walk-dir-retry-ok".to_vec()),
        ]);
        let remote_disk = new_remote_disk_with_transport(Arc::new(transport.clone())).await;
        let opts = WalkDirOptions {
            bucket: "bucket".to_string(),
            base_dir: "config/iam".to_string(),
            recursive: true,
            report_notfound: false,
            filter_prefix: None,
            forward_to: None,
            limit: 10,
            disk_id: String::new(),
            ..Default::default()
        };
        let mut writer = Vec::new();

        let err = remote_disk
            .walk_dir(opts, &mut writer)
            .await
            .expect_err("partial stream failure should be returned without retry");

        assert!(matches!(err, DiskError::Io(ref io_err) if io_err.kind() == io::ErrorKind::ConnectionReset));
        assert_eq!(writer, b"partial-walk-dir");
        assert_eq!(transport.calls().len(), 1, "walk_dir should not retry after writing partial bytes");
    }

    #[tokio::test]
    async fn test_remote_disk_endpoints_with_different_schemes() {
        let test_cases = vec![
            ("http://server:9000", "server:9000"),
            ("http://plain-server:80", "plain-server"),
            ("http://plain-server", "plain-server"),
            ("https://secure-server:443", "secure-server"),
            ("http://192.168.1.100:8080", "192.168.1.100:8080"),
            ("https://secure-server", "secure-server"),
        ];

        for (url_str, expected_hostname) in test_cases {
            let url = url::Url::parse(url_str).expect("operation should succeed");
            let endpoint = Endpoint {
                url: url.clone(),
                is_local: false,
                pool_idx: 0,
                set_idx: 0,
                disk_idx: 0,
            };

            let disk_option = DiskOption {
                cleanup: false,
                health_check: false,
            };

            let remote_disk = RemoteDisk::new(&endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
                .await
                .expect("operation should succeed");

            assert!(!remote_disk.is_local());
            assert_eq!(remote_disk.host_name(), expected_hostname);
            // Note: to_string() might not contain the exact hostname format
            assert!(!remote_disk.to_string().is_empty());
        }
    }

    #[tokio::test]
    async fn test_remote_disk_location_validation() {
        // Test valid location
        let url = url::Url::parse("http://server:9000").expect("operation should succeed");
        let valid_endpoint = Endpoint {
            url: url.clone(),
            is_local: false,
            pool_idx: 0,
            set_idx: 1,
            disk_idx: 2,
        };

        let disk_option = DiskOption {
            cleanup: false,
            health_check: false,
        };

        let remote_disk = RemoteDisk::new(&valid_endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
            .await
            .expect("operation should succeed");
        let location = remote_disk.get_disk_location();
        assert!(location.valid());
        assert_eq!(location.pool_idx, Some(0));
        assert_eq!(location.set_idx, Some(1));
        assert_eq!(location.disk_idx, Some(2));

        // Test invalid location (negative indices)
        let invalid_endpoint = Endpoint {
            url: url.clone(),
            is_local: false,
            pool_idx: -1,
            set_idx: -1,
            disk_idx: -1,
        };

        let remote_disk_invalid = RemoteDisk::new(&invalid_endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
            .await
            .expect("operation should succeed");
        let invalid_location = remote_disk_invalid.get_disk_location();
        assert!(!invalid_location.valid());
        assert_eq!(invalid_location.pool_idx, None);
        assert_eq!(invalid_location.set_idx, None);
        assert_eq!(invalid_location.disk_idx, None);
    }

    #[tokio::test]
    async fn test_remote_disk_close() {
        let url = url::Url::parse("http://server:9000").expect("operation should succeed");
        let endpoint = Endpoint {
            url: url.clone(),
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let disk_option = DiskOption {
            cleanup: false,
            health_check: false,
        };

        let remote_disk = RemoteDisk::new(&endpoint, &disk_option, Arc::new(TcpHttpInternodeDataTransport))
            .await
            .expect("operation should succeed");

        // Test close operation (should succeed)
        let result = remote_disk.close().await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_execute_with_timeout_marks_remote_disk_faulty() {
        let url = url::Url::parse("http://remote-timeout:9000").expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: false,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("operation should succeed");

        let err = remote_disk
            .execute_with_timeout(
                || async {
                    tokio::time::sleep(Duration::from_millis(50)).await;
                    Ok::<(), Error>(())
                },
                Duration::from_millis(10),
            )
            .await
            .expect_err("timeout should fail");

        assert!(err.to_string().contains("timeout"));
        assert!(remote_disk.is_online().await, "first timeout should keep the remote disk online");
        assert_eq!(
            remote_disk.runtime_state(),
            RuntimeDriveHealthState::Suspect,
            "first timeout should move the remote disk into suspect state"
        );
    }

    #[tokio::test]
    async fn test_execute_with_timeout_can_ignore_remote_timeout_failure() {
        let url = url::Url::parse("http://remote-timeout-ignored:9000").expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: false,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("operation should succeed");

        let err = remote_disk
            .execute_with_timeout_for_op_and_health_action(
                "walk_dir",
                || async {
                    tokio::time::sleep(Duration::from_millis(50)).await;
                    Ok::<(), Error>(())
                },
                Duration::from_millis(10),
                FailureHealthAction::IgnoreFailure,
            )
            .await
            .expect_err("timeout should fail");

        assert!(err.to_string().contains("timeout"));
        assert!(remote_disk.is_online().await, "ignored timeout should not mark remote disk faulty");
    }

    #[tokio::test]
    async fn test_execute_with_timeout_zero_duration_waits_for_operation() {
        let url = url::Url::parse("http://remote-no-timeout:9000").expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: false,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("operation should succeed");

        remote_disk
            .execute_with_timeout(
                || async {
                    tokio::time::sleep(Duration::from_millis(10)).await;
                    Ok::<(), Error>(())
                },
                Duration::ZERO,
            )
            .await
            .expect("zero duration should disable the operation timeout");

        assert!(
            remote_disk.is_online().await,
            "successful no-timeout operation should keep remote disk online"
        );
    }

    #[tokio::test]
    async fn test_execute_with_timeout_evicts_cached_connection() {
        let addr = "http://127.0.0.1:59991".to_string();
        let url = url::Url::parse(&format!("{addr}/data")).expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: false,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("operation should succeed");

        let channel = TonicEndpoint::from_shared(addr.clone())
            .expect("operation should succeed")
            .connect_lazy();
        runtime_sources::cache_test_node_channel(addr.clone(), channel).await;
        assert!(runtime_sources::test_node_channel_is_cached(&addr).await);

        let _ = remote_disk
            .execute_with_timeout(
                || async {
                    tokio::time::sleep(Duration::from_millis(50)).await;
                    Ok::<(), Error>(())
                },
                Duration::from_millis(10),
            )
            .await
            .expect_err("timeout should fail");

        assert!(
            !runtime_sources::test_node_channel_is_cached(&addr).await,
            "timeout should evict cached connection"
        );
    }

    #[tokio::test]
    async fn test_execute_with_timeout_marks_faulty_on_timeout_like_error() {
        let addr = "http://127.0.0.1:59992".to_string();
        let url = url::Url::parse(&format!("{addr}/data")).expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: false,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("operation should succeed");

        let channel = TonicEndpoint::from_shared(addr.clone())
            .expect("operation should succeed")
            .connect_lazy();
        runtime_sources::cache_test_node_channel(addr.clone(), channel).await;

        let err = remote_disk
            .execute_with_timeout(
                || async { Err::<(), Error>(DiskError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, "stall timeout"))) },
                Duration::from_secs(1),
            )
            .await
            .expect_err("timeout-like operation error should fail");

        assert_eq!(
            match &err {
                DiskError::Io(io_err) => io_err.kind(),
                other => panic!("expected io timeout error, got {other:?}"),
            },
            std::io::ErrorKind::TimedOut
        );
        assert!(
            remote_disk.is_online().await,
            "first timeout-like error should keep the remote disk online"
        );
        assert_eq!(
            remote_disk.runtime_state(),
            RuntimeDriveHealthState::Suspect,
            "first timeout-like error should move the remote disk into suspect state"
        );
        assert!(
            !runtime_sources::test_node_channel_is_cached(&addr).await,
            "timeout-like errors should evict cached connection"
        );
    }

    #[tokio::test]
    async fn test_execute_with_timeout_marks_faulty_on_network_like_error() {
        let addr = "http://127.0.0.1:59993".to_string();
        let url = url::Url::parse(&format!("{addr}/data")).expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: false,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("operation should succeed");

        let channel = TonicEndpoint::from_shared(addr.clone())
            .expect("operation should succeed")
            .connect_lazy();
        runtime_sources::cache_test_node_channel(addr.clone(), channel).await;

        let err = remote_disk
            .execute_with_timeout(
                || async {
                    Err::<(), Error>(DiskError::Io(std::io::Error::new(
                        std::io::ErrorKind::ConnectionRefused,
                        "connection refused",
                    )))
                },
                Duration::from_secs(1),
            )
            .await
            .expect_err("network-like operation error should fail");

        assert_eq!(
            match &err {
                DiskError::Io(io_err) => io_err.kind(),
                other => panic!("expected io network error, got {other:?}"),
            },
            std::io::ErrorKind::ConnectionRefused
        );
        assert!(
            remote_disk.is_online().await,
            "first network-like error should keep the remote disk online"
        );
        assert_eq!(
            remote_disk.runtime_state(),
            RuntimeDriveHealthState::Suspect,
            "first network-like error should move the remote disk into suspect state"
        );
        assert!(
            !runtime_sources::test_node_channel_is_cached(&addr).await,
            "network-like errors should evict cached connection"
        );
    }

    #[tokio::test]
    async fn test_execute_with_timeout_can_ignore_network_like_error() {
        let addr = "http://127.0.0.1:59995".to_string();
        let url = url::Url::parse(&format!("{addr}/data")).expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: false,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("operation should succeed");

        let channel = TonicEndpoint::from_shared(addr.clone())
            .expect("operation should succeed")
            .connect_lazy();
        runtime_sources::cache_test_node_channel(addr.clone(), channel).await;

        let err = remote_disk
            .execute_with_timeout_for_op_and_health_action(
                "walk_dir",
                || async { Err::<(), Error>(DiskError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, "stall timeout"))) },
                Duration::from_secs(1),
                FailureHealthAction::IgnoreFailure,
            )
            .await
            .expect_err("timeout-like operation error should fail");

        assert_eq!(
            match &err {
                DiskError::Io(io_err) => io_err.kind(),
                other => panic!("expected io timeout error, got {other:?}"),
            },
            std::io::ErrorKind::TimedOut
        );
        assert!(
            remote_disk.is_online().await,
            "ignored network-like error should not mark remote disk faulty"
        );
        assert!(
            runtime_sources::test_node_channel_is_cached(&addr).await,
            "ignored network-like error should not evict cached connection"
        );
    }

    #[tokio::test]
    async fn test_execute_with_timeout_keeps_remote_disk_online_for_business_error() {
        let addr = "http://127.0.0.1:59994".to_string();
        let url = url::Url::parse(&format!("{addr}/data")).expect("operation should succeed");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: false,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("operation should succeed");

        let channel = TonicEndpoint::from_shared(addr.clone())
            .expect("operation should succeed")
            .connect_lazy();
        runtime_sources::cache_test_node_channel(addr.clone(), channel).await;

        let err = remote_disk
            .execute_with_timeout(|| async { Err::<(), Error>(DiskError::FileNotFound) }, Duration::from_secs(1))
            .await
            .expect_err("business error should still fail the operation");

        assert_eq!(err, DiskError::FileNotFound);
        assert!(remote_disk.is_online().await, "business errors should not mark remote disk faulty");
        assert!(
            runtime_sources::test_node_channel_is_cached(&addr).await,
            "business errors should not evict cached connection"
        );
    }

    #[test]
    fn test_remote_disk_sync_properties() {
        let url = url::Url::parse("https://secure-remote:9000/data").expect("operation should succeed");
        let endpoint = Endpoint {
            url: url.clone(),
            is_local: false,
            pool_idx: 1,
            set_idx: 2,
            disk_idx: 3,
        };

        // Test endpoint method - we can't test this without creating RemoteDisk instance
        // but we can test that the endpoint contains expected values
        assert_eq!(endpoint.url, url);
        assert!(!endpoint.is_local);
        assert_eq!(endpoint.pool_idx, 1);
        assert_eq!(endpoint.set_idx, 2);
        assert_eq!(endpoint.disk_idx, 3);
    }

    #[tokio::test(flavor = "current_thread")]
    async fn remote_disk_recovery_probe_logs_keep_request_id_span_context() {
        let logs = CapturedLogs::default();
        let subscriber = Registry::default().with(
            tracing_subscriber::fmt::layer()
                .with_writer(logs.clone())
                .with_ansi(false)
                .without_time()
                .json()
                .flatten_event(true)
                .with_current_span(true)
                .with_span_list(true),
        );
        let _guard = tracing::subscriber::set_default(subscriber);
        // The `recovery-monitor` span and the monitor's own log events are
        // production callsites that sibling tests exercise from subscriber-less
        // threads; without this they can be cached as `Interest::never()` and go
        // silently missing here.
        let _callsite_pin = crate::test_tracing::pin_callsite_interest_for_test();

        let endpoint = Endpoint {
            url: url::Url::parse("http://127.0.0.1:59996/data").expect("endpoint URL should parse"),
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };
        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: true,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("remote disk should construct");

        let span = tracing::info_span!("request-span", request_id = "req-remote-disk");
        let _entered = span.enter();
        let done = remote_disk.spawn_recovery_monitor_log_probe_for_test();
        done.await
            .expect("remote disk recovery monitor probe should signal completion");

        let log = logs
            .lines()
            .into_iter()
            .find(|value| value.get("message").and_then(Value::as_str) == Some("remote disk recovery monitor log probe"))
            .expect("expected remote disk recovery monitor probe log");

        assert_eq!(log["span"]["name"], Value::String("recovery-monitor".to_string()));
        assert_eq!(log["span"]["kind"], Value::String("remote_disk".to_string()));
        let spans = log["spans"].as_array().expect("spans should be present");
        assert!(spans.iter().any(|span| {
            span.get("name").and_then(Value::as_str) == Some("request-span")
                && span.get("request_id").and_then(Value::as_str) == Some("req-remote-disk")
        }));
    }

    #[tokio::test(flavor = "current_thread")]
    async fn remote_disk_network_error_starts_recovery_monitor_with_request_context() {
        let logs = CapturedLogs::default();
        let subscriber = Registry::default().with(
            tracing_subscriber::fmt::layer()
                .with_writer(logs.clone())
                .with_ansi(false)
                .without_time()
                .json()
                .flatten_event(true)
                .with_current_span(true)
                .with_span_list(true),
        );
        let _guard = tracing::subscriber::set_default(subscriber);
        // The `recovery-monitor` span and the monitor's own log events are
        // production callsites that sibling tests exercise from subscriber-less
        // threads; without this they can be cached as `Interest::never()` and go
        // silently missing here.
        let _callsite_pin = crate::test_tracing::pin_callsite_interest_for_test();

        let addr = "http://127.0.0.1:59997".to_string();
        let endpoint = Endpoint {
            url: url::Url::parse(&format!("{addr}/data")).expect("endpoint URL should parse"),
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };

        let remote_disk = RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: true,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("remote disk should construct");

        let span = tracing::info_span!("request-span", request_id = "req-remote-disk-e2e");
        let _entered = span.enter();

        let err = remote_disk
            .execute_with_timeout(
                || async {
                    Err::<(), Error>(DiskError::Io(std::io::Error::new(
                        std::io::ErrorKind::ConnectionRefused,
                        "connection refused",
                    )))
                },
                Duration::from_secs(1),
            )
            .await
            .expect_err("network-like operation error should fail");
        assert_eq!(
            match &err {
                DiskError::Io(io_err) => io_err.kind(),
                other => panic!("expected io network error, got {other:?}"),
            },
            std::io::ErrorKind::ConnectionRefused
        );

        tokio::task::yield_now().await;
        tokio::time::sleep(Duration::from_millis(20)).await;
        remote_disk.cancel_token.cancel();
        tokio::task::yield_now().await;

        let lines = logs.lines();
        let marked_suspect = lines
            .iter()
            .find(|value| value.get("state").and_then(Value::as_str) == Some("marked_suspect"))
            .expect("expected marked_suspect log");
        assert!(
            marked_suspect["spans"]
                .as_array()
                .expect("spans should be present")
                .iter()
                .any(|span| {
                    span.get("name").and_then(Value::as_str) == Some("request-span")
                        && span.get("request_id").and_then(Value::as_str) == Some("req-remote-disk-e2e")
                })
        );

        let recovery_started = lines
            .iter()
            .find(|value| value.get("state").and_then(Value::as_str) == Some("recovery_monitor_started"))
            .expect("expected recovery_monitor_started log");
        assert_eq!(recovery_started["span"]["name"], Value::String("recovery-monitor".to_string()));
        assert_eq!(recovery_started["span"]["kind"], Value::String("remote_disk".to_string()));
        assert!(
            recovery_started["spans"]
                .as_array()
                .expect("spans should be present")
                .iter()
                .any(|span| {
                    span.get("name").and_then(Value::as_str) == Some("request-span")
                        && span.get("request_id").and_then(Value::as_str) == Some("req-remote-disk-e2e")
                })
        );
    }

    /// Peer that completes the TCP connect and then goes silent, so every RPC issued over the
    /// cached lazy channel stays pending until the caller's own deadline fires.
    async fn spawn_stalled_grpc_peer() -> Option<(String, tokio::task::JoinHandle<()>)> {
        let listener = match TcpListener::bind("127.0.0.1:0").await {
            Ok(listener) => listener,
            Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return None,
            Err(err) => panic!("test listener should bind: {err}"),
        };
        let addr = listener.local_addr().expect("listener local address should be available");
        let accept_task = tokio::spawn(async move {
            let mut accepted = Vec::new();
            while let Ok((stream, _)) = listener.accept().await {
                accepted.push(stream);
            }
        });

        let base_addr = format!("http://{}:{}", addr.ip(), addr.port());
        let channel = TonicEndpoint::from_shared(base_addr.clone())
            .expect("stalled peer endpoint should parse")
            .connect_lazy();
        runtime_sources::cache_test_node_channel(base_addr.clone(), channel).await;
        Some((base_addr, accept_task))
    }

    async fn remote_disk_for_addr(base_addr: &str) -> RemoteDisk {
        let url = url::Url::parse(&format!("{base_addr}/data/rustfs0")).expect("endpoint url should parse");
        let endpoint = Endpoint {
            url,
            is_local: false,
            pool_idx: 0,
            set_idx: 0,
            disk_idx: 0,
        };
        RemoteDisk::new(
            &endpoint,
            &DiskOption {
                cleanup: false,
                health_check: false,
            },
            Arc::new(TcpHttpInternodeDataTransport),
        )
        .await
        .expect("remote disk should construct")
    }

    #[tokio::test]
    async fn list_volumes_bounds_the_wait_on_a_stalled_peer() {
        runtime_sources::ensure_test_rpc_secret();
        let Some((base_addr, accept_task)) = spawn_stalled_grpc_peer().await else {
            return;
        };
        let remote_disk = remote_disk_for_addr(&base_addr).await;

        temp_env::async_with_vars([(rustfs_config::ENV_DRIVE_MAX_TIMEOUT_DURATION, Some("1"))], async {
            let err = tokio::time::timeout(Duration::from_secs(10), remote_disk.list_volumes())
                .await
                .expect("list_volumes must bound the wait on a stalled peer")
                .expect_err("a stalled peer must fail list_volumes");
            assert!(matches!(err, DiskError::Timeout), "expected the operation deadline to fire, got {err:?}");
        })
        .await;

        accept_task.abort();
    }

    #[tokio::test]
    async fn delete_volume_bounds_the_wait_on_a_stalled_peer() {
        runtime_sources::ensure_test_rpc_secret();
        let Some((base_addr, accept_task)) = spawn_stalled_grpc_peer().await else {
            return;
        };
        let remote_disk = remote_disk_for_addr(&base_addr).await;

        temp_env::async_with_vars([(rustfs_config::ENV_DRIVE_MAX_TIMEOUT_DURATION, Some("1"))], async {
            let err = tokio::time::timeout(Duration::from_secs(10), remote_disk.delete_volume("bucket", false))
                .await
                .expect("delete_volume must bound the wait on a stalled peer")
                .expect_err("a stalled peer must fail delete_volume");
            assert!(matches!(err, DiskError::Timeout), "expected the operation deadline to fire, got {err:?}");
        })
        .await;

        accept_task.abort();
    }
}
