mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 00:17:11 +00:00
refactor(common): move scanner/heal contracts into dedicated crates (#6615)
refactor(common): move scanner/heal domain contracts into dedicated crates crates/common carried ~5.6K lines of scanner/heal domain code (metrics.rs, heal_channel.rs, last_minute.rs) parked there to break dependency cycles; every scanner type change recompiled all 11 rustfs-common dependents. Pure move, zero renames, zero shape changes (backlog#1843): - New crate rustfs-heal-contracts receives heal_channel. - New crate rustfs-scanner-contracts receives metrics, last_minute, and the GLOBAL_INIT_TIME trio (metrics::report() reads it as the current-cycle fallback, so it must live below the shim to avoid a dependency cycle). - rustfs-common re-exports everything at the old paths as a transitional shim; consumers migrate crate by crate, then the shims are deleted.
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -27,8 +26,10 @@ pub static GLOBAL_CONN_MAP: LazyLock<RwLock<HashMap<String, Channel>>> = LazyLoc
|
||||
pub static GLOBAL_ROOT_CERT: LazyLock<RwLock<Option<Vec<u8>>>> = LazyLock::new(|| RwLock::new(None));
|
||||
pub static GLOBAL_MTLS_IDENTITY: LazyLock<RwLock<Option<MtlsIdentityPem>>> = LazyLock::new(|| RwLock::new(None));
|
||||
pub static GLOBAL_OUTBOUND_TLS_GENERATION: LazyLock<AtomicU64> = LazyLock::new(|| AtomicU64::new(0));
|
||||
/// Global initialization time of the RustFS node.
|
||||
pub static GLOBAL_INIT_TIME: LazyLock<RwLock<Option<DateTime<Utc>>>> = LazyLock::new(|| RwLock::new(None));
|
||||
|
||||
// Transitional re-export shim (backlog#1843): the node init-time global moved
|
||||
// to rustfs-scanner-contracts, whose metrics report reads it directly.
|
||||
pub use rustfs_scanner_contracts::{GLOBAL_INIT_TIME, get_global_init_time, set_global_init_time_now};
|
||||
|
||||
/// Log level to use when reporting cached gRPC connection eviction.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -63,20 +64,6 @@ pub fn try_get_global_local_node_name() -> Option<String> {
|
||||
.filter(|name| !name.is_empty())
|
||||
}
|
||||
|
||||
/// Set the global RustFS initialization time to the current UTC time.
|
||||
pub async fn set_global_init_time_now() {
|
||||
let now = Utc::now();
|
||||
*GLOBAL_INIT_TIME.write().await = Some(now);
|
||||
}
|
||||
|
||||
/// Get the global RustFS initialization time.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Option<DateTime<Utc>>` - The initialization time if set.
|
||||
pub async fn get_global_init_time() -> Option<DateTime<Utc>> {
|
||||
*GLOBAL_INIT_TIME.read().await
|
||||
}
|
||||
|
||||
/// Set the global RustFS address used for gRPC connections.
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -1,704 +0,0 @@
|
||||
// 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 serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
sync::OnceLock,
|
||||
};
|
||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const HEAL_DELETE_DANGLING: bool = true;
|
||||
pub const RUSTFS_RESERVED_BUCKET: &str = "rustfs";
|
||||
pub const RUSTFS_RESERVED_BUCKET_PATH: &str = "/rustfs";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum HealItemType {
|
||||
Metadata,
|
||||
Bucket,
|
||||
BucketMetadata,
|
||||
Object,
|
||||
}
|
||||
|
||||
impl HealItemType {
|
||||
pub fn to_str(&self) -> &str {
|
||||
match self {
|
||||
HealItemType::Metadata => "metadata",
|
||||
HealItemType::Bucket => "bucket",
|
||||
HealItemType::BucketMetadata => "bucket-metadata",
|
||||
HealItemType::Object => "object",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HealItemType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum DriveState {
|
||||
Ok,
|
||||
Offline,
|
||||
Corrupt,
|
||||
Missing,
|
||||
PermissionDenied,
|
||||
Faulty,
|
||||
RootMount,
|
||||
Unknown(String),
|
||||
Unformatted, // only returned by disk
|
||||
}
|
||||
|
||||
impl DriveState {
|
||||
pub fn to_str(&self) -> &str {
|
||||
match self {
|
||||
DriveState::Ok => "ok",
|
||||
DriveState::Offline => "offline",
|
||||
DriveState::Corrupt => "corrupt",
|
||||
DriveState::Missing => "missing",
|
||||
DriveState::PermissionDenied => "permission-denied",
|
||||
DriveState::Faulty => "faulty",
|
||||
DriveState::RootMount => "root-mount",
|
||||
DriveState::Unknown(reason) => reason,
|
||||
DriveState::Unformatted => "unformatted",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for DriveState {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
DriveState::Unknown(reason) => DriveState::Unknown(reason.clone()),
|
||||
DriveState::Ok => DriveState::Ok,
|
||||
DriveState::Offline => DriveState::Offline,
|
||||
DriveState::Corrupt => DriveState::Corrupt,
|
||||
DriveState::Missing => DriveState::Missing,
|
||||
DriveState::PermissionDenied => DriveState::PermissionDenied,
|
||||
DriveState::Faulty => DriveState::Faulty,
|
||||
DriveState::RootMount => DriveState::RootMount,
|
||||
DriveState::Unformatted => DriveState::Unformatted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DriveState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum HealScanMode {
|
||||
Unknown = 0,
|
||||
#[default]
|
||||
Normal = 1,
|
||||
Deep = 2,
|
||||
}
|
||||
|
||||
impl HealScanMode {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Unknown => "unknown",
|
||||
Self::Normal => "normal",
|
||||
Self::Deep => "deep",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_u8(value: u8) -> Option<Self> {
|
||||
match value {
|
||||
0 => Some(Self::Unknown),
|
||||
1 => Some(Self::Normal),
|
||||
2 => Some(Self::Deep),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for HealScanMode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_u8(*self as u8)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for HealScanMode {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct HealScanModeVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for HealScanModeVisitor {
|
||||
type Value = HealScanMode;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an integer between 0 and 2")
|
||||
}
|
||||
|
||||
fn visit_u8<E>(self, value: u8) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
HealScanMode::from_u8(value).ok_or_else(|| E::custom(format!("invalid HealScanMode value: {value}")))
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
if value > u8::MAX as u64 {
|
||||
return Err(E::custom(format!("HealScanMode value too large: {value}")));
|
||||
}
|
||||
self.visit_u8(value as u8)
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
if value < 0 || value > u8::MAX as i64 {
|
||||
return Err(E::custom(format!("invalid HealScanMode value: {value}")));
|
||||
}
|
||||
self.visit_u8(value as u8)
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
// Try parsing as number string first (for URL-encoded values)
|
||||
if let Ok(num) = value.parse::<u8>() {
|
||||
return self.visit_u8(num);
|
||||
}
|
||||
// Try parsing as named string
|
||||
match value {
|
||||
"Unknown" | "unknown" => Ok(HealScanMode::Unknown),
|
||||
"Normal" | "normal" => Ok(HealScanMode::Normal),
|
||||
"Deep" | "deep" => Ok(HealScanMode::Deep),
|
||||
_ => Err(E::custom(format!("invalid HealScanMode string: {value}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(HealScanModeVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct HealOpts {
|
||||
pub recursive: bool,
|
||||
#[serde(rename = "dryRun")]
|
||||
pub dry_run: bool,
|
||||
pub remove: bool,
|
||||
pub recreate: bool,
|
||||
#[serde(rename = "scanMode")]
|
||||
pub scan_mode: HealScanMode,
|
||||
#[serde(rename = "updateParity")]
|
||||
pub update_parity: bool,
|
||||
#[serde(rename = "nolock")]
|
||||
pub no_lock: bool,
|
||||
#[serde(rename = "pool", default)]
|
||||
pub pool: Option<usize>,
|
||||
#[serde(rename = "set", default)]
|
||||
pub set: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HealAdmissionDropReason {
|
||||
QueueFull,
|
||||
PolicyDropped,
|
||||
/// HS-06: an admin heal start overlaps (same bucket with mutually
|
||||
/// containing prefixes, or the same erasure set) an already running or
|
||||
/// queued task. Only produced when RUSTFS_HEAL_OVERLAP_POLICY=minio_error.
|
||||
AlreadyRunning,
|
||||
/// HS-06: same as [`Self::AlreadyRunning`] but for paths that merely
|
||||
/// contain (or are contained by) the active task's path.
|
||||
OverlappingPaths,
|
||||
}
|
||||
|
||||
impl HealAdmissionDropReason {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::QueueFull => "queue_full",
|
||||
Self::PolicyDropped => "policy_dropped",
|
||||
Self::AlreadyRunning => "already_running",
|
||||
Self::OverlappingPaths => "overlapping_paths",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HealAdmissionResult {
|
||||
Accepted,
|
||||
Merged,
|
||||
Full,
|
||||
Dropped(HealAdmissionDropReason),
|
||||
}
|
||||
|
||||
/// Admission decision together with the canonical task identifier.
|
||||
///
|
||||
/// A merged request must return the identifier of the task that already owns
|
||||
/// the work instead of exposing the discarded request identifier as a new
|
||||
/// client token.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HealAdmissionReceipt {
|
||||
/// Admission decision for the submitted request.
|
||||
pub result: HealAdmissionResult,
|
||||
/// Canonical identifier of the accepted or merged task.
|
||||
pub task_id: String,
|
||||
}
|
||||
|
||||
impl HealAdmissionResult {
|
||||
pub fn result_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Accepted => "accepted",
|
||||
Self::Merged => "merged",
|
||||
Self::Full => "full",
|
||||
Self::Dropped(_) => "dropped",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reason_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Dropped(reason) => reason.as_str(),
|
||||
_ => "none",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_admitted(self) -> bool {
|
||||
matches!(self, Self::Accepted | Self::Merged)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HealRequestSource {
|
||||
#[default]
|
||||
Internal,
|
||||
Admin,
|
||||
Scanner,
|
||||
AutoHeal,
|
||||
ReadRepair,
|
||||
/// Mission Repair Feed: intents delivered by error paths and replayed
|
||||
/// from the durable MRF journal.
|
||||
Mrf,
|
||||
}
|
||||
|
||||
impl HealRequestSource {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Internal => "internal",
|
||||
Self::Admin => "admin",
|
||||
Self::Scanner => "scanner",
|
||||
Self::AutoHeal => "auto_heal",
|
||||
Self::ReadRepair => "read_repair",
|
||||
Self::Mrf => "mrf",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Heal channel command type
|
||||
#[derive(Debug)]
|
||||
pub enum HealChannelCommand {
|
||||
/// Start a new heal task
|
||||
Start {
|
||||
request: HealChannelRequest,
|
||||
response_tx: oneshot::Sender<Result<HealAdmissionResult, String>>,
|
||||
},
|
||||
/// Query heal task status
|
||||
Query {
|
||||
heal_path: String,
|
||||
client_token: String,
|
||||
/// Incremental result cursor (HS-06): only items with a sequence
|
||||
/// greater than this are returned; `None` keeps the full snapshot.
|
||||
since_seq: Option<u64>,
|
||||
response_tx: oneshot::Sender<Result<HealChannelResponse, String>>,
|
||||
},
|
||||
/// Cancel heal task
|
||||
Cancel {
|
||||
heal_path: String,
|
||||
client_token: String,
|
||||
response_tx: oneshot::Sender<Result<HealChannelResponse, String>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Heal request from admin to ahm
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HealChannelRequest {
|
||||
/// Unique request ID
|
||||
pub id: String,
|
||||
/// Disk ID for heal disk/erasure set task
|
||||
pub disk: Option<String>,
|
||||
/// Bucket name
|
||||
pub bucket: String,
|
||||
/// Object prefix (optional)
|
||||
pub object_prefix: Option<String>,
|
||||
/// Object version ID (optional)
|
||||
pub object_version_id: Option<String>,
|
||||
/// Force start heal
|
||||
pub force_start: bool,
|
||||
/// Priority
|
||||
pub priority: HealChannelPriority,
|
||||
/// Pool index (optional)
|
||||
pub pool_index: Option<usize>,
|
||||
/// Set index (optional)
|
||||
pub set_index: Option<usize>,
|
||||
/// Scan mode (optional)
|
||||
pub scan_mode: Option<HealScanMode>,
|
||||
/// Whether to remove corrupted data
|
||||
pub remove_corrupted: Option<bool>,
|
||||
/// Whether to recreate missing data
|
||||
pub recreate_missing: Option<bool>,
|
||||
/// Whether to update parity
|
||||
pub update_parity: Option<bool>,
|
||||
/// Whether to recursively process
|
||||
pub recursive: Option<bool>,
|
||||
/// Whether to dry run
|
||||
pub dry_run: Option<bool>,
|
||||
/// Whether to skip namespace locking
|
||||
pub no_lock: Option<bool>,
|
||||
/// Timeout in seconds (optional)
|
||||
pub timeout_seconds: Option<u64>,
|
||||
/// Origin of the request for operational status and queue accounting
|
||||
pub source: HealRequestSource,
|
||||
}
|
||||
|
||||
/// Heal response from ahm to admin
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HealChannelResponse {
|
||||
/// Request ID
|
||||
pub request_id: String,
|
||||
/// Success status
|
||||
pub success: bool,
|
||||
/// Response data (if successful)
|
||||
pub data: Option<Vec<u8>>,
|
||||
/// Error message (if failed)
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Heal priority
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HealChannelPriority {
|
||||
/// Low priority
|
||||
Low,
|
||||
/// Normal priority
|
||||
#[default]
|
||||
Normal,
|
||||
/// High priority
|
||||
High,
|
||||
/// Critical priority
|
||||
Critical,
|
||||
}
|
||||
|
||||
/// Heal channel sender
|
||||
pub type HealChannelSender = mpsc::UnboundedSender<HealChannelCommand>;
|
||||
|
||||
/// Heal channel receiver
|
||||
pub type HealChannelReceiver = mpsc::UnboundedReceiver<HealChannelCommand>;
|
||||
|
||||
/// Canonical-receipt start command kept separate from the legacy public enum.
|
||||
#[derive(Debug)]
|
||||
pub struct HealReceiptCommand {
|
||||
/// Heal request to admit.
|
||||
pub request: HealChannelRequest,
|
||||
/// Completion channel for the admission receipt.
|
||||
pub response_tx: oneshot::Sender<Result<HealAdmissionReceipt, String>>,
|
||||
}
|
||||
|
||||
/// Canonical-receipt command receiver.
|
||||
pub type HealReceiptReceiver = mpsc::UnboundedReceiver<HealReceiptCommand>;
|
||||
|
||||
struct HealChannelSenders {
|
||||
command: HealChannelSender,
|
||||
receipt: mpsc::UnboundedSender<HealReceiptCommand>,
|
||||
}
|
||||
|
||||
/// Global heal channel sender
|
||||
static GLOBAL_HEAL_CHANNEL_SENDERS: OnceLock<HealChannelSenders> = OnceLock::new();
|
||||
|
||||
type HealResponseSender = broadcast::Sender<HealChannelResponse>;
|
||||
|
||||
/// Global heal response broadcaster
|
||||
static GLOBAL_HEAL_RESPONSE_SENDER: OnceLock<HealResponseSender> = OnceLock::new();
|
||||
|
||||
/// Initialize global heal channel
|
||||
pub fn init_heal_channel() -> Result<HealChannelReceiver, &'static str> {
|
||||
let (receiver, receipt_receiver) = init_heal_channels()?;
|
||||
drop(receipt_receiver);
|
||||
Ok(receiver)
|
||||
}
|
||||
|
||||
/// Initialize the legacy command and canonical-receipt channels atomically.
|
||||
pub fn init_heal_channels() -> Result<(HealChannelReceiver, HealReceiptReceiver), &'static str> {
|
||||
let (command, command_receiver) = mpsc::unbounded_channel();
|
||||
let (receipt, receipt_receiver) = mpsc::unbounded_channel();
|
||||
GLOBAL_HEAL_CHANNEL_SENDERS
|
||||
.set(HealChannelSenders { command, receipt })
|
||||
.map_err(|_| "Heal channel sender already initialized")?;
|
||||
Ok((command_receiver, receipt_receiver))
|
||||
}
|
||||
|
||||
/// Get global heal channel sender
|
||||
pub fn get_heal_channel_sender() -> Option<&'static HealChannelSender> {
|
||||
GLOBAL_HEAL_CHANNEL_SENDERS.get().map(|senders| &senders.command)
|
||||
}
|
||||
|
||||
/// Send heal command through global channel
|
||||
pub async fn send_heal_command(command: HealChannelCommand) -> Result<(), String> {
|
||||
if let Some(sender) = get_heal_channel_sender() {
|
||||
sender
|
||||
.send(command)
|
||||
.map_err(|e| format!("Failed to send heal command: {e}"))?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Heal channel not initialized".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn heal_response_sender() -> &'static HealResponseSender {
|
||||
GLOBAL_HEAL_RESPONSE_SENDER.get_or_init(|| {
|
||||
let (tx, _rx) = broadcast::channel(1024);
|
||||
tx
|
||||
})
|
||||
}
|
||||
|
||||
/// Publish a heal response to subscribers.
|
||||
pub fn publish_heal_response(response: HealChannelResponse) -> Result<(), broadcast::error::SendError<HealChannelResponse>> {
|
||||
let sender = heal_response_sender();
|
||||
let _ = sender.send(response);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribe to heal responses.
|
||||
pub fn subscribe_heal_responses() -> broadcast::Receiver<HealChannelResponse> {
|
||||
heal_response_sender().subscribe()
|
||||
}
|
||||
|
||||
/// Send heal start request and wait for structured admission feedback.
|
||||
pub async fn send_heal_request_with_receipt(request: HealChannelRequest) -> Result<HealAdmissionReceipt, String> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let senders = GLOBAL_HEAL_CHANNEL_SENDERS
|
||||
.get()
|
||||
.ok_or_else(|| "Heal channel not initialized".to_string())?;
|
||||
senders
|
||||
.receipt
|
||||
.send(HealReceiptCommand { request, response_tx })
|
||||
.map_err(|err| format!("Failed to send heal receipt command: {err}"))?;
|
||||
response_rx
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive heal admission response: {e}"))?
|
||||
}
|
||||
|
||||
/// Send heal start request and wait for structured admission feedback.
|
||||
pub async fn send_heal_request_with_admission(request: HealChannelRequest) -> Result<HealAdmissionResult, String> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
send_heal_command(HealChannelCommand::Start { request, response_tx }).await?;
|
||||
response_rx
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive heal admission response: {e}"))?
|
||||
}
|
||||
|
||||
/// Send heal start request
|
||||
pub async fn send_heal_request(request: HealChannelRequest) -> Result<(), String> {
|
||||
match send_heal_request_with_admission(request).await? {
|
||||
HealAdmissionResult::Accepted | HealAdmissionResult::Merged => Ok(()),
|
||||
HealAdmissionResult::Full => Err("Heal request queue is full".to_string()),
|
||||
HealAdmissionResult::Dropped(reason) => Err(format!("Heal request dropped: {}", reason.as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn receive_heal_channel_response(
|
||||
response_rx: oneshot::Receiver<Result<HealChannelResponse, String>>,
|
||||
) -> Result<HealChannelResponse, String> {
|
||||
response_rx
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive heal channel response: {e}"))?
|
||||
}
|
||||
|
||||
/// Send heal query request
|
||||
pub async fn query_heal_status(heal_path: String, client_token: String) -> Result<HealChannelResponse, String> {
|
||||
query_heal_status_since(heal_path, client_token, None).await
|
||||
}
|
||||
|
||||
/// Incremental heal query (HS-06): pass the client's last seen sequence
|
||||
/// number to receive only newer result items.
|
||||
pub async fn query_heal_status_since(
|
||||
heal_path: String,
|
||||
client_token: String,
|
||||
since_seq: Option<u64>,
|
||||
) -> Result<HealChannelResponse, String> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
send_heal_command(HealChannelCommand::Query {
|
||||
heal_path,
|
||||
client_token,
|
||||
since_seq,
|
||||
response_tx,
|
||||
})
|
||||
.await?;
|
||||
receive_heal_channel_response(response_rx).await
|
||||
}
|
||||
|
||||
/// Send heal cancel request
|
||||
pub async fn cancel_heal_task(heal_path: String, client_token: String) -> Result<HealChannelResponse, String> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
send_heal_command(HealChannelCommand::Cancel {
|
||||
heal_path,
|
||||
client_token,
|
||||
response_tx,
|
||||
})
|
||||
.await?;
|
||||
receive_heal_channel_response(response_rx).await
|
||||
}
|
||||
|
||||
/// Create a new heal request
|
||||
pub fn create_heal_request(
|
||||
bucket: String,
|
||||
object_prefix: Option<String>,
|
||||
force_start: bool,
|
||||
priority: Option<HealChannelPriority>,
|
||||
) -> HealChannelRequest {
|
||||
HealChannelRequest {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
bucket,
|
||||
object_prefix,
|
||||
object_version_id: None,
|
||||
force_start,
|
||||
priority: priority.unwrap_or_default(),
|
||||
pool_index: None,
|
||||
set_index: None,
|
||||
scan_mode: None,
|
||||
remove_corrupted: None,
|
||||
recreate_missing: None,
|
||||
update_parity: None,
|
||||
recursive: None,
|
||||
dry_run: None,
|
||||
no_lock: None,
|
||||
timeout_seconds: None,
|
||||
source: HealRequestSource::Internal,
|
||||
disk: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new heal request with advanced options
|
||||
pub fn create_heal_request_with_options(
|
||||
bucket: String,
|
||||
object_prefix: Option<String>,
|
||||
force_start: bool,
|
||||
priority: Option<HealChannelPriority>,
|
||||
pool_index: Option<usize>,
|
||||
set_index: Option<usize>,
|
||||
) -> HealChannelRequest {
|
||||
HealChannelRequest {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
bucket,
|
||||
object_prefix,
|
||||
object_version_id: None,
|
||||
force_start,
|
||||
priority: priority.unwrap_or_default(),
|
||||
pool_index,
|
||||
set_index,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a heal response
|
||||
pub fn create_heal_response(
|
||||
request_id: String,
|
||||
success: bool,
|
||||
data: Option<Vec<u8>>,
|
||||
error: Option<String>,
|
||||
) -> HealChannelResponse {
|
||||
HealChannelResponse {
|
||||
request_id,
|
||||
success,
|
||||
data,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_heal_disk(set_disk_id: String, priority: Option<HealChannelPriority>) -> Result<(), String> {
|
||||
let req = HealChannelRequest {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
bucket: "".to_string(),
|
||||
object_prefix: None,
|
||||
disk: Some(set_disk_id),
|
||||
object_version_id: None,
|
||||
force_start: false,
|
||||
priority: priority.unwrap_or(HealChannelPriority::Low),
|
||||
pool_index: None,
|
||||
set_index: None,
|
||||
scan_mode: None,
|
||||
remove_corrupted: None,
|
||||
recreate_missing: None,
|
||||
update_parity: None,
|
||||
recursive: None,
|
||||
dry_run: None,
|
||||
no_lock: None,
|
||||
timeout_seconds: None,
|
||||
source: HealRequestSource::AutoHeal,
|
||||
};
|
||||
send_heal_request(req).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn heal_request_source_labels_are_stable() {
|
||||
assert_eq!(HealRequestSource::Scanner.as_str(), "scanner");
|
||||
assert_eq!(HealRequestSource::Admin.as_str(), "admin");
|
||||
assert_eq!(HealRequestSource::AutoHeal.as_str(), "auto_heal");
|
||||
assert_eq!(HealRequestSource::Internal.as_str(), "internal");
|
||||
assert_eq!(HealRequestSource::ReadRepair.as_str(), "read_repair");
|
||||
|
||||
let request = HealChannelRequest::default();
|
||||
assert_eq!(request.source, HealRequestSource::Internal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heal_admission_result_labels_are_stable() {
|
||||
assert_eq!(HealAdmissionResult::Accepted.result_label(), "accepted");
|
||||
assert_eq!(HealAdmissionResult::Merged.result_label(), "merged");
|
||||
assert_eq!(HealAdmissionResult::Full.result_label(), "full");
|
||||
assert_eq!(
|
||||
HealAdmissionResult::Dropped(HealAdmissionDropReason::QueueFull).reason_label(),
|
||||
"queue_full"
|
||||
);
|
||||
assert!(HealAdmissionResult::Merged.is_admitted());
|
||||
assert!(!HealAdmissionResult::Full.is_admitted());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heal_response_broadcast_reaches_subscriber() {
|
||||
let mut receiver = subscribe_heal_responses();
|
||||
let response = create_heal_response("req-1".to_string(), true, None, None);
|
||||
|
||||
publish_heal_response(response.clone()).expect("publish should succeed");
|
||||
|
||||
let received = receiver.recv().await.expect("should receive heal response");
|
||||
assert_eq!(received.request_id, response.request_id);
|
||||
assert!(received.success);
|
||||
|
||||
drop(receiver);
|
||||
let response = create_heal_response("req-no-subscriber".to_string(), true, None, None);
|
||||
|
||||
publish_heal_response(response).expect("publish without subscribers should be ignored");
|
||||
}
|
||||
}
|
||||
@@ -1,498 +0,0 @@
|
||||
// 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 std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
#[derive(Clone, Debug, Default, Copy)]
|
||||
pub struct AccElem {
|
||||
pub total: u64,
|
||||
pub size: u64,
|
||||
pub n: u64,
|
||||
}
|
||||
|
||||
impl AccElem {
|
||||
pub fn add(&mut self, dur: &Duration) {
|
||||
let dur = dur.as_secs();
|
||||
self.total = self.total.wrapping_add(dur);
|
||||
self.n = self.n.wrapping_add(1);
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, b: &AccElem) {
|
||||
self.n = self.n.wrapping_add(b.n);
|
||||
self.total = self.total.wrapping_add(b.total);
|
||||
self.size = self.size.wrapping_add(b.size);
|
||||
}
|
||||
|
||||
pub fn avg(&self) -> Duration {
|
||||
if self.n >= 1 && self.total > 0 {
|
||||
return Duration::from_secs(self.total / self.n);
|
||||
}
|
||||
Duration::from_secs(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LastMinuteLatency {
|
||||
pub totals: Vec<AccElem>,
|
||||
pub last_sec: u64,
|
||||
}
|
||||
|
||||
impl Default for LastMinuteLatency {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
totals: vec![AccElem::default(); 60],
|
||||
last_sec: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LastMinuteLatency {
|
||||
pub fn merge(&mut self, o: &LastMinuteLatency) -> LastMinuteLatency {
|
||||
let mut merged = LastMinuteLatency::default();
|
||||
let mut x = o.clone();
|
||||
if self.last_sec > o.last_sec {
|
||||
x.forward_to(self.last_sec);
|
||||
merged.last_sec = self.last_sec;
|
||||
} else {
|
||||
self.forward_to(o.last_sec);
|
||||
merged.last_sec = o.last_sec;
|
||||
}
|
||||
|
||||
// Both operands must be read in their forwarded form so aged-out
|
||||
// ring-buffer slots stay zeroed: `x` is the forwarded copy of `o`,
|
||||
// and `self` is forwarded in place in the `else` branch above.
|
||||
for i in 0..merged.totals.len() {
|
||||
merged.totals[i] = AccElem {
|
||||
total: self.totals[i].total.wrapping_add(x.totals[i].total),
|
||||
n: self.totals[i].n.wrapping_add(x.totals[i].n),
|
||||
size: self.totals[i].size.wrapping_add(x.totals[i].size),
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
pub fn add(&mut self, t: &Duration) {
|
||||
let sec = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
self.forward_to(sec);
|
||||
let win_idx = sec % 60;
|
||||
self.totals[win_idx as usize].add(t);
|
||||
self.last_sec = sec;
|
||||
}
|
||||
|
||||
pub fn add_all(&mut self, sec: u64, a: &AccElem) {
|
||||
self.forward_to(sec);
|
||||
let win_idx = sec % 60;
|
||||
self.totals[win_idx as usize].merge(a);
|
||||
self.last_sec = sec;
|
||||
}
|
||||
|
||||
pub fn get_total(&mut self) -> AccElem {
|
||||
let mut res = AccElem::default();
|
||||
let sec = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
self.forward_to(sec);
|
||||
for elem in self.totals.iter() {
|
||||
res.merge(elem);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub fn forward_to(&mut self, t: u64) {
|
||||
if self.last_sec >= t {
|
||||
return;
|
||||
}
|
||||
if t - self.last_sec >= 60 {
|
||||
self.totals = vec![AccElem::default(); 60];
|
||||
self.last_sec = t;
|
||||
return;
|
||||
}
|
||||
while self.last_sec != t {
|
||||
let idx = (self.last_sec + 1) % 60;
|
||||
self.totals[idx as usize] = AccElem::default();
|
||||
self.last_sec += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_acc_elem_add_multiple_durations() {
|
||||
let mut elem = AccElem::default();
|
||||
|
||||
elem.add(&Duration::from_secs(3));
|
||||
elem.add(&Duration::from_secs(7));
|
||||
elem.add(&Duration::from_secs(2));
|
||||
|
||||
assert_eq!(elem.total, 12);
|
||||
assert_eq!(elem.n, 3);
|
||||
assert_eq!(elem.size, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acc_elem_add_subsecond_duration() {
|
||||
let mut elem = AccElem::default();
|
||||
// Duration less than 1 second should be truncated to 0
|
||||
let duration = Duration::from_millis(500);
|
||||
|
||||
elem.add(&duration);
|
||||
|
||||
assert_eq!(elem.total, 0); // as_secs() truncates subsecond values
|
||||
assert_eq!(elem.n, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acc_elem_merge_with_data() {
|
||||
let mut elem1 = AccElem {
|
||||
total: 10,
|
||||
size: 100,
|
||||
n: 2,
|
||||
};
|
||||
let elem2 = AccElem {
|
||||
total: 15,
|
||||
size: 200,
|
||||
n: 3,
|
||||
};
|
||||
|
||||
elem1.merge(&elem2);
|
||||
|
||||
assert_eq!(elem1.total, 25);
|
||||
assert_eq!(elem1.size, 300);
|
||||
assert_eq!(elem1.n, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acc_elem_avg_zero_count() {
|
||||
let elem = AccElem {
|
||||
total: 10,
|
||||
size: 0,
|
||||
n: 0,
|
||||
};
|
||||
|
||||
let avg = elem.avg();
|
||||
assert_eq!(avg, Duration::from_secs(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acc_elem_avg_rounding() {
|
||||
let elem = AccElem {
|
||||
total: 10,
|
||||
size: 0,
|
||||
n: 3,
|
||||
};
|
||||
|
||||
let avg = elem.avg();
|
||||
assert_eq!(avg, Duration::from_secs(3)); // 10 / 3 = 3 (integer division)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_minute_latency_forward_to_past_time() {
|
||||
let mut latency = LastMinuteLatency {
|
||||
last_sec: 100,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Add some data to verify it's not cleared
|
||||
latency.totals[0].total = 10;
|
||||
latency.totals[0].n = 1;
|
||||
|
||||
latency.forward_to(50); // Past time
|
||||
|
||||
assert_eq!(latency.last_sec, 100); // Should not change
|
||||
assert_eq!(latency.totals[0].total, 10); // Data should remain
|
||||
assert_eq!(latency.totals[0].n, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_minute_latency_forward_to_large_gap() {
|
||||
let mut latency = LastMinuteLatency {
|
||||
last_sec: 100,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Add some data to verify it's cleared
|
||||
latency.totals[0].total = 10;
|
||||
latency.totals[0].n = 1;
|
||||
|
||||
latency.forward_to(200); // Gap >= 60 seconds
|
||||
|
||||
assert_eq!(latency.last_sec, 200); // last_sec should be updated to target time
|
||||
|
||||
// All data should be cleared
|
||||
for elem in &latency.totals {
|
||||
assert_eq!(elem.total, 0);
|
||||
assert_eq!(elem.size, 0);
|
||||
assert_eq!(elem.n, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_minute_latency_forward_to_small_gap() {
|
||||
let mut latency = LastMinuteLatency {
|
||||
last_sec: 100,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Add data at specific indices
|
||||
latency.totals[41].total = 10; // (100 + 1) % 60 = 41
|
||||
latency.totals[42].total = 20; // (100 + 2) % 60 = 42
|
||||
|
||||
latency.forward_to(102); // Forward by 2 seconds
|
||||
|
||||
assert_eq!(latency.last_sec, 102);
|
||||
|
||||
// The slots that were advanced should be cleared
|
||||
assert_eq!(latency.totals[41].total, 0); // Cleared during forward
|
||||
assert_eq!(latency.totals[42].total, 0); // Cleared during forward
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_minute_latency_add_all_multiple() {
|
||||
let mut latency = LastMinuteLatency::default();
|
||||
|
||||
let acc_elem1 = AccElem {
|
||||
total: 10,
|
||||
size: 50,
|
||||
n: 2,
|
||||
};
|
||||
let acc_elem2 = AccElem {
|
||||
total: 20,
|
||||
size: 100,
|
||||
n: 4,
|
||||
};
|
||||
|
||||
latency.add_all(1000, &acc_elem1);
|
||||
latency.add_all(1000, &acc_elem2); // Same second
|
||||
|
||||
let idx = 1000 % 60;
|
||||
assert_eq!(latency.totals[idx as usize].total, 30); // 10 + 20
|
||||
assert_eq!(latency.totals[idx as usize].size, 150); // 50 + 100
|
||||
assert_eq!(latency.totals[idx as usize].n, 6); // 2 + 4
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_minute_latency_merge_different_times() {
|
||||
let mut latency1 = LastMinuteLatency::default();
|
||||
let mut latency2 = LastMinuteLatency::default();
|
||||
|
||||
latency1.last_sec = 1000;
|
||||
latency2.last_sec = 1010; // 10 seconds later
|
||||
|
||||
// Add data to both
|
||||
latency1.totals[0].total = 10;
|
||||
latency2.totals[0].total = 20;
|
||||
|
||||
let merged = latency1.merge(&latency2);
|
||||
|
||||
assert_eq!(merged.last_sec, 1010); // Should use the later time
|
||||
assert_eq!(merged.totals[0].total, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_minute_latency_merge_ages_out_stale_slots_self_newer() {
|
||||
// self.last_sec > o.last_sec branch: `o` is forwarded to self.last_sec,
|
||||
// which zeroes the ring-buffer slots for seconds 1001..=1010, i.e.
|
||||
// indices 41..=50. Data parked in one of those slots is stale and must
|
||||
// be excluded from the merged result.
|
||||
let mut newer = LastMinuteLatency::default();
|
||||
let mut older = LastMinuteLatency::default();
|
||||
|
||||
newer.last_sec = 1010;
|
||||
older.last_sec = 1000;
|
||||
|
||||
// Stale slot: second 1005 -> index 45, cleared by forward_to(1010).
|
||||
let stale_idx = (1005 % 60) as usize;
|
||||
older.totals[stale_idx].total = 111;
|
||||
older.totals[stale_idx].n = 5;
|
||||
older.totals[stale_idx].size = 999;
|
||||
|
||||
// In-window slot: older's own last_sec (1000 -> index 40) is kept.
|
||||
let kept_idx = (1000 % 60) as usize;
|
||||
older.totals[kept_idx].total = 3;
|
||||
older.totals[kept_idx].n = 1;
|
||||
older.totals[kept_idx].size = 30;
|
||||
|
||||
// newer's current data (1010 -> index 50) must survive.
|
||||
let newer_idx = (1010 % 60) as usize;
|
||||
newer.totals[newer_idx].total = 7;
|
||||
newer.totals[newer_idx].n = 1;
|
||||
newer.totals[newer_idx].size = 70;
|
||||
|
||||
let merged = newer.merge(&older);
|
||||
|
||||
assert_eq!(merged.last_sec, 1010);
|
||||
// The stale older slot is aged out -> excluded from the sum.
|
||||
assert_eq!(merged.totals[stale_idx].total, 0);
|
||||
assert_eq!(merged.totals[stale_idx].n, 0);
|
||||
assert_eq!(merged.totals[stale_idx].size, 0);
|
||||
// In-window older data is retained.
|
||||
assert_eq!(merged.totals[kept_idx].total, 3);
|
||||
assert_eq!(merged.totals[kept_idx].n, 1);
|
||||
// newer data is retained.
|
||||
assert_eq!(merged.totals[newer_idx].total, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_minute_latency_merge_ages_out_of_window_self_newer() {
|
||||
// self.last_sec > o.last_sec with a full-window gap (>= 60s): all of
|
||||
// `o` is aged out and only `self`'s data remains.
|
||||
let mut newer = LastMinuteLatency::default();
|
||||
let mut older = LastMinuteLatency::default();
|
||||
|
||||
newer.last_sec = 1070;
|
||||
older.last_sec = 1000; // gap of 70 >= 60 -> all of older is aged out
|
||||
|
||||
// 1070 % 60 == 1000 % 60 == 10, so both write the same slot; the fix
|
||||
// must yield exactly newer's value, not newer + stale older.
|
||||
let idx = (1070 % 60) as usize;
|
||||
older.totals[idx].total = 111;
|
||||
older.totals[idx].n = 5;
|
||||
older.totals[idx].size = 999;
|
||||
newer.totals[idx].total = 7;
|
||||
newer.totals[idx].n = 1;
|
||||
newer.totals[idx].size = 70;
|
||||
|
||||
let merged = newer.merge(&older);
|
||||
|
||||
assert_eq!(merged.last_sec, 1070);
|
||||
assert_eq!(merged.totals[idx].total, 7);
|
||||
assert_eq!(merged.totals[idx].n, 1);
|
||||
assert_eq!(merged.totals[idx].size, 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_minute_latency_merge_ages_out_of_window_o_newer() {
|
||||
// Mirror of the above for the else branch: `self` is older and is
|
||||
// forwarded to o.last_sec, aging out self's out-of-window data.
|
||||
let mut older = LastMinuteLatency::default();
|
||||
let mut newer = LastMinuteLatency::default();
|
||||
|
||||
older.last_sec = 1000;
|
||||
newer.last_sec = 1070; // gap of 70 >= 60 -> all of older (self) is aged out
|
||||
|
||||
let idx = (1070 % 60) as usize; // 1000 % 60 == 1070 % 60 == 10
|
||||
older.totals[idx].total = 111;
|
||||
older.totals[idx].n = 5;
|
||||
older.totals[idx].size = 999;
|
||||
newer.totals[idx].total = 7;
|
||||
newer.totals[idx].n = 1;
|
||||
newer.totals[idx].size = 70;
|
||||
|
||||
let merged = older.merge(&newer);
|
||||
|
||||
assert_eq!(merged.last_sec, 1070);
|
||||
// self's stale data aged out; only newer's data remains.
|
||||
assert_eq!(merged.totals[idx].total, 7);
|
||||
assert_eq!(merged.totals[idx].n, 1);
|
||||
assert_eq!(merged.totals[idx].size, 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_minute_latency_window_wraparound() {
|
||||
let mut latency = LastMinuteLatency::default();
|
||||
|
||||
// Test that indices wrap around correctly
|
||||
for sec in 0..120 {
|
||||
// Test for 2 minutes
|
||||
let acc_elem = AccElem {
|
||||
total: sec,
|
||||
size: 0,
|
||||
n: 1,
|
||||
};
|
||||
latency.add_all(sec, &acc_elem);
|
||||
|
||||
let expected_idx = sec % 60;
|
||||
assert_eq!(latency.totals[expected_idx as usize].total, sec);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_case_max_values() {
|
||||
let mut elem = AccElem {
|
||||
total: u64::MAX - 50,
|
||||
size: u64::MAX - 50,
|
||||
n: u64::MAX - 50,
|
||||
};
|
||||
|
||||
let other = AccElem {
|
||||
total: 100,
|
||||
size: 100,
|
||||
n: 100,
|
||||
};
|
||||
|
||||
// This should not panic due to overflow, values will wrap around
|
||||
elem.merge(&other);
|
||||
|
||||
// Values should wrap around due to overflow (wrapping_add behavior)
|
||||
assert_eq!(elem.total, 49); // (u64::MAX - 50) + 100 wraps to 49
|
||||
assert_eq!(elem.size, 49);
|
||||
assert_eq!(elem.n, 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forward_to_boundary_conditions() {
|
||||
let mut latency = LastMinuteLatency {
|
||||
last_sec: 59,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Add data at the last slot
|
||||
latency.totals[59].total = 100;
|
||||
latency.totals[59].n = 1;
|
||||
|
||||
// Forward exactly 60 seconds (boundary case)
|
||||
latency.forward_to(119);
|
||||
|
||||
// All data should be cleared
|
||||
for elem in &latency.totals {
|
||||
assert_eq!(elem.total, 0);
|
||||
assert_eq!(elem.n, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_total_with_data() {
|
||||
let mut latency = LastMinuteLatency::default();
|
||||
|
||||
// Set a recent timestamp to avoid forward_to clearing data
|
||||
let current_time = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
latency.last_sec = current_time;
|
||||
|
||||
// Add data to multiple slots
|
||||
latency.totals[0] = AccElem {
|
||||
total: 10,
|
||||
size: 100,
|
||||
n: 1,
|
||||
};
|
||||
latency.totals[1] = AccElem {
|
||||
total: 20,
|
||||
size: 200,
|
||||
n: 2,
|
||||
};
|
||||
latency.totals[59] = AccElem {
|
||||
total: 30,
|
||||
size: 300,
|
||||
n: 3,
|
||||
};
|
||||
|
||||
let total = latency.get_total();
|
||||
|
||||
assert_eq!(total.total, 60);
|
||||
assert_eq!(total.size, 600);
|
||||
assert_eq!(total.n, 6);
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,18 @@
|
||||
|
||||
// pub mod error;
|
||||
pub mod globals;
|
||||
pub mod heal_channel;
|
||||
pub mod last_minute;
|
||||
pub mod metrics;
|
||||
pub mod mrf_channel;
|
||||
mod readiness;
|
||||
pub mod table_catalog;
|
||||
pub mod trace_bus;
|
||||
|
||||
// Transitional re-export shims (backlog#1843): these modules moved to the
|
||||
// rustfs-heal-contracts / rustfs-scanner-contracts crates. Consumers migrate
|
||||
// to the new paths crate by crate; the shims are deleted once
|
||||
// `rg 'rustfs_common::(metrics|heal_channel|last_minute)'` reports zero hits.
|
||||
pub use rustfs_heal_contracts::heal_channel;
|
||||
pub use rustfs_scanner_contracts::{last_minute, metrics};
|
||||
|
||||
pub use globals::*;
|
||||
pub use readiness::{GlobalReadiness, SystemStage};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user