Files
rustfs/crates/io-metrics/src/sampler/process.rs
T
Zhengchao An e3d7892404 test(io-metrics): assert what the remaining smoke tests only called (#6238)
Eight tests in this crate called a recorder and asserted nothing. Five of them were worse than that: every `record_*` in `list_objects_metrics` returns early unless `get_stage_metrics_enabled()` is true, and that flag defaults to false, so those tests only ever exercised the early return — never the code their names describe.

They now run against a local `DebuggingRecorder` with the flag on, and each asserts the boundary it is named for: an empty page reports the scan count as its amplification instead of dividing by zero, a zero read quorum is recorded rather than skipped, index serving divides verification attempts by returned objects, and the `-1` whole-directory sentinel reaches the limit histogram unclamped.

`msgpack_json_fallback_counter_records_without_panicking` has no in-struct total to check, so it now asserts the emission: two direction/message pairs must land in two separate series, which a dropped label would collapse into one.

The two process-sampler tests discarded their snapshots. They now assert what cannot differ between callers — a process has one start time and one descriptor limit regardless of which entry point or which sampler observed it, and the status enum must match its numeric projection.

This clears io-metrics from the census (`scripts/find_assertless_tests.py`), taking the tree from 61 candidates to 53.

Refs backlog#1836
2026-08-19 10:32:09 +08:00

228 lines
8.5 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 std::sync::{Mutex, OnceLock};
use sysinfo::{Pid, ProcessRefreshKind, ProcessStatus, ProcessesToUpdate, System};
static PROCESS_SAMPLER: OnceLock<Mutex<ProcessSampler>> = OnceLock::new();
#[inline]
fn current_pid() -> Pid {
Pid::from_u32(std::process::id())
}
#[derive(Debug)]
pub struct ProcessSampler {
pid: Pid,
sys: System,
}
impl Default for ProcessSampler {
fn default() -> Self {
Self::new()
}
}
impl ProcessSampler {
#[inline]
pub fn new() -> Self {
let pid = current_pid();
let mut sys = System::new();
sys.refresh_processes_specifics(ProcessesToUpdate::Some(&[pid]), true, ProcessRefreshKind::everything());
Self { pid, sys }
}
#[inline]
pub fn snapshot_resource_and_system(&mut self) -> (ProcessResourceSnapshot, ProcessSystemSnapshot) {
snapshot_process_resource_and_system_with(self)
}
}
#[inline]
fn process_sampler() -> &'static Mutex<ProcessSampler> {
PROCESS_SAMPLER.get_or_init(|| Mutex::new(ProcessSampler::new()))
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ProcessResourceSnapshot {
pub cpu_percent: f64,
pub memory_bytes: u64,
pub uptime_seconds: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProcessStatusSnapshot {
Running = 0,
Sleeping = 1,
Zombie = 2,
#[default]
Other = 3,
}
impl From<ProcessStatus> for ProcessStatusSnapshot {
fn from(status: ProcessStatus) -> Self {
match status {
ProcessStatus::Run => ProcessStatusSnapshot::Running,
ProcessStatus::Sleep => ProcessStatusSnapshot::Sleeping,
ProcessStatus::Zombie => ProcessStatusSnapshot::Zombie,
_ => ProcessStatusSnapshot::Other,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ProcessSystemSnapshot {
pub locks_read_total: u64,
pub locks_write_total: u64,
pub cpu_total_seconds: f64,
pub go_routine_total: u64,
pub disk_read_bytes: u64,
pub disk_write_bytes: u64,
pub io_rchar_bytes: u64,
pub io_read_bytes: u64,
pub io_wchar_bytes: u64,
pub io_write_bytes: u64,
pub start_time_seconds: u64,
pub uptime_seconds: u64,
pub file_descriptor_limit_total: u64,
pub file_descriptor_open_total: u64,
pub syscall_read_total: u64,
pub syscall_write_total: u64,
pub resident_memory_bytes: u64,
pub virtual_memory_bytes: u64,
pub virtual_memory_max_bytes: u64,
pub status: ProcessStatusSnapshot,
pub status_value: i64,
}
/// Collect resource-only process snapshot for the current process.
#[inline]
pub fn snapshot_process_resource() -> ProcessResourceSnapshot {
snapshot_process_resource_and_system().0
}
/// Collect system-level process snapshot for the current process.
#[inline]
pub fn snapshot_process_system() -> ProcessSystemSnapshot {
snapshot_process_resource_and_system().1
}
/// Collect both resource and system snapshots in one sysinfo refresh.
#[inline]
pub fn snapshot_process_resource_and_system() -> (ProcessResourceSnapshot, ProcessSystemSnapshot) {
let mut sampler = process_sampler().lock().unwrap_or_else(|poisoned| poisoned.into_inner());
snapshot_process_resource_and_system_with(&mut sampler)
}
#[inline]
pub fn snapshot_process_resource_and_system_with(
sampler: &mut ProcessSampler,
) -> (ProcessResourceSnapshot, ProcessSystemSnapshot) {
let platform_stats = crate::snapshot_process_platform_stats();
let lock_snapshot = crate::snapshot_process_lock_counts();
sampler
.sys
.refresh_processes_specifics(ProcessesToUpdate::Some(&[sampler.pid]), true, ProcessRefreshKind::everything());
if let Some(process) = sampler.sys.process(sampler.pid) {
let disk_usage = process.disk_usage();
let status = ProcessStatusSnapshot::from(process.status());
let uptime_seconds = process.run_time();
let resource_stats = ProcessResourceSnapshot {
cpu_percent: process.cpu_usage() as f64,
memory_bytes: process.memory(),
uptime_seconds,
};
let process_stats = ProcessSystemSnapshot {
locks_read_total: lock_snapshot.read_locks_held,
locks_write_total: lock_snapshot.write_locks_held,
cpu_total_seconds: process.accumulated_cpu_time() as f64 / 1000.0,
disk_read_bytes: disk_usage.read_bytes,
disk_write_bytes: disk_usage.written_bytes,
file_descriptor_limit_total: process.open_files_limit().map_or(0, |value| value as u64),
file_descriptor_open_total: process.open_files().map_or(0, |value| value as u64),
go_routine_total: process.tasks().map_or(0, |tasks| tasks.len() as u64),
io_rchar_bytes: platform_stats.io_rchar_bytes.unwrap_or(disk_usage.total_read_bytes),
io_read_bytes: platform_stats.io_read_bytes.unwrap_or(disk_usage.total_read_bytes),
io_wchar_bytes: platform_stats.io_wchar_bytes.unwrap_or(disk_usage.total_written_bytes),
io_write_bytes: platform_stats.io_write_bytes.unwrap_or(disk_usage.total_written_bytes),
resident_memory_bytes: process.memory(),
start_time_seconds: process.start_time(),
status,
status_value: status as i64,
syscall_read_total: platform_stats.syscall_read_total.unwrap_or(0),
syscall_write_total: platform_stats.syscall_write_total.unwrap_or(0),
uptime_seconds,
virtual_memory_bytes: process.virtual_memory(),
virtual_memory_max_bytes: platform_stats.virtual_memory_max_bytes.unwrap_or(0),
};
(resource_stats, process_stats)
} else {
(ProcessResourceSnapshot::default(), ProcessSystemSnapshot::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn process_status_snapshot_maps_sysinfo_values() {
assert_eq!(ProcessStatusSnapshot::from(ProcessStatus::Run), ProcessStatusSnapshot::Running);
assert_eq!(ProcessStatusSnapshot::from(ProcessStatus::Sleep), ProcessStatusSnapshot::Sleeping);
assert_eq!(ProcessStatusSnapshot::from(ProcessStatus::Zombie), ProcessStatusSnapshot::Zombie);
}
#[test]
fn combined_snapshot_agrees_with_the_individual_ones_on_per_process_facts() {
// Previously three discarded calls that asserted nothing. The values that
// move (cpu, memory) cannot be compared across calls, but the facts that
// identify the process must not differ by which entry point produced them
// (rustfs/backlog#1836).
let system = snapshot_process_system();
let (_, combined_system) = snapshot_process_resource_and_system();
assert_eq!(
system.start_time_seconds, combined_system.start_time_seconds,
"both entry points describe this process, so its start time cannot differ"
);
assert_eq!(
system.file_descriptor_limit_total, combined_system.file_descriptor_limit_total,
"the descriptor limit is a property of the process, not of the call"
);
assert_eq!(
system.status_value, combined_system.status_value,
"the status enum and its numeric projection must stay in step"
);
assert_eq!(combined_system.status_value, combined_system.status as i64);
}
#[test]
fn independent_samplers_observe_the_same_process() {
let mut sampler_a = ProcessSampler::new();
let mut sampler_b = ProcessSampler::new();
let (_, system_a) = snapshot_process_resource_and_system_with(&mut sampler_a);
let (_, system_b) = snapshot_process_resource_and_system_with(&mut sampler_b);
// Two samplers hold separate sysinfo state; they must still agree on the
// process they are both looking at rather than each inventing a value.
assert_eq!(system_a.start_time_seconds, system_b.start_time_seconds);
assert_eq!(system_a.file_descriptor_limit_total, system_b.file_descriptor_limit_total);
}
}