mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 13:36:50 +00:00
fix(connect): retry incomplete inventory samples
This commit is contained in:
@@ -550,6 +550,8 @@ pub enum InventoryError {
|
|||||||
DriveCount,
|
DriveCount,
|
||||||
#[error("the RustFS inventory capacity is outside protocol bounds")]
|
#[error("the RustFS inventory capacity is outside protocol bounds")]
|
||||||
Capacity,
|
Capacity,
|
||||||
|
#[error("the RustFS inventory snapshot is incomplete: observed {observed} of {expected} configured drives")]
|
||||||
|
SnapshotIncomplete { expected: usize, observed: usize },
|
||||||
#[error("the Connect inventory schedule is invalid")]
|
#[error("the Connect inventory schedule is invalid")]
|
||||||
Schedule,
|
Schedule,
|
||||||
#[error("the Connect inventory sequence is exhausted")]
|
#[error("the Connect inventory sequence is exhausted")]
|
||||||
|
|||||||
@@ -199,6 +199,15 @@ where
|
|||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
let snapshot = match cancellable(&task_shutdown, sample()).await {
|
let snapshot = match cancellable(&task_shutdown, sample()).await {
|
||||||
Some(Ok(snapshot)) => snapshot,
|
Some(Ok(snapshot)) => snapshot,
|
||||||
|
Some(Err(InventoryError::SnapshotIncomplete { .. })) => {
|
||||||
|
let delay = backoff;
|
||||||
|
backoff = backoff.saturating_mul(2).min(retry_schedule.max_backoff);
|
||||||
|
let _ = status_tx.send(InventoryStatus::BackingOff { delay });
|
||||||
|
if sleep_or_cancel(&task_shutdown, delay).await {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Some(Err(error)) => return failed_inventory(&status_tx, error),
|
Some(Err(error)) => return failed_inventory(&status_tx, error),
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use crate::storage_api::startup::services::{ECStore, EndpointServerPools, Server
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
connect::{
|
connect::{
|
||||||
CoarseNodeSummary, HeartbeatConfig, HeartbeatRuntime, InventoryFlag, InventoryRuntime, InventorySchedule,
|
CoarseNodeSummary, HeartbeatConfig, HeartbeatRuntime, InventoryError, InventoryFlag, InventoryRuntime, InventorySchedule,
|
||||||
InventorySnapshot, spawn_heartbeat_runtime, spawn_inventory_runtime,
|
InventorySnapshot, spawn_heartbeat_runtime, spawn_inventory_runtime,
|
||||||
},
|
},
|
||||||
init::{init_buffer_profile_system, init_kms_system},
|
init::{init_buffer_profile_system, init_kms_system},
|
||||||
@@ -80,6 +80,9 @@ pub(crate) async fn init_startup_runtime_services(
|
|||||||
let optional_runtimes = init_optional_runtime_services().await?;
|
let optional_runtimes = init_optional_runtime_services().await?;
|
||||||
let heartbeat_config = HeartbeatConfig::from_env().map_err(std::io::Error::other)?;
|
let heartbeat_config = HeartbeatConfig::from_env().map_err(std::io::Error::other)?;
|
||||||
let heartbeat_nodes = heartbeat_config.as_ref().map(|_| endpoint_pools.get_nodes().len());
|
let heartbeat_nodes = heartbeat_config.as_ref().map(|_| endpoint_pools.get_nodes().len());
|
||||||
|
let inventory_drives = heartbeat_config
|
||||||
|
.as_ref()
|
||||||
|
.map(|_| endpoint_pools.as_ref().iter().map(|pool| pool.endpoints.as_ref().len()).sum());
|
||||||
|
|
||||||
init_buffer_profile_system(config);
|
init_buffer_profile_system(config);
|
||||||
init_deadlock_detector_runtime();
|
init_deadlock_detector_runtime();
|
||||||
@@ -100,7 +103,7 @@ pub(crate) async fn init_startup_runtime_services(
|
|||||||
let enable_scanner = init_background_service_runtime(store.clone()).await?;
|
let enable_scanner = init_background_service_runtime(store.clone()).await?;
|
||||||
init_observability_runtime(store.clone(), ctx.clone()).await;
|
init_observability_runtime(store.clone(), ctx.clone()).await;
|
||||||
let heartbeat = start_heartbeat_runtime(heartbeat_config.clone(), heartbeat_nodes, &ctx)?;
|
let heartbeat = start_heartbeat_runtime(heartbeat_config.clone(), heartbeat_nodes, &ctx)?;
|
||||||
let inventory = start_inventory_runtime(heartbeat_config, heartbeat_nodes, store, &ctx)?;
|
let inventory = start_inventory_runtime(heartbeat_config, heartbeat_nodes, inventory_drives, store, &ctx)?;
|
||||||
let heartbeat = heartbeat.map(|heartbeat| heartbeat.with_inventory(inventory));
|
let heartbeat = heartbeat.map(|heartbeat| heartbeat.with_inventory(inventory));
|
||||||
|
|
||||||
Ok(StartupServiceRuntime {
|
Ok(StartupServiceRuntime {
|
||||||
@@ -129,6 +132,7 @@ fn start_heartbeat_runtime(
|
|||||||
fn start_inventory_runtime(
|
fn start_inventory_runtime(
|
||||||
config: Option<HeartbeatConfig>,
|
config: Option<HeartbeatConfig>,
|
||||||
node_count: Option<usize>,
|
node_count: Option<usize>,
|
||||||
|
expected_drive_count: Option<usize>,
|
||||||
store: Arc<ECStore>,
|
store: Arc<ECStore>,
|
||||||
shutdown: &CancellationToken,
|
shutdown: &CancellationToken,
|
||||||
) -> Result<Option<InventoryRuntime>> {
|
) -> Result<Option<InventoryRuntime>> {
|
||||||
@@ -136,21 +140,57 @@ fn start_inventory_runtime(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let node_count = node_count.unwrap_or_default();
|
let node_count = node_count.unwrap_or_default();
|
||||||
|
let expected_drive_count = expected_drive_count.unwrap_or_default();
|
||||||
spawn_inventory_runtime(Some(config), InventorySchedule::default(), shutdown, move || {
|
spawn_inventory_runtime(Some(config), InventorySchedule::default(), shutdown, move || {
|
||||||
let store = store.clone();
|
let store = store.clone();
|
||||||
async move {
|
async move {
|
||||||
let info = StorageAdminApi::storage_info(store.as_ref()).await;
|
let info = StorageAdminApi::storage_info(store.as_ref()).await;
|
||||||
let total = crate::app::storage_api::capacity::get_total_usable_capacity(&info.disks, &info) as u64;
|
inventory_snapshot(node_count, expected_drive_count, info)
|
||||||
let free = crate::app::storage_api::capacity::get_total_usable_capacity_free(&info.disks, &info) as u64;
|
|
||||||
let mut flags = Vec::with_capacity(3);
|
|
||||||
if info.disks.iter().any(|disk| disk.state == rustfs_madmin::ITEM_OFFLINE) {
|
|
||||||
flags.extend([InventoryFlag::ClusterDegraded, InventoryFlag::DriveOffline]);
|
|
||||||
}
|
|
||||||
if info.disks.iter().any(|disk| disk.healing) {
|
|
||||||
flags.push(InventoryFlag::ClusterHealing);
|
|
||||||
}
|
|
||||||
InventorySnapshot::current(node_count, info.disks.len(), total, free, flags)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map_err(std::io::Error::other)
|
.map_err(std::io::Error::other)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inventory_snapshot(
|
||||||
|
node_count: usize,
|
||||||
|
expected_drive_count: usize,
|
||||||
|
info: rustfs_madmin::StorageInfo,
|
||||||
|
) -> std::result::Result<InventorySnapshot, InventoryError> {
|
||||||
|
if info.disks.len() != expected_drive_count {
|
||||||
|
return Err(InventoryError::SnapshotIncomplete {
|
||||||
|
expected: expected_drive_count,
|
||||||
|
observed: info.disks.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let total = crate::app::storage_api::capacity::get_total_usable_capacity(&info.disks, &info) as u64;
|
||||||
|
let free = crate::app::storage_api::capacity::get_total_usable_capacity_free(&info.disks, &info) as u64;
|
||||||
|
let mut flags = Vec::with_capacity(3);
|
||||||
|
if info.disks.iter().any(|disk| disk.state == rustfs_madmin::ITEM_OFFLINE) {
|
||||||
|
flags.extend([InventoryFlag::ClusterDegraded, InventoryFlag::DriveOffline]);
|
||||||
|
}
|
||||||
|
if info.disks.iter().any(|disk| disk.healing) {
|
||||||
|
flags.push(InventoryFlag::ClusterHealing);
|
||||||
|
}
|
||||||
|
InventorySnapshot::current(node_count, info.disks.len(), total, free, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inventory_rejects_a_partial_startup_storage_snapshot() {
|
||||||
|
let info = rustfs_madmin::StorageInfo {
|
||||||
|
disks: vec![rustfs_madmin::Disk::default()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
inventory_snapshot(2, 2, info),
|
||||||
|
Err(InventoryError::SnapshotIncomplete {
|
||||||
|
expected: 2,
|
||||||
|
observed: 1
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -500,6 +500,39 @@ async fn connect_inventory_disconnect_retries_without_resampling() {
|
|||||||
runtime.shutdown().await;
|
runtime.shutdown().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn connect_inventory_retries_an_incomplete_sample_before_delivery() {
|
||||||
|
let pki = TestPki::new();
|
||||||
|
let content_hash = snapshot().content_hash().expect("content hash");
|
||||||
|
let server = server(&pki, vec![Reply::ok(&content_hash)]).await;
|
||||||
|
let temp = tempfile::tempdir().expect("tempdir");
|
||||||
|
let shutdown = CancellationToken::new();
|
||||||
|
let samples = Arc::new(AtomicUsize::new(0));
|
||||||
|
let sampled = samples.clone();
|
||||||
|
let runtime = spawn_inventory_runtime(Some(config(&temp, &pki, &server)), schedule(), &shutdown, move || {
|
||||||
|
let attempt = sampled.fetch_add(1, Ordering::Relaxed);
|
||||||
|
std::future::ready(if attempt == 0 {
|
||||||
|
Err(rustfs::connect::InventoryError::SnapshotIncomplete {
|
||||||
|
expected: 96,
|
||||||
|
observed: 12,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(snapshot())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.expect("start inventory")
|
||||||
|
.expect("configured inventory");
|
||||||
|
let mut status = runtime.status();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
wait_for(&mut status, |status| matches!(status, InventoryStatus::Online { .. })).await,
|
||||||
|
InventoryStatus::Online { content_hash: accepted, .. } if accepted == content_hash
|
||||||
|
));
|
||||||
|
assert_eq!(samples.load(Ordering::Relaxed), 2);
|
||||||
|
assert_eq!(server.seen.lock().expect("seen lock").len(), 1);
|
||||||
|
runtime.shutdown().await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn connect_inventory_revoked_device_stops_without_retrying() {
|
async fn connect_inventory_revoked_device_stops_without_retrying() {
|
||||||
let pki = TestPki::new();
|
let pki = TestPki::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user