use serde::{Deserialize, Serialize};
pub const CONTENTION_DIAG_SCHEMA_V1: &str = "ee.diag.contention.v1";
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContentionPosture {
#[default]
Ok,
Warm,
Hot,
Contended,
}
impl ContentionPosture {
#[must_use]
pub fn worst(self, other: Self) -> Self {
if self >= other { self } else { other }
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Ok => "ok",
Self::Warm => "warm",
Self::Hot => "hot",
Self::Contended => "contended",
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WriteLockContention {
pub running: bool,
pub queue_depth: usize,
pub total_processed: u64,
pub avg_wait_ms: f64,
pub max_wait_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_wait_ms_p50: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_wait_ms_p99: Option<u64>,
pub posture: ContentionPosture,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadPoolContention {
pub max_size: usize,
pub active: usize,
pub idle: usize,
pub active_pins: usize,
pub expired_pins: usize,
pub max_seen: usize,
pub drops: u64,
pub release_failures: u64,
pub ad_hoc_bypass_count: u64,
pub acquire_wait_samples: usize,
pub acquire_wait_p50_ns: u128,
pub acquire_wait_p99_ns: u128,
pub size_was_zero: bool,
pub underized: bool,
pub posture: ContentionPosture,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SingleflightContention {
pub status: String,
pub configured_surface_count: u32,
pub active_leader_count: u32,
pub leader_start_count: u64,
pub follower_wait_count: u64,
pub follower_timeout_count: u64,
pub leader_failure_count: u64,
pub reused_result_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub coalesce_efficiency: Option<f64>,
pub posture: ContentionPosture,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupCommitContention {
pub enabled: bool,
pub batches: u64,
pub writes_coalesced: u64,
pub fsync_saved: u64,
pub avg_batch_size: f64,
pub posture: ContentionPosture,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndexIntakeContention {
pub intake_mode: String,
pub rebuilds: u64,
pub swap_stalls: u64,
pub avg_swap_ms: f64,
pub posture: ContentionPosture,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct L2CacheContention {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub hit_rate: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thrash_ratio: Option<f64>,
pub posture: ContentionPosture,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FlockGateContention {
pub acquires: u64,
pub contended_acquires: u64,
pub avg_wait_ms: f64,
pub max_wait_ms: u64,
pub timeouts: u64,
pub posture: ContentionPosture,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContentionFinding {
pub source: String,
pub severity: ContentionPosture,
pub reason_code: String,
pub detail: String,
pub suggested_commands: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContentionSourceGap {
pub source: String,
pub code: String,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ContentionDiagReport {
pub schema_tag: &'static str,
pub overall_posture: ContentionPosture,
pub write_lock: WriteLockContention,
pub read_pool: ReadPoolContention,
pub singleflight: SingleflightContention,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_commit: Option<GroupCommitContention>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index_intake: Option<IndexIntakeContention>,
#[serde(skip_serializing_if = "Option::is_none")]
pub l2_cache: Option<L2CacheContention>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flock_gate: Option<FlockGateContention>,
pub top_contention: Vec<ContentionFinding>,
pub unavailable_sources: Vec<ContentionSourceGap>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn posture_orders_by_severity() {
assert!(ContentionPosture::Ok < ContentionPosture::Warm);
assert!(ContentionPosture::Warm < ContentionPosture::Hot);
assert!(ContentionPosture::Hot < ContentionPosture::Contended);
assert_eq!(
ContentionPosture::Warm.worst(ContentionPosture::Contended),
ContentionPosture::Contended
);
assert_eq!(
ContentionPosture::Hot.worst(ContentionPosture::Ok),
ContentionPosture::Hot
);
}
#[test]
fn posture_str_matches_serde() {
for posture in [
ContentionPosture::Ok,
ContentionPosture::Warm,
ContentionPosture::Hot,
ContentionPosture::Contended,
] {
let json = serde_json::to_string(&posture).expect("serialize posture");
assert_eq!(json, format!("\"{}\"", posture.as_str()));
}
}
}