mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 11:32:19 +00:00
eb41f45175
Removing both blankets exposes 23 items, of which only four are deleted. The ratio is the point: close to the core data path the blankets were hiding test assertions and migration seams, not dead code. A cfg-split function is the reason two symbols in the internode transport look dead when neither is. build_internode_data_transport_from_env has two bodies, one under #[cfg(test)] that calls build_internode_data_transport directly and one under #[cfg(not(test))] that goes through the INTERNODE_DATA_TRANSPORT static so tests do not share process-global transport state. Each half's helper is live in exactly one build, and because cargo check --tests compiles both the lib target and the test harness, both symbols appear in one warning list. Deleting either one breaks the other lane. Both are kept with allows naming their half. Three deletion candidates were withdrawn after a per-name grep: ParallelReader::new, ErasureDecodeReader::new and SyncErasureDecodeReader::new all have test callers. The last two are exactly the shape of the dead wrapper deleted in #6084 — a thin forward to a new_with_metrics_path sibling — except that sibling is live in production (set_disk/read.rs) and the wrappers are used by tests. Deleted: - RemotePeerS3Client::get_addr and RemoteLocker::from_url, neither with a consumer in any lane. - RemotePeerS3Client's node field, which new writes after using it to derive addr and nothing ever reads. Its only other writer was a test helper that built a whole Node solely to fill the field; that block goes too. - ParallelReader::can_decode, superseded by an inlined copy. The copy's comment named the method it replaced, so deleting the method alone would have left a dangling reference; the comment now describes the check instead of pointing at a method that no longer exists. Kept with allows: the erasure items are decode/encode invariants asserted by their own files' tests (shard_read_launch_order, decode_with_read_costs, emit_data_shards, queued_block_bytes, the engine trait facets, the ParallelReader and decode-reader constructors, encode_stream_callback_async). On the cluster side, peer_replay_state, heal_bucket_local and clone_drives are test-only, InternodeDataTransportCapabilities and tcp_http are constructed only by transport test doubles, and the InternodeDataTransport trait's name/capabilities pair is an unused capability-negotiation facet kept for the transport split (backlog#1350) — six impls provide them and no caller negotiates on them yet. Verification, four lanes warning-free: default, --tests, --features rio-v2 --tests, --features test-util --tests. cargo nextest run -p rustfs-ecstore 4041 passed; clippy --lib --tests -D warnings clean; make pre-commit exit 0. Ref rustfs/backlog#1823 (step 2).
910 lines
37 KiB
Rust
910 lines
37 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use crate::cluster::rpc::client::{
|
|
AuthenticatedChannel, TonicInterceptor, gen_tonic_signature_interceptor, node_service_time_out_client,
|
|
};
|
|
use crate::cluster::rpc::set_tonic_rolling_mutation_body_digest;
|
|
use async_trait::async_trait;
|
|
use bytes::Bytes;
|
|
use rustfs_lock::{
|
|
LockClient, LockError, LockInfo, LockRequest, LockResponse, LockStats, LockStatus, LockType, Result,
|
|
types::{LockId, LockMetadata, LockPriority},
|
|
};
|
|
use rustfs_protos::proto_gen::node_service::{BatchGenerallyLockRequest, GenerallyLockRequest, PingRequest};
|
|
use rustfs_protos::{
|
|
ConnectionEvictionLogLevel, evict_failed_connection_with_log_level, models::PingBodyBuilder,
|
|
proto_gen::node_service::node_service_client::NodeServiceClient,
|
|
};
|
|
use std::time::Duration;
|
|
use tokio::time::timeout;
|
|
use tonic::Request;
|
|
use tonic::service::interceptor::InterceptedService;
|
|
use tracing::{debug, info, warn};
|
|
|
|
fn attach_lock_mutation_body_digest<T: rustfs_protos::CanonicalMutationBody>(request: &mut Request<T>) -> std::io::Result<()> {
|
|
set_tonic_rolling_mutation_body_digest(request)
|
|
}
|
|
|
|
/// Remote lock client implementation
|
|
#[derive(Debug, Clone)]
|
|
pub struct RemoteClient {
|
|
addr: String,
|
|
}
|
|
|
|
impl RemoteClient {
|
|
pub fn new(endpoint: String) -> Self {
|
|
Self { addr: endpoint }
|
|
}
|
|
|
|
fn build_ping_request() -> PingRequest {
|
|
let mut fbb = flatbuffers::FlatBufferBuilder::new();
|
|
let payload = fbb.create_vector(b"health-check");
|
|
let mut builder = PingBodyBuilder::new(&mut fbb);
|
|
builder.add_payload(payload);
|
|
let root = builder.finish();
|
|
fbb.finish(root, None);
|
|
|
|
PingRequest {
|
|
version: 1,
|
|
body: Bytes::copy_from_slice(fbb.finished_data()),
|
|
}
|
|
}
|
|
|
|
/// Create a minimal LockRequest for unlock operations using only lock_id
|
|
fn create_unlock_request(lock_id: &LockId) -> LockRequest {
|
|
LockRequest {
|
|
lock_id: lock_id.clone(),
|
|
resource: lock_id.resource.clone(),
|
|
lock_type: LockType::Exclusive, // Type doesn't matter for unlock
|
|
owner: String::new(), // Owner not needed, server uses lock_id
|
|
acquire_timeout: std::time::Duration::from_secs(30),
|
|
ttl: std::time::Duration::from_secs(300),
|
|
metadata: LockMetadata::default(),
|
|
priority: LockPriority::Normal,
|
|
deadlock_detection: false,
|
|
suppress_contention_logs: false,
|
|
refresh_interval: None,
|
|
}
|
|
}
|
|
|
|
pub async fn get_client(&self) -> Result<NodeServiceClient<InterceptedService<AuthenticatedChannel, TonicInterceptor>>> {
|
|
// P3-2 offline bypass (now covering the lock path too): fast-fail a peer already marked
|
|
// offline instead of paying the connect timeout, so dsync reaches quorum sooner. Does not
|
|
// change quorum; the self-healing re-probe keeps the peer recoverable.
|
|
if let Some(reason) = crate::cluster::rpc::remote_disk::internode_offline_bypass_reason(&self.addr) {
|
|
return Err(LockError::internal(reason));
|
|
}
|
|
node_service_time_out_client(&self.addr, TonicInterceptor::Signature(gen_tonic_signature_interceptor()))
|
|
.await
|
|
.map_err(|err| LockError::internal(format!("can not get client, err: {err}")))
|
|
}
|
|
|
|
fn is_scanner_leader_lock(resource_summary: &str) -> bool {
|
|
resource_summary == ".rustfs.sys/leader.lock@latest"
|
|
}
|
|
|
|
/// Classify a `tonic::Status` as a transport-level failure of the cached channel.
|
|
///
|
|
/// Only genuine connection problems (connect refused/reset, keepalive/deadline
|
|
/// on the channel itself, cancelled in-flight streams) justify evicting and
|
|
/// re-dialing the cached channel. A broken transport surfaces either as one of
|
|
/// the codes below or carries the underlying hyper/h2 error as its `source`.
|
|
///
|
|
/// Server-produced application statuses (`Unauthenticated`/`PermissionDenied`
|
|
/// from the signature interceptor, `Internal`/`FailedPrecondition` when the
|
|
/// peer's lock service is not ready yet, `InvalidArgument`, `ResourceExhausted`,
|
|
/// `Unimplemented`, ...) are reconstructed from grpc-status trailers on the
|
|
/// client and have no `source`. Evicting the channel for those cannot help —
|
|
/// the channel is healthy — and only churns the connection while advancing the
|
|
/// peer toward the offline threshold. See issue #4567.
|
|
fn is_transport_failure(status: &tonic::Status) -> bool {
|
|
use tonic::Code;
|
|
std::error::Error::source(status).is_some()
|
|
|| matches!(
|
|
status.code(),
|
|
Code::Unavailable | Code::DeadlineExceeded | Code::Unknown | Code::Cancelled
|
|
)
|
|
}
|
|
|
|
async fn evict_connection(&self, op: &'static str, reason: &str, resource_summary: &str) {
|
|
let log_level = if Self::is_scanner_leader_lock(resource_summary) {
|
|
debug!(
|
|
addr = %self.addr,
|
|
op,
|
|
reason,
|
|
resource_summary,
|
|
"Evicting cached remote lock connection for scanner leader-lock RPC failure"
|
|
);
|
|
ConnectionEvictionLogLevel::Debug
|
|
} else {
|
|
warn!(
|
|
addr = %self.addr,
|
|
op,
|
|
reason,
|
|
resource_summary,
|
|
"Evicting cached remote lock connection after RPC failure"
|
|
);
|
|
ConnectionEvictionLogLevel::Warn
|
|
};
|
|
evict_failed_connection_with_log_level(&self.addr, log_level).await;
|
|
}
|
|
|
|
fn summarize_resources(requests: &[LockRequest]) -> String {
|
|
const LIMIT: usize = 3;
|
|
let mut resources = requests
|
|
.iter()
|
|
.take(LIMIT)
|
|
.map(|request| request.resource.to_string())
|
|
.collect::<Vec<_>>();
|
|
if requests.len() > LIMIT {
|
|
resources.push(format!("... (+{} more)", requests.len() - LIMIT));
|
|
}
|
|
resources.join(", ")
|
|
}
|
|
|
|
fn rpc_timeout() -> Duration {
|
|
Duration::from_millis(
|
|
rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_LOCK_RPC_TIMEOUT_MS,
|
|
rustfs_config::DEFAULT_OBJECT_LOCK_RPC_TIMEOUT_MS,
|
|
)
|
|
.max(1),
|
|
)
|
|
}
|
|
|
|
async fn execute_rpc<T, F>(&self, op: &'static str, resource_summary: &str, future: F) -> std::result::Result<T, LockError>
|
|
where
|
|
F: std::future::Future<Output = std::result::Result<T, tonic::Status>>,
|
|
{
|
|
let lock_timeout = Self::rpc_timeout();
|
|
match timeout(lock_timeout, future).await {
|
|
Ok(Ok(response)) => Ok(response),
|
|
Ok(Err(err)) => {
|
|
let reason = err.to_string();
|
|
// Only evict (and re-dial) the cached channel when the failure is a genuine
|
|
// transport problem. A server-produced application status (auth denied, peer
|
|
// lock service not ready, invalid args, ...) arrives on a perfectly healthy
|
|
// channel; evicting it just churns the connection and pushes the peer toward
|
|
// the offline threshold for no benefit. See issue #4567.
|
|
let transport_failure = Self::is_transport_failure(&err);
|
|
if Self::is_scanner_leader_lock(resource_summary) {
|
|
debug!(
|
|
addr = %self.addr,
|
|
op,
|
|
timeout_ms = lock_timeout.as_millis(),
|
|
resource_summary,
|
|
tonic_code = ?err.code(),
|
|
tonic_message = err.message(),
|
|
transport_failure,
|
|
"Remote lock RPC returned tonic error for scanner leader lock"
|
|
);
|
|
} else {
|
|
warn!(
|
|
addr = %self.addr,
|
|
op,
|
|
timeout_ms = lock_timeout.as_millis(),
|
|
resource_summary,
|
|
tonic_code = ?err.code(),
|
|
tonic_message = err.message(),
|
|
transport_failure,
|
|
"Remote lock RPC returned tonic error"
|
|
);
|
|
}
|
|
if transport_failure {
|
|
self.evict_connection(op, &reason, resource_summary).await;
|
|
}
|
|
Err(LockError::internal(format!("{op} RPC failed: {reason}")))
|
|
}
|
|
Err(_) => {
|
|
let reason = format!("RPC timed out after {:?}", lock_timeout);
|
|
if Self::is_scanner_leader_lock(resource_summary) {
|
|
debug!(
|
|
addr = %self.addr,
|
|
op,
|
|
timeout_ms = lock_timeout.as_millis(),
|
|
resource_summary,
|
|
"Remote lock RPC timed out for scanner leader lock"
|
|
);
|
|
} else {
|
|
warn!(
|
|
addr = %self.addr,
|
|
op,
|
|
timeout_ms = lock_timeout.as_millis(),
|
|
resource_summary,
|
|
"Remote lock RPC timed out"
|
|
);
|
|
}
|
|
self.evict_connection(op, &reason, resource_summary).await;
|
|
Err(LockError::timeout(format!("remote lock RPC {op} on {}", self.addr), lock_timeout))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn rpc_timeout_failure_response(request: &LockRequest, err: &LockError) -> LockResponse {
|
|
LockResponse::failure(format!("Remote lock RPC timed out: {err}"), request.acquire_timeout)
|
|
}
|
|
|
|
fn rpc_failure_response(_request: &LockRequest, err: &LockError) -> LockResponse {
|
|
LockResponse::failure(format!("Remote lock RPC failed: {err}"), Duration::ZERO)
|
|
}
|
|
|
|
fn rpc_failure_batch(requests: &[LockRequest], err: &LockError) -> Vec<LockResponse> {
|
|
requests
|
|
.iter()
|
|
.map(|request| Self::rpc_failure_response(request, err))
|
|
.collect()
|
|
}
|
|
|
|
fn rpc_timeout_failure_batch(requests: &[LockRequest], err: &LockError) -> Vec<LockResponse> {
|
|
requests
|
|
.iter()
|
|
.map(|request| Self::rpc_timeout_failure_response(request, err))
|
|
.collect()
|
|
}
|
|
|
|
fn build_lock_info(request: &LockRequest, lock_info_json: Option<String>) -> LockInfo {
|
|
if let Some(lock_info_json) = lock_info_json {
|
|
match serde_json::from_str::<LockInfo>(&lock_info_json) {
|
|
Ok(info) => info,
|
|
Err(e) => {
|
|
warn!("Failed to deserialize lock_info from response: {}, using request data", e);
|
|
LockInfo {
|
|
id: request.lock_id.clone(),
|
|
resource: request.resource.clone(),
|
|
lock_type: request.lock_type,
|
|
status: LockStatus::Acquired,
|
|
owner: request.owner.clone(),
|
|
acquired_at: std::time::SystemTime::now(),
|
|
expires_at: std::time::SystemTime::now() + request.ttl,
|
|
last_refreshed: std::time::SystemTime::now(),
|
|
metadata: request.metadata.clone(),
|
|
priority: request.priority,
|
|
wait_start_time: None,
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
LockInfo {
|
|
id: request.lock_id.clone(),
|
|
resource: request.resource.clone(),
|
|
lock_type: request.lock_type,
|
|
status: LockStatus::Acquired,
|
|
owner: request.owner.clone(),
|
|
acquired_at: std::time::SystemTime::now(),
|
|
expires_at: std::time::SystemTime::now() + request.ttl,
|
|
last_refreshed: std::time::SystemTime::now(),
|
|
metadata: request.metadata.clone(),
|
|
priority: request.priority,
|
|
wait_start_time: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unknown_lock_info(lock_id: &LockId) -> LockInfo {
|
|
LockInfo {
|
|
id: lock_id.clone(),
|
|
resource: lock_id.resource.clone(),
|
|
lock_type: LockType::Exclusive,
|
|
status: LockStatus::Acquired,
|
|
owner: "unknown".to_string(),
|
|
acquired_at: std::time::SystemTime::now(),
|
|
expires_at: std::time::SystemTime::now() + std::time::Duration::from_secs(3600),
|
|
last_refreshed: std::time::SystemTime::now(),
|
|
metadata: LockMetadata::default(),
|
|
priority: LockPriority::Normal,
|
|
wait_start_time: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl LockClient for RemoteClient {
|
|
async fn acquire_lock(&self, request: &LockRequest) -> Result<LockResponse> {
|
|
info!("remote acquire_exclusive for {}", request.resource);
|
|
let mut client = self.get_client().await?;
|
|
let resource_summary = request.resource.to_string();
|
|
let mut req = Request::new(GenerallyLockRequest {
|
|
args: serde_json::to_string(&request)
|
|
.map_err(|e| LockError::internal(format!("Failed to serialize request: {e}")))?,
|
|
});
|
|
attach_lock_mutation_body_digest(&mut req)?;
|
|
|
|
let resp = match self.execute_rpc("lock", &resource_summary, client.lock(req)).await {
|
|
Ok(resp) => resp.into_inner(),
|
|
Err(err @ LockError::Timeout { .. }) => return Ok(Self::rpc_timeout_failure_response(request, &err)),
|
|
Err(err) => return Ok(Self::rpc_failure_response(request, &err)),
|
|
};
|
|
|
|
// Check if the lock acquisition was successful
|
|
if resp.success {
|
|
Ok(LockResponse::success(
|
|
Self::build_lock_info(request, resp.lock_info),
|
|
std::time::Duration::ZERO,
|
|
))
|
|
} else {
|
|
// Lock acquisition failed
|
|
Ok(LockResponse::failure(
|
|
resp.error_info
|
|
.unwrap_or_else(|| "Lock acquisition failed on remote server".to_string()),
|
|
std::time::Duration::ZERO,
|
|
))
|
|
}
|
|
}
|
|
|
|
async fn acquire_locks_batch(&self, requests: &[LockRequest]) -> Result<Vec<LockResponse>> {
|
|
if requests.is_empty() {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let mut client = self.get_client().await?;
|
|
let resource_summary = Self::summarize_resources(requests);
|
|
let mut req = Request::new(BatchGenerallyLockRequest {
|
|
args: requests
|
|
.iter()
|
|
.map(|request| {
|
|
serde_json::to_string(request).map_err(|e| LockError::internal(format!("Failed to serialize request: {e}")))
|
|
})
|
|
.collect::<Result<Vec<_>>>()?,
|
|
});
|
|
attach_lock_mutation_body_digest(&mut req)?;
|
|
|
|
let resp = match self
|
|
.execute_rpc("lock_batch", &resource_summary, client.lock_batch(req))
|
|
.await
|
|
{
|
|
Ok(resp) => resp.into_inner(),
|
|
Err(err @ LockError::Timeout { .. }) => return Ok(Self::rpc_timeout_failure_batch(requests, &err)),
|
|
Err(err) => return Ok(Self::rpc_failure_batch(requests, &err)),
|
|
};
|
|
|
|
Ok(requests
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, request)| match resp.results.get(idx) {
|
|
Some(result) if result.success => {
|
|
LockResponse::success(Self::build_lock_info(request, result.lock_info.clone()), std::time::Duration::ZERO)
|
|
}
|
|
Some(result) => LockResponse::failure(
|
|
result
|
|
.error_info
|
|
.clone()
|
|
.unwrap_or_else(|| "Lock acquisition failed on remote server".to_string()),
|
|
std::time::Duration::ZERO,
|
|
),
|
|
None => LockResponse::failure(
|
|
format!("Lock batch response missing entry for request index {idx}"),
|
|
std::time::Duration::ZERO,
|
|
),
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
async fn release(&self, lock_id: &LockId) -> Result<bool> {
|
|
info!("remote release for {}", lock_id);
|
|
|
|
let unlock_request = Self::create_unlock_request(lock_id);
|
|
let request_string = serde_json::to_string(&unlock_request)
|
|
.map_err(|e| LockError::internal(format!("Failed to serialize request: {e}")))?;
|
|
let mut client = self.get_client().await?;
|
|
let resource_summary = unlock_request.resource.to_string();
|
|
let mut req = Request::new(GenerallyLockRequest { args: request_string });
|
|
attach_lock_mutation_body_digest(&mut req)?;
|
|
let resp = self
|
|
.execute_rpc("release", &resource_summary, client.un_lock(req))
|
|
.await?
|
|
.into_inner();
|
|
if let Some(error_info) = resp.error_info {
|
|
return Err(LockError::internal(error_info));
|
|
}
|
|
Ok(resp.success)
|
|
}
|
|
|
|
async fn release_locks_batch(&self, lock_ids: &[LockId]) -> Result<Vec<bool>> {
|
|
if lock_ids.is_empty() {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let unlock_requests = lock_ids.iter().map(Self::create_unlock_request).collect::<Vec<_>>();
|
|
let mut client = self.get_client().await?;
|
|
let resource_summary = Self::summarize_resources(&unlock_requests);
|
|
let mut req = Request::new(BatchGenerallyLockRequest {
|
|
args: unlock_requests
|
|
.iter()
|
|
.map(|request| {
|
|
serde_json::to_string(request).map_err(|e| LockError::internal(format!("Failed to serialize request: {e}")))
|
|
})
|
|
.collect::<Result<Vec<_>>>()?,
|
|
});
|
|
attach_lock_mutation_body_digest(&mut req)?;
|
|
|
|
let resp = self
|
|
.execute_rpc("release_batch", &resource_summary, client.un_lock_batch(req))
|
|
.await?
|
|
.into_inner();
|
|
|
|
Ok(lock_ids
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, _)| resp.results.get(idx).map(|result| result.success).unwrap_or(false))
|
|
.collect())
|
|
}
|
|
|
|
async fn refresh(&self, lock_id: &LockId) -> Result<bool> {
|
|
info!("remote refresh for {}", lock_id);
|
|
let refresh_request = Self::create_unlock_request(lock_id);
|
|
let mut client = self.get_client().await?;
|
|
let resource_summary = refresh_request.resource.to_string();
|
|
let mut req = Request::new(GenerallyLockRequest {
|
|
args: serde_json::to_string(&refresh_request)
|
|
.map_err(|e| LockError::internal(format!("Failed to serialize request: {e}")))?,
|
|
});
|
|
attach_lock_mutation_body_digest(&mut req)?;
|
|
let resp = self
|
|
.execute_rpc("refresh", &resource_summary, client.refresh(req))
|
|
.await?
|
|
.into_inner();
|
|
if let Some(error_info) = resp.error_info {
|
|
return Err(LockError::internal(error_info));
|
|
}
|
|
Ok(resp.success)
|
|
}
|
|
|
|
async fn force_release(&self, lock_id: &LockId) -> Result<bool> {
|
|
info!("remote force_release for {}", lock_id);
|
|
let force_request = Self::create_unlock_request(lock_id);
|
|
let mut client = self.get_client().await?;
|
|
let resource_summary = force_request.resource.to_string();
|
|
let mut req = Request::new(GenerallyLockRequest {
|
|
args: serde_json::to_string(&force_request)
|
|
.map_err(|e| LockError::internal(format!("Failed to serialize request: {e}")))?,
|
|
});
|
|
attach_lock_mutation_body_digest(&mut req)?;
|
|
let resp = self
|
|
.execute_rpc("force_release", &resource_summary, client.force_un_lock(req))
|
|
.await?
|
|
.into_inner();
|
|
if let Some(error_info) = resp.error_info {
|
|
return Err(LockError::internal(error_info));
|
|
}
|
|
Ok(resp.success)
|
|
}
|
|
|
|
async fn check_status(&self, lock_id: &LockId) -> Result<Option<LockInfo>> {
|
|
info!("remote check_status for {}", lock_id);
|
|
|
|
// Since there's no direct status query in the gRPC service,
|
|
// we attempt a non-blocking lock acquisition to check if the resource is available
|
|
let status_request = Self::create_unlock_request(lock_id);
|
|
let resource_summary = status_request.resource.to_string();
|
|
let mut client = self.get_client().await?;
|
|
|
|
// Try to acquire a very short-lived lock to test availability
|
|
let mut req = Request::new(GenerallyLockRequest {
|
|
args: serde_json::to_string(&status_request)
|
|
.map_err(|e| LockError::internal(format!("Failed to serialize request: {e}")))?,
|
|
});
|
|
attach_lock_mutation_body_digest(&mut req)?;
|
|
|
|
// Try exclusive lock first with very short timeout
|
|
let resp = match self.execute_rpc("check_status", &resource_summary, client.lock(req)).await {
|
|
Ok(response) => response.into_inner(),
|
|
Err(_) => return Ok(Some(Self::unknown_lock_info(lock_id))),
|
|
};
|
|
|
|
if resp.success {
|
|
// If we successfully acquired the lock, the resource was free.
|
|
// Immediately release it on a best-effort basis.
|
|
let mut release_req = Request::new(GenerallyLockRequest {
|
|
args: serde_json::to_string(&status_request)
|
|
.map_err(|e| LockError::internal(format!("Failed to serialize request: {e}")))?,
|
|
});
|
|
attach_lock_mutation_body_digest(&mut release_req)?;
|
|
let _ = self
|
|
.execute_rpc("check_status_release", &resource_summary, client.un_lock(release_req))
|
|
.await;
|
|
|
|
Ok(None)
|
|
} else {
|
|
// Lock acquisition failed, meaning someone is holding it.
|
|
// We can't determine the exact details remotely, so return a generic status.
|
|
Ok(Some(Self::unknown_lock_info(lock_id)))
|
|
}
|
|
}
|
|
|
|
async fn get_stats(&self) -> Result<LockStats> {
|
|
info!("remote get_stats from {}", self.addr);
|
|
|
|
// Since there's no direct statistics endpoint in the gRPC service,
|
|
// we return basic stats indicating this is a remote client
|
|
let stats = LockStats {
|
|
last_updated: std::time::SystemTime::now(),
|
|
..Default::default()
|
|
};
|
|
|
|
// We could potentially enhance this by:
|
|
// 1. Keeping local counters of operations performed
|
|
// 2. Adding a stats gRPC method to the service
|
|
// 3. Querying server health endpoints
|
|
|
|
// For now, return minimal stats indicating remote connectivity
|
|
Ok(stats)
|
|
}
|
|
|
|
async fn close(&self) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn is_online(&self) -> bool {
|
|
// Use Ping interface to test if remote service is online
|
|
let mut client = match self.get_client().await {
|
|
Ok(client) => client,
|
|
Err(_) => {
|
|
info!("remote client {} connection failed", self.addr);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
let ping_req = Request::new(Self::build_ping_request());
|
|
|
|
match client.ping(ping_req).await {
|
|
Ok(_) => {
|
|
info!("remote client {} is online", self.addr);
|
|
true
|
|
}
|
|
Err(_) => {
|
|
info!("remote client {} ping failed", self.addr);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn is_local(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::runtime::sources as runtime_sources;
|
|
use rustfs_lock::{ObjectKey, types::LockPriority};
|
|
use tokio::net::TcpListener;
|
|
use tokio::task::JoinHandle;
|
|
use tonic::transport::Endpoint as TonicEndpoint;
|
|
|
|
async fn spawn_hanging_listener() -> Option<(String, 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 = format!("http://{}", listener.local_addr().expect("listener local address should be available"));
|
|
let task = tokio::spawn(async move {
|
|
if let Ok((stream, _)) = listener.accept().await {
|
|
let _stream = stream;
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
}
|
|
});
|
|
Some((addr, task))
|
|
}
|
|
|
|
async fn closed_listener_addr() -> Option<String> {
|
|
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 = format!("http://{}", listener.local_addr().expect("listener local address should be available"));
|
|
drop(listener);
|
|
Some(addr)
|
|
}
|
|
|
|
async fn cache_lazy_channel(addr: &str) {
|
|
let channel = TonicEndpoint::from_shared(addr.to_string()).unwrap().connect_lazy();
|
|
runtime_sources::cache_test_node_channel(addr.to_string(), channel).await;
|
|
}
|
|
|
|
fn ensure_test_rpc_secret() {
|
|
runtime_sources::ensure_test_rpc_secret();
|
|
}
|
|
|
|
fn test_lock_request(timeout_duration: Duration) -> LockRequest {
|
|
LockRequest::new(ObjectKey::new("bucket", "object"), LockType::Exclusive, "owner-a")
|
|
.with_acquire_timeout(timeout_duration)
|
|
.with_priority(LockPriority::Normal)
|
|
}
|
|
|
|
#[test]
|
|
fn lock_mutation_helper_marks_single_and_batch_requests_for_rolling_auth() {
|
|
let mut single = Request::new(GenerallyLockRequest {
|
|
args: "single-lock".to_string(),
|
|
});
|
|
attach_lock_mutation_body_digest(&mut single).expect("single lock digest must be attached");
|
|
assert!(
|
|
single
|
|
.extensions()
|
|
.get::<crate::cluster::rpc::http_auth::RollingMutationBodyDigest>()
|
|
.is_some()
|
|
);
|
|
|
|
let mut batch = Request::new(BatchGenerallyLockRequest {
|
|
args: vec!["batch-lock".to_string()],
|
|
});
|
|
attach_lock_mutation_body_digest(&mut batch).expect("batch lock digest must be attached");
|
|
assert!(
|
|
batch
|
|
.extensions()
|
|
.get::<crate::cluster::rpc::http_auth::RollingMutationBodyDigest>()
|
|
.is_some()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial_test::serial]
|
|
async fn test_remote_client_acquire_lock_uses_rpc_timeout_and_evicts_connection() {
|
|
ensure_test_rpc_secret();
|
|
let Some((addr, accept_task)) = spawn_hanging_listener().await else {
|
|
return;
|
|
};
|
|
cache_lazy_channel(&addr).await;
|
|
assert!(runtime_sources::test_node_channel_is_cached(&addr).await);
|
|
|
|
temp_env::async_with_vars([(rustfs_config::ENV_OBJECT_LOCK_RPC_TIMEOUT_MS, Some("50"))], async {
|
|
let client = RemoteClient::new(addr.clone());
|
|
let request = test_lock_request(Duration::from_millis(5));
|
|
let started_at = tokio::time::Instant::now();
|
|
|
|
let response = client.acquire_lock(&request).await.unwrap();
|
|
let elapsed = started_at.elapsed();
|
|
|
|
assert!(
|
|
elapsed >= Duration::from_millis(40),
|
|
"remote lock RPC should use configured transport timeout, got {elapsed:?}"
|
|
);
|
|
assert!(
|
|
elapsed < Duration::from_secs(1),
|
|
"test RPC timeout should keep the test fast, got {elapsed:?}"
|
|
);
|
|
assert!(!response.success, "timed out lock acquisition should fail");
|
|
assert!(
|
|
response
|
|
.error
|
|
.as_deref()
|
|
.is_some_and(|error| error.contains("Remote lock RPC timed out")),
|
|
"expected remote RPC timeout marker, got {:?}",
|
|
response.error
|
|
);
|
|
assert!(
|
|
!runtime_sources::test_node_channel_is_cached(&addr).await,
|
|
"transport timeout should evict cached connection"
|
|
);
|
|
})
|
|
.await;
|
|
|
|
accept_task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial_test::serial]
|
|
async fn test_remote_client_acquire_locks_batch_uses_rpc_timeout_and_evicts_connection() {
|
|
ensure_test_rpc_secret();
|
|
let Some((addr, accept_task)) = spawn_hanging_listener().await else {
|
|
return;
|
|
};
|
|
cache_lazy_channel(&addr).await;
|
|
assert!(runtime_sources::test_node_channel_is_cached(&addr).await);
|
|
|
|
temp_env::async_with_vars([(rustfs_config::ENV_OBJECT_LOCK_RPC_TIMEOUT_MS, Some("50"))], async {
|
|
let client = RemoteClient::new(addr.clone());
|
|
let requests = vec![test_lock_request(Duration::from_millis(5))];
|
|
let started_at = tokio::time::Instant::now();
|
|
|
|
let responses = client.acquire_locks_batch(&requests).await.unwrap();
|
|
let elapsed = started_at.elapsed();
|
|
|
|
assert!(
|
|
elapsed >= Duration::from_millis(40),
|
|
"remote batch lock RPC should use configured transport timeout, got {elapsed:?}"
|
|
);
|
|
assert!(
|
|
elapsed < Duration::from_secs(1),
|
|
"test RPC timeout should keep the test fast, got {elapsed:?}"
|
|
);
|
|
assert_eq!(responses.len(), 1);
|
|
assert!(!responses[0].success, "timed out batch lock acquisition should fail");
|
|
assert!(
|
|
responses[0]
|
|
.error
|
|
.as_deref()
|
|
.is_some_and(|error| error.contains("Remote lock RPC timed out")),
|
|
"expected remote RPC timeout marker, got {:?}",
|
|
responses[0].error
|
|
);
|
|
assert!(
|
|
!runtime_sources::test_node_channel_is_cached(&addr).await,
|
|
"batch transport timeout should evict cached connection"
|
|
);
|
|
})
|
|
.await;
|
|
|
|
accept_task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial_test::serial]
|
|
async fn test_remote_client_release_uses_rpc_timeout_and_evicts_connection() {
|
|
ensure_test_rpc_secret();
|
|
let Some((addr, accept_task)) = spawn_hanging_listener().await else {
|
|
return;
|
|
};
|
|
cache_lazy_channel(&addr).await;
|
|
assert!(runtime_sources::test_node_channel_is_cached(&addr).await);
|
|
|
|
temp_env::async_with_vars([(rustfs_config::ENV_OBJECT_LOCK_RPC_TIMEOUT_MS, Some("50"))], async {
|
|
let client = RemoteClient::new(addr.clone());
|
|
let request = test_lock_request(Duration::from_millis(5));
|
|
let started_at = tokio::time::Instant::now();
|
|
|
|
let err = client.release(&request.lock_id).await.expect_err("release should time out");
|
|
let elapsed = started_at.elapsed();
|
|
|
|
assert!(
|
|
elapsed >= Duration::from_millis(40),
|
|
"remote release RPC should use configured transport timeout, got {elapsed:?}"
|
|
);
|
|
assert!(
|
|
elapsed < Duration::from_secs(1),
|
|
"test RPC timeout should keep the test fast, got {elapsed:?}"
|
|
);
|
|
assert!(matches!(err, LockError::Timeout { .. }), "expected remote release timeout, got {err:?}");
|
|
assert!(
|
|
!runtime_sources::test_node_channel_is_cached(&addr).await,
|
|
"release timeout should evict cached connection"
|
|
);
|
|
})
|
|
.await;
|
|
|
|
accept_task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial_test::serial]
|
|
async fn test_remote_client_refresh_tonic_error_evicts_connection() {
|
|
ensure_test_rpc_secret();
|
|
let Some(addr) = closed_listener_addr().await else {
|
|
return;
|
|
};
|
|
cache_lazy_channel(&addr).await;
|
|
assert!(runtime_sources::test_node_channel_is_cached(&addr).await);
|
|
|
|
temp_env::async_with_vars([(rustfs_config::ENV_OBJECT_LOCK_RPC_TIMEOUT_MS, Some("500"))], async {
|
|
let client = RemoteClient::new(addr.clone());
|
|
let request = test_lock_request(Duration::from_millis(5));
|
|
|
|
let err = client
|
|
.refresh(&request.lock_id)
|
|
.await
|
|
.expect_err("refresh should report tonic failure");
|
|
|
|
assert!(
|
|
err.to_string().contains("refresh RPC failed"),
|
|
"expected refresh RPC failure marker, got {err}"
|
|
);
|
|
assert!(
|
|
!runtime_sources::test_node_channel_is_cached(&addr).await,
|
|
"refresh tonic error should evict cached connection"
|
|
);
|
|
})
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial_test::serial]
|
|
async fn test_remote_client_check_status_timeout_evicts_connection_and_preserves_status_shape() {
|
|
ensure_test_rpc_secret();
|
|
let Some((addr, accept_task)) = spawn_hanging_listener().await else {
|
|
return;
|
|
};
|
|
cache_lazy_channel(&addr).await;
|
|
assert!(runtime_sources::test_node_channel_is_cached(&addr).await);
|
|
|
|
temp_env::async_with_vars([(rustfs_config::ENV_OBJECT_LOCK_RPC_TIMEOUT_MS, Some("50"))], async {
|
|
let client = RemoteClient::new(addr.clone());
|
|
let request = test_lock_request(Duration::from_millis(5));
|
|
let started_at = tokio::time::Instant::now();
|
|
|
|
let status = client.check_status(&request.lock_id).await.unwrap();
|
|
let elapsed = started_at.elapsed();
|
|
|
|
assert!(
|
|
elapsed >= Duration::from_millis(40),
|
|
"remote check_status RPC should use configured transport timeout, got {elapsed:?}"
|
|
);
|
|
assert!(
|
|
elapsed < Duration::from_secs(1),
|
|
"test RPC timeout should keep the test fast, got {elapsed:?}"
|
|
);
|
|
let info = status.expect("communication failure should preserve unknown lock status shape");
|
|
assert_eq!(info.id, request.lock_id);
|
|
assert_eq!(info.owner, "unknown");
|
|
assert!(
|
|
!runtime_sources::test_node_channel_is_cached(&addr).await,
|
|
"check_status timeout should evict cached connection"
|
|
);
|
|
})
|
|
.await;
|
|
|
|
accept_task.abort();
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_transport_failure_classifies_only_channel_level_errors() {
|
|
use tonic::{Code, Status};
|
|
|
|
// Genuine transport failures: broken/unusable channel.
|
|
for code in [Code::Unavailable, Code::DeadlineExceeded, Code::Unknown, Code::Cancelled] {
|
|
assert!(
|
|
RemoteClient::is_transport_failure(&Status::new(code, "boom")),
|
|
"{code:?} should be treated as a transport failure"
|
|
);
|
|
}
|
|
|
|
// A status carrying an underlying transport error as its source is a transport failure
|
|
// regardless of code (tonic reports connection/h2 breakage this way).
|
|
let sourced = Status::from_error(Box::new(std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset")));
|
|
assert!(
|
|
RemoteClient::is_transport_failure(&sourced),
|
|
"a status with an underlying transport source should be a transport failure"
|
|
);
|
|
|
|
// Server-produced application statuses arrive on a healthy channel and must NOT evict it.
|
|
for code in [
|
|
Code::Unauthenticated,
|
|
Code::PermissionDenied,
|
|
Code::Internal,
|
|
Code::FailedPrecondition,
|
|
Code::InvalidArgument,
|
|
Code::NotFound,
|
|
Code::AlreadyExists,
|
|
Code::ResourceExhausted,
|
|
Code::Unimplemented,
|
|
Code::Aborted,
|
|
Code::OutOfRange,
|
|
] {
|
|
assert!(
|
|
!RemoteClient::is_transport_failure(&Status::new(code, "denied")),
|
|
"{code:?} is an application status and must not be treated as a transport failure"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[serial_test::serial]
|
|
fn test_remote_client_rpc_timeout_honors_configured_deadline() {
|
|
temp_env::with_var(rustfs_config::ENV_OBJECT_LOCK_RPC_TIMEOUT_MS, None::<&str>, || {
|
|
assert_eq!(
|
|
RemoteClient::rpc_timeout(),
|
|
Duration::from_millis(rustfs_config::DEFAULT_OBJECT_LOCK_RPC_TIMEOUT_MS)
|
|
);
|
|
});
|
|
temp_env::with_var(rustfs_config::ENV_OBJECT_LOCK_RPC_TIMEOUT_MS, Some("50"), || {
|
|
assert_eq!(RemoteClient::rpc_timeout(), Duration::from_millis(50));
|
|
});
|
|
temp_env::with_var(rustfs_config::ENV_OBJECT_LOCK_RPC_TIMEOUT_MS, Some("0"), || {
|
|
assert_eq!(RemoteClient::rpc_timeout(), Duration::from_millis(1));
|
|
});
|
|
}
|
|
}
|