use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use crate::isolation::isolation_log::shell_quote;
pub const CGROUP_SAMPLE_INTERVAL_SECONDS: u64 = 1;
pub mod cgroup_shell_vars {
pub const SAMPLE: &str = "__start_command_cgroup";
pub const SAMPLER: &str = "__start_command_cgroup_sampler";
pub const FILE: &str = "__start_command_cgroup_file";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OomScope {
Unknown,
ContainerLimit,
HostOrParent,
}
impl OomScope {
pub fn as_str(&self) -> &'static str {
match self {
OomScope::Unknown => "unknown",
OomScope::ContainerLimit => "container-limit",
OomScope::HostOrParent => "host-or-parent",
}
}
pub fn note(&self) -> &'static str {
match self {
OomScope::Unknown => "OOM kill scope unknown",
OomScope::ContainerLimit => "the container hit its own memory limit",
OomScope::HostOrParent => "a host-wide or parent cgroup OOM killed processes here",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CgroupMemory {
pub limit_bytes: Option<u64>,
pub peak_bytes: Option<u64>,
pub oom_events: Option<u64>,
pub oom_kills: Option<u64>,
}
pub fn build_cgroup_functions_snippet() -> String {
[
concat!(
"__start_command_cgroup_dir() { __scd_id=$(docker inspect -f '{{.Id}}' \"$1\" 2>/dev/null); ",
"[ -n \"$__scd_id\" ] || return 1; ",
"__scd_pid=$(docker inspect -f '{{.State.Pid}}' \"$1\" 2>/dev/null); __scd_rel=''; ",
"case \"$__scd_pid\" in ''|0|*[!0-9]*) ;; ",
"*) __scd_rel=$(sed -n 's/^0:://p' \"${START_COMMAND_PROC_ROOT:-/proc}/$__scd_pid/cgroup\" 2>/dev/null);; esac; ",
"__scd_root=${START_COMMAND_CGROUP_ROOT:-/sys/fs/cgroup}; ",
"for __scd_dir in ${__scd_rel:+\"$__scd_root$__scd_rel\"} ",
"\"$__scd_root/system.slice/docker-$__scd_id.scope\" \"$__scd_root/docker/$__scd_id\"; do ",
"case \"$__scd_dir\" in *\"$__scd_id\"*) if [ -r \"$__scd_dir/memory.events\" ]; then ",
"printf '%s' \"$__scd_dir\"; return 0; fi;; esac; done; return 1; }",
),
concat!(
"__start_command_cgroup_read() { __scr_events=$(cat \"$1/memory.events\" 2>/dev/null) || return 1; ",
"__scr_oom=$(printf '%s\\n' \"$__scr_events\" | sed -n 's/^oom //p'); ",
"__scr_kill=$(printf '%s\\n' \"$__scr_events\" | sed -n 's/^oom_kill //p'); ",
"__scr_max=$(cat \"$1/memory.max\" 2>/dev/null); __scr_peak=$(cat \"$1/memory.peak\" 2>/dev/null); ",
"printf '%s %s %s %s %s\\n' \"${__scr_max:--}\" \"${__scr_peak:--}\" ",
"\"${__scr_oom:--}\" \"${__scr_kill:--}\" \"$1\"; }",
),
]
.join("; ")
}
pub fn build_cgroup_sampler_start_snippet(container_name: &str) -> String {
use cgroup_shell_vars::{FILE, SAMPLER};
[
build_cgroup_functions_snippet(),
format!("{}=\"${{TMPDIR:-/tmp}}/start-command-cgroup.$$\"", FILE),
format!("rm -f \"${f}\" \"${f}.tmp\"", f = FILE),
format!(
"( __scs_dir=$(__start_command_cgroup_dir {name}) || exit 0; \
while __scs_line=$(__start_command_cgroup_read \"$__scs_dir\"); do \
printf '%s\\n' \"$__scs_line\" > \"${f}.tmp\" && mv -f \"${f}.tmp\" \"${f}\"; \
sleep {interval}; done ) >/dev/null 2>&1 & {sampler}=$!",
name = shell_quote(container_name),
f = FILE,
interval = CGROUP_SAMPLE_INTERVAL_SECONDS,
sampler = SAMPLER,
),
]
.join("; ")
}
pub fn build_cgroup_sampler_stop_snippet() -> String {
use cgroup_shell_vars::{FILE, SAMPLE, SAMPLER};
[
format!("kill \"${}\" 2>/dev/null", SAMPLER),
format!("wait \"${}\" 2>/dev/null", SAMPLER),
format!("{}=$(cat \"${}\" 2>/dev/null)", SAMPLE, FILE),
format!("__scs_dir=$(printf '%s' \"${}\" | cut -s -d ' ' -f 5-)", SAMPLE),
format!(
"if [ -n \"$__scs_dir\" ] && __scs_line=$(__start_command_cgroup_read \"$__scs_dir\"); then {}=$__scs_line; fi",
SAMPLE
),
format!("rm -f \"${f}\" \"${f}.tmp\"", f = FILE),
format!(
"{s}=$(printf '%s' \"${s}\" | cut -d ' ' -f 1-4)",
s = SAMPLE
),
]
.join("; ")
}
pub fn build_cgroup_memory_log_snippet(quoted_log_path: &str) -> String {
let sample = cgroup_shell_vars::SAMPLE;
format!(
"__start_command_cgroup_log() {{ __scm_note=''; \
if [ \"$4\" -gt 0 ] 2>/dev/null; then __scm_note=' ({unknown})'; fi; \
printf 'Memory: memory.max=%s memory.peak=%s oom=%s oom_kill=%s%s\\n' \
\"$1\" \"$2\" \"$3\" \"$4\" \"$__scm_note\"; }}; \
if [ -n \"${sample}\" ]; then __start_command_cgroup_log ${sample} >> {log}; fi",
unknown = OomScope::Unknown.note(),
sample = sample,
log = quoted_log_path,
)
}
fn parse_counter(text: &str) -> Option<u64> {
if !text.is_empty() && text.chars().all(|c| c.is_ascii_digit()) {
text.parse().ok()
} else {
None
}
}
pub fn parse_cgroup_memory_sample(text: &str) -> Option<CgroupMemory> {
let fields: Vec<&str> = text.split_whitespace().collect();
if fields.len() < 4 {
return None;
}
let counters = CgroupMemory {
limit_bytes: parse_counter(fields[0]),
peak_bytes: parse_counter(fields[1]),
oom_events: parse_counter(fields[2]),
oom_kills: parse_counter(fields[3]),
};
if counters.oom_events.is_none() && counters.oom_kills.is_none() {
return None;
}
Some(counters)
}
pub fn format_cgroup_memory_log_line(text: &str) -> Option<String> {
let counters = parse_cgroup_memory_sample(text)?;
let fields: Vec<&str> = text.split_whitespace().collect();
let note = describe_cgroup_oom_scope(&counters)
.map(|scope| format!(" ({})", scope.note()))
.unwrap_or_default();
Some(format!(
"Memory: memory.max={} memory.peak={} oom={} oom_kill={}{}",
fields[0], fields[1], fields[2], fields[3], note
))
}
pub fn describe_cgroup_oom_scope(memory: &CgroupMemory) -> Option<OomScope> {
memory.oom_kills.filter(|kills| *kills > 0)?;
Some(OomScope::Unknown)
}
fn format_bytes(bytes: Option<u64>) -> String {
let Some(bytes) = bytes else {
return "unknown".to_string();
};
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut value = bytes as f64;
let mut unit = 0;
while value >= 1024.0 && unit < UNITS.len() - 1 {
value /= 1024.0;
unit += 1;
}
if unit == 0 {
format!("{} B", bytes)
} else {
format!("{:.1} {}", value, UNITS[unit])
}
}
pub fn format_cgroup_memory(memory: &CgroupMemory) -> String {
let limit = match memory.limit_bytes {
Some(_) => format!("{} limit", format_bytes(memory.limit_bytes)),
None => "no limit".to_string(),
};
let count = |value: Option<u64>| value.map_or_else(|| "unknown".to_string(), |v| v.to_string());
let note = describe_cgroup_oom_scope(memory)
.map(|scope| format!(" ({})", scope.note()))
.unwrap_or_default();
format!(
"peak {} of {}, oom {}, oom_kill {}{}",
format_bytes(memory.peak_bytes),
limit,
count(memory.oom_events),
count(memory.oom_kills),
note
)
}
fn counter_from_value(value: Option<&Value>) -> Option<u64> {
match value? {
Value::Number(number) => number.as_u64().or_else(|| {
number
.as_f64()
.filter(|v| v.fract() == 0.0 && *v >= 0.0 && *v <= 9_007_199_254_740_991.0)
.map(|v| v as u64)
}),
Value::String(text) => parse_counter(text.trim()),
_ => None,
}
}
pub fn normalize_cgroup_memory(value: &Value) -> Option<CgroupMemory> {
let object = value.as_object()?;
Some(CgroupMemory {
limit_bytes: counter_from_value(object.get("limitBytes")),
peak_bytes: counter_from_value(object.get("peakBytes")),
oom_events: counter_from_value(object.get("oomEvents")),
oom_kills: counter_from_value(object.get("oomKills")),
})
}
pub fn deserialize_cgroup_memory<'de, D>(deserializer: D) -> Result<Option<CgroupMemory>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<Value>::deserialize(deserializer)?;
Ok(value.as_ref().and_then(normalize_cgroup_memory))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_counters_and_treats_max_as_no_limit() {
assert_eq!(
parse_cgroup_memory_sample("max 1024 0 2"),
Some(CgroupMemory {
limit_bytes: None,
peak_bytes: Some(1024),
oom_events: Some(0),
oom_kills: Some(2),
})
);
assert_eq!(parse_cgroup_memory_sample("max - - -"), None);
assert_eq!(parse_cgroup_memory_sample("1 2 3"), None);
}
#[test]
fn normalizes_floats_strings_and_nulls() {
let value = serde_json::json!({
"limitBytes": 268435456.0,
"peakBytes": "1024",
"oomEvents": null,
"oomKills": -1
});
assert_eq!(
normalize_cgroup_memory(&value),
Some(CgroupMemory {
limit_bytes: Some(268_435_456),
peak_bytes: Some(1024),
oom_events: None,
oom_kills: None,
})
);
assert_eq!(normalize_cgroup_memory(&Value::Null), None);
}
}