use super::HEX_KEY_PREFIX;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum MissingStatsReason {
NoSchedulerBinary,
NoScheduler { reason: String },
DuringFreeze,
Cancelled,
SchedulerError { errno: i32, args: serde_json::Value },
MissingResp { args: serde_json::Value },
RequestTooLarge { size: usize, max: usize },
ResponseTooLarge { size: usize, max: usize },
MutexPoisoned,
}
impl std::fmt::Display for MissingStatsReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoSchedulerBinary => {
write!(f, "no scheduler_binary configured for this run")
}
Self::NoScheduler { reason } => {
write!(f, "guest relay reports no scheduler: {reason}")
}
Self::DuringFreeze => {
write!(
f,
"stats request cancelled — freeze coordinator paused the scheduler"
)
}
Self::Cancelled => {
write!(
f,
"stats request cancelled — run-wide cancel flag was set (watchdog or shutdown)"
)
}
Self::SchedulerError { errno, args } => {
write!(f, "scheduler returned errno={errno} (args={args})")
}
Self::MissingResp { args } => {
write!(f, "scheduler envelope missing 'resp' key (args={args})")
}
Self::RequestTooLarge { size, max } => {
write!(f, "stats request {size} bytes exceeds {max}-byte cap")
}
Self::ResponseTooLarge { size, max } => {
write!(f, "stats response {size} bytes exceeds {max}-byte cap")
}
Self::MutexPoisoned => {
write!(f, "stats client response mutex was poisoned")
}
}
}
}
impl From<&anyhow::Error> for MissingStatsReason {
fn from(e: &anyhow::Error) -> Self {
if let Some(typed) = e.downcast_ref::<crate::vmm::sched_stats::SchedStatsError>() {
return Self::from(typed);
}
Self::NoScheduler {
reason: e.to_string(),
}
}
}
impl From<&crate::vmm::sched_stats::SchedStatsError> for MissingStatsReason {
fn from(e: &crate::vmm::sched_stats::SchedStatsError) -> Self {
use crate::vmm::sched_stats::SchedStatsError as S;
match e {
S::Poisoned => Self::MutexPoisoned,
S::RequestTooLarge { size, max } => Self::RequestTooLarge {
size: *size,
max: *max,
},
S::ResponseTooLarge { size, max } => Self::ResponseTooLarge {
size: *size,
max: *max,
},
S::DuringFreeze => Self::DuringFreeze,
S::Cancelled => Self::Cancelled,
S::NoScheduler { reason } => Self::NoScheduler {
reason: reason.clone(),
},
S::SchedulerError { errno, args } => Self::SchedulerError {
errno: *errno,
args: args.clone(),
},
S::MissingResp { args } => Self::MissingResp { args: args.clone() },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct ExcludedMap {
pub name: String,
pub map_kva: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum SnapshotError {
MapNotFound {
requested: String,
available: Vec<String>,
},
VarNotFound {
requested: String,
available: Vec<String>,
},
AmbiguousVar {
requested: String,
found_in: Vec<String>,
},
FieldNotFound {
requested: String,
walked: String,
component: String,
available: Vec<String>,
},
NotAStruct {
requested: String,
walked: String,
component: String,
kind: String,
},
TypeMismatch {
expected: String,
actual: String,
requested: String,
},
IndexOutOfRange {
map: String,
index: usize,
len: usize,
},
PerCpuSlot {
map: String,
cpu: u32,
len: usize,
unmapped: bool,
},
NoMatch {
map: String,
op: String,
len: usize,
available_keys: Vec<String>,
},
EmptyPathComponent { requested: String },
PerCpuNotNarrowed { map: String },
NoRendered { map: String, side: String },
PlaceholderSample { tag: String, reason: String },
MissingStats {
tag: String,
reason: MissingStatsReason,
},
HostFieldUnavailable { tag: String, cpu: u32 },
PlaceholderSnapshot { tag: Option<String> },
NoActiveScheduler { reason: String },
ActiveFilterExcludedMaps {
requested: String,
active_obj: String,
excluded_maps: Vec<ExcludedMap>,
whitelist_kvas: Vec<u64>,
},
WalkerDriftedWithinPhase {
phase: crate::assert::Phase,
pinned_kvas: Vec<u64>,
sample_kvas: Vec<u64>,
requested: String,
},
ProjectionFailed { reason: String },
}
impl std::fmt::Display for SnapshotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SnapshotError::MapNotFound {
requested,
available,
} => {
write!(
f,
"snapshot has no map '{requested}' (captured maps: {available:?})"
)
}
SnapshotError::VarNotFound {
requested,
available,
} => {
write!(
f,
"snapshot has no global variable '{requested}' in any \
*.bss/*.data/*.rodata map (available globals: {available:?})"
)
}
SnapshotError::AmbiguousVar {
requested,
found_in,
} => {
write!(
f,
"snapshot global '{requested}' is ambiguous (found in \
{found_in:?}); use Snapshot::active().var(name) (or the \
shorthand Snapshot::live_var(name)) to pick the active \
scheduler's copy automatically, or Snapshot::map(name) \
to address a specific scheduler's bss explicitly"
)
}
SnapshotError::FieldNotFound {
requested,
walked,
component,
available,
} => {
write!(
f,
"path '{requested}': component '{component}' (after walking '{walked}') \
not found (members at this depth: {available:?})"
)
}
SnapshotError::NotAStruct {
requested,
walked,
component,
kind,
} => {
write!(
f,
"path '{requested}': component '{component}' (after walking '{walked}') \
expected a Struct, got {kind}"
)
}
SnapshotError::TypeMismatch {
expected,
actual,
requested,
} => {
write!(
f,
"path '{requested}': cannot read as {expected} — actual rendered \
variant is {actual}"
)
}
SnapshotError::IndexOutOfRange { map, index, len } => {
write!(f, "map '{map}': index {index} out of range (length {len})")
}
SnapshotError::PerCpuSlot {
map,
cpu,
len,
unmapped,
} => {
if *unmapped {
write!(f, "map '{map}': cpu {cpu} per-CPU slot is unmapped (None)")
} else {
write!(
f,
"map '{map}': cpu {cpu} out of range (have {len} per-CPU slots)"
)
}
}
SnapshotError::NoMatch {
map,
op,
len,
available_keys,
} => {
if *len == 0 {
write!(f, "map '{map}': {op} matched no entries (map is empty)")
} else if available_keys.is_empty() {
write!(
f,
"map '{map}': {op} matched none of {len} entries (sample keys unavailable)"
)
} else {
write!(
f,
"map '{map}': {op} matched none of {len} entries (first {sampled}: {available_keys:?})",
sampled = available_keys.len(),
)?;
if available_keys.iter().all(|k| k.starts_with(HEX_KEY_PREFIX)) {
write!(
f,
" (BTF missing at capture — keys shown as hex bytes; \
rebuild guest kernel with CONFIG_DEBUG_INFO_BTF=y for \
typed keys)"
)?;
}
Ok(())
}
}
SnapshotError::EmptyPathComponent { requested } => {
write!(
f,
"path '{requested}' has an empty component (consecutive '.')"
)
}
SnapshotError::PerCpuNotNarrowed { map } => {
write!(
f,
"map '{map}': per-CPU entry without a CPU narrow — call .cpu(N) first"
)
}
SnapshotError::NoRendered { map, side } => {
write!(
f,
"map '{map}': {side} has no rendered structure (no BTF type at capture time)"
)
}
SnapshotError::PlaceholderSample { tag, reason } => {
write!(
f,
"sample '{tag}' is a placeholder report (capture pipeline did not land): \
{reason}"
)
}
SnapshotError::MissingStats { tag, reason } => {
write!(f, "sample '{tag}': stats absent ({reason})")
}
SnapshotError::HostFieldUnavailable { tag, cpu } => {
write!(
f,
"sample '{tag}': per_cpu_time has no entry for cpu {cpu} \
(placeholder report or kernel-walker resolution failure)"
)
}
SnapshotError::PlaceholderSnapshot { tag } => match tag {
Some(t) => write!(
f,
"snapshot '{t}' is a placeholder — the freeze-rendezvous \
path could not capture real data; no maps to walk"
),
None => f.write_str(
"snapshot is a placeholder — the freeze-rendezvous path \
could not capture real data; no maps to walk",
),
},
SnapshotError::NoActiveScheduler { reason } => {
write!(
f,
"snapshot has no currently-active scheduler ({reason}); \
use Snapshot::vars(name) to enumerate every observed \
copy explicitly, Snapshot::live_var(name) to keep the \
typed error path while opting into the active filter, \
or Snapshot::map(\"<obj>.<section>\") to address a \
specific scheduler's bss directly"
)
}
SnapshotError::ActiveFilterExcludedMaps {
requested,
active_obj,
excluded_maps,
whitelist_kvas,
} => {
let excluded_rendered = excluded_maps
.iter()
.map(|m| format!("{}@{:#x}", m.name, m.map_kva))
.collect::<Vec<_>>()
.join(", ");
let some_zero = excluded_maps.iter().any(|m| m.map_kva == 0);
let some_alias = excluded_maps
.iter()
.any(|m| m.map_kva != 0 && !whitelist_kvas.contains(&m.map_kva));
let cause = match (some_zero, some_alias) {
(false, true) => {
"this snapshot pre-dates your most recent \
Op::ReplaceScheduler / Op::AttachScheduler — \
wait for the next periodic boundary (or re-run \
the test) so the walker re-publishes the live \
scheduler's KVAs"
}
(true, false) => {
"the captured maps have no recorded KVAs — \
the snapshot pre-dates the walker plumbing, \
or the capture path failed to record per-map KVAs"
}
(true, true) => {
"some captured maps lack KVAs and some disagree \
with the walker's whitelist — both \
pre-walker-capture state and a post-swap window \
can produce this; re-run the test to regenerate \
the snapshot"
}
(false, false) => "captured KVAs were neither absent nor in disagreement",
};
write!(
f,
"snapshot lookup '{requested}' returned no hits under the \
active filter (obj='{active_obj}'): the walker's KVA \
whitelist {whitelist_kvas:#x?} excluded {n} captured map(s) \
sharing the obj prefix: {excluded_rendered} — {cause}. \
Reach for Snapshot::vars('{requested}') to enumerate every \
copy across all obj prefixes, or Snapshot::map(\"<name>\") \
to address one of the excluded maps directly.",
n = excluded_maps.len(),
)
}
SnapshotError::WalkerDriftedWithinPhase {
phase,
pinned_kvas,
sample_kvas,
requested,
} => {
write!(
f,
"walker drift within {phase:?}: lookup '{requested}' resolved against \
KVA set {sample_kvas:#x?}, but an earlier same-phase snapshot pinned \
{pinned_kvas:#x?}. The walker re-published mid-phase (typical cause: \
a post-Op::ReplaceScheduler swap window). The drifted sample is \
surfaced as Err so per-phase reducers (counter_delta_per_phase, \
ratio_across_phases) see monotonic Ok-sequences from one walker \
decision; address by stepping the phase past the swap settle window \
or by reading via the explicit picker form."
)
}
SnapshotError::ProjectionFailed { reason } => {
write!(f, "projection failed: {reason}")
}
}
}
}
impl std::error::Error for SnapshotError {}
pub type SnapshotResult<T> = std::result::Result<T, SnapshotError>;
#[derive(Debug)]
#[non_exhaustive]
pub struct DrainedSnapshotEntry {
pub tag: String,
pub report: crate::monitor::dump::FailureDumpReport,
pub stats: std::result::Result<serde_json::Value, MissingStatsReason>,
pub elapsed_ms: Option<u64>,
pub step_index: Option<u16>,
}
#[cfg(test)]
mod tests_api_gaps {
use super::*;
#[test]
fn projection_failed_display_carries_reason() {
let e = SnapshotError::ProjectionFailed {
reason: "live_var_via picker rejected all 2 candidates".to_string(),
};
let rendered = format!("{e}");
assert_eq!(
rendered,
"projection failed: live_var_via picker rejected all 2 candidates"
);
}
#[test]
fn projection_failed_eq_and_hash_round_trip() {
let a = SnapshotError::ProjectionFailed {
reason: "x".to_string(),
};
let b = a.clone();
assert_eq!(a, b);
let mut seen = std::collections::HashSet::new();
seen.insert(a);
assert!(seen.contains(&b));
}
}