start_command/
cgroup_memory.rs1use serde::{Deserialize, Deserializer, Serialize};
30use serde_json::Value;
31
32use crate::isolation::isolation_log::shell_quote;
33
34pub const CGROUP_SAMPLE_INTERVAL_SECONDS: u64 = 1;
36
37pub mod cgroup_shell_vars {
39 pub const SAMPLE: &str = "__start_command_cgroup";
41 pub const SAMPLER: &str = "__start_command_cgroup_sampler";
42 pub const FILE: &str = "__start_command_cgroup_file";
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum OomScope {
48 Unknown,
50 ContainerLimit,
52 HostOrParent,
54}
55
56impl OomScope {
57 pub fn as_str(&self) -> &'static str {
58 match self {
59 OomScope::Unknown => "unknown",
60 OomScope::ContainerLimit => "container-limit",
61 OomScope::HostOrParent => "host-or-parent",
62 }
63 }
64
65 pub fn note(&self) -> &'static str {
66 match self {
67 OomScope::Unknown => "OOM kill scope unknown",
68 OomScope::ContainerLimit => "the container hit its own memory limit",
69 OomScope::HostOrParent => "a host-wide or parent cgroup OOM killed processes here",
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct CgroupMemory {
78 pub limit_bytes: Option<u64>,
79 pub peak_bytes: Option<u64>,
80 pub oom_events: Option<u64>,
81 pub oom_kills: Option<u64>,
82}
83
84pub fn build_cgroup_functions_snippet() -> String {
98 r###"__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; }; __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'); case "$__scr_oom:$__scr_kill" in *[!0-9:]*|:*|*:) return 1;; esac; __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"; }"###.to_string()
99}
100
101pub fn build_cgroup_sampler_start_snippet(container_name: &str) -> String {
105 r###"__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; }; __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'); case "$__scr_oom:$__scr_kill" in *[!0-9:]*|:*|*:) return 1;; esac; __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"; }; __start_command_cgroup_file="${TMPDIR:-/tmp}/start-command-cgroup.$"; rm -f "$__start_command_cgroup_file" "$__start_command_cgroup_file.tmp" "$__start_command_cgroup_file.reason"; __start_command_memory_limit=$(docker inspect -f '{{.HostConfig.Memory}}' '__SC_NAME__' 2>/dev/null); __start_command_sample_name='__SC_NAME__'; ( export START_COMMAND_CGROUP_SAMPLING=1; __scs_previous=''; __scs_previous_at=0; __scs_changed=0; __scs_dir=$(__start_command_cgroup_dir '__SC_NAME__'); while :; do __scs_line=""; if [ -n "$__scs_dir" ]; then __scs_line=$(__start_command_cgroup_read "$__scs_dir"); fi; if [ -z "$__scs_line" ]; then __scs_ns=$(docker inspect -f '{{.HostConfig.CgroupnsMode}}' '__SC_NAME__' 2>/dev/null); if [ "$__scs_ns" = private ]; then __scs_remote=$(docker exec '__SC_NAME__' sh -c '__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; }; __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'\''); case "$__scr_oom:$__scr_kill" in *[!0-9:]*|:*|*:) return 1;; esac; __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"; }; __start_command_cgroup_read /sys/fs/cgroup' 2>&1); __scs_exit=$?; if [ "$__scs_exit" = 0 ]; then __scs_line=$__scs_remote; else __scs_detail=$(printf "%s" "$__scs_remote" | sed -n "1p" | cut -c 1-240); printf '%s
106' "local cgroup unavailable; Docker exec exit=$__scs_exit: ${__scs_detail:-private cgroup v2 counters unreadable}" > "$__start_command_cgroup_file.reason"; fi; else printf '%s
107' "local cgroup unavailable; container cgroup namespace=${__scs_ns:-unavailable}, private required" > "$__start_command_cgroup_file.reason"; fi; fi; __scs_now=$(date +%s); if [ -n "$__scs_line" ]; then __scs_kill=$(printf "%s" "$__scs_line" | cut -d " " -f 4); if [ -n "$__scs_previous" ] && [ "$__scs_kill" -gt "$__scs_previous" ] 2>/dev/null && [ "$((__scs_now-__scs_previous_at))" -le 3 ]; then __scs_changed=$__scs_now; fi; __scs_previous=$__scs_kill; __scs_previous_at=$__scs_now; __scs_fields=$(printf "%s" "$__scs_line" | cut -d " " -f 1-4); printf '%s %s %s %s\n' "$__scs_fields" "$__scs_changed" "$__scs_now" "$__scs_dir" > "$__start_command_cgroup_file.tmp" && mv -f "$__start_command_cgroup_file.tmp" "$__start_command_cgroup_file"; fi; if [ "$(docker inspect -f '{{.State.Running}}' '__SC_NAME__' 2>/dev/null)" = false ]; then break; fi; sleep 1; done ) >/dev/null 2>&1 & __start_command_cgroup_sampler=$!"###.replace("'__SC_NAME__'", &shell_quote(container_name))
108}
109
110pub fn build_cgroup_sampler_stop_snippet() -> String {
115 r###"kill "$__start_command_cgroup_sampler" 2>/dev/null; wait "$__start_command_cgroup_sampler" 2>/dev/null; __start_command_cgroup=$(cat "$__start_command_cgroup_file" 2>/dev/null); __start_command_memory_unavailable=$(cat "$__start_command_cgroup_file.reason" 2>/dev/null); __scs_dir=$(printf '%s' "$__start_command_cgroup" | cut -s -d ' ' -f 7-); if [ -n "$__scs_dir" ] && __scs_line=$(__start_command_cgroup_read "$__scs_dir"); then __scs_old=$(printf '%s' "$__start_command_cgroup" | cut -d ' ' -f 4); __scs_at=$(printf '%s' "$__start_command_cgroup" | cut -d ' ' -f 6); __scs_changed=$(printf '%s' "$__start_command_cgroup" | cut -d ' ' -f 5); __scs_now=$(date +%s); __scs_new=$(printf '%s' "$__scs_line" | cut -d ' ' -f 4); if [ "$__scs_new" -gt "$__scs_old" ] 2>/dev/null && [ "$((__scs_now-__scs_at))" -le 3 ]; then __scs_changed=$__scs_now; fi; __start_command_cgroup="$(printf '%s' "$__scs_line" | cut -d ' ' -f 1-4) $__scs_changed $__scs_now"; fi; rm -f "$__start_command_cgroup_file" "$__start_command_cgroup_file.tmp" "$__start_command_cgroup_file.reason"; __start_command_cgroup=$(printf '%s' "$__start_command_cgroup" | cut -d ' ' -f 1-6)"###.to_string()
116}
117
118pub fn build_cgroup_memory_log_snippet(quoted_log_path: &str) -> String {
121 r###"__start_command_cgroup_log() { __scm_note=''; if [ "$4" -gt 0 ] 2>/dev/null; then __scm_note=' (OOM kill scope 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 "$__start_command_cgroup" ]; then __start_command_cgroup_log $__start_command_cgroup >> __SC_LOG__; else printf 'Memory: unavailable (%s) memory.limit=%s (HostConfig)\n' "${__start_command_memory_unavailable:-sampler stopped before a reading; local cgroup unavailable}" "${__start_command_memory_limit:-unknown}" >> __SC_LOG__; fi"###.replace("__SC_LOG__", quoted_log_path)
122}
123
124fn parse_counter(text: &str) -> Option<u64> {
125 if !text.is_empty() && text.chars().all(|c| c.is_ascii_digit()) {
126 text.parse().ok()
127 } else {
128 None
129 }
130}
131
132pub fn parse_cgroup_memory_sample(text: &str) -> Option<CgroupMemory> {
135 let fields: Vec<&str> = text.split_whitespace().collect();
136 if fields.len() < 4 {
137 return None;
138 }
139 let counters = CgroupMemory {
140 limit_bytes: parse_counter(fields[0]),
141 peak_bytes: parse_counter(fields[1]),
142 oom_events: parse_counter(fields[2]),
143 oom_kills: parse_counter(fields[3]),
144 };
145 if counters.oom_events.is_none() && counters.oom_kills.is_none() {
146 return None;
148 }
149 Some(counters)
150}
151
152pub fn format_cgroup_memory_log_line(text: &str) -> Option<String> {
155 let counters = parse_cgroup_memory_sample(text)?;
156 let fields: Vec<&str> = text.split_whitespace().collect();
157 let note = describe_cgroup_oom_scope(&counters)
158 .map(|scope| format!(" ({})", scope.note()))
159 .unwrap_or_default();
160 Some(format!(
161 "Memory: memory.max={} memory.peak={} oom={} oom_kill={}{}",
162 fields[0], fields[1], fields[2], fields[3], note
163 ))
164}
165
166pub fn describe_cgroup_oom_scope(memory: &CgroupMemory) -> Option<OomScope> {
171 memory.oom_kills.filter(|kills| *kills > 0)?;
172 Some(OomScope::Unknown)
173}
174
175fn format_bytes(bytes: Option<u64>) -> String {
176 let Some(bytes) = bytes else {
177 return "unknown".to_string();
178 };
179 const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
180 let mut value = bytes as f64;
181 let mut unit = 0;
182 while value >= 1024.0 && unit < UNITS.len() - 1 {
183 value /= 1024.0;
184 unit += 1;
185 }
186 if unit == 0 {
187 format!("{} B", bytes)
188 } else {
189 format!("{:.1} {}", value, UNITS[unit])
190 }
191}
192
193pub fn format_cgroup_memory(memory: &CgroupMemory) -> String {
196 let limit = match memory.limit_bytes {
197 Some(_) => format!("{} limit", format_bytes(memory.limit_bytes)),
198 None => "no limit".to_string(),
199 };
200 let count = |value: Option<u64>| value.map_or_else(|| "unknown".to_string(), |v| v.to_string());
201 let note = describe_cgroup_oom_scope(memory)
202 .map(|scope| format!(" ({})", scope.note()))
203 .unwrap_or_default();
204 format!(
205 "peak {} of {}, oom {}, oom_kill {}{}",
206 format_bytes(memory.peak_bytes),
207 limit,
208 count(memory.oom_events),
209 count(memory.oom_kills),
210 note
211 )
212}
213
214fn counter_from_value(value: Option<&Value>) -> Option<u64> {
215 match value? {
216 Value::Number(number) => number.as_u64().or_else(|| {
217 number
218 .as_f64()
219 .filter(|v| v.fract() == 0.0 && *v >= 0.0 && *v <= 9_007_199_254_740_991.0)
220 .map(|v| v as u64)
221 }),
222 Value::String(text) => parse_counter(text.trim()),
223 _ => None,
224 }
225}
226
227pub fn normalize_cgroup_memory(value: &Value) -> Option<CgroupMemory> {
230 let object = value.as_object()?;
231 Some(CgroupMemory {
232 limit_bytes: counter_from_value(object.get("limitBytes")),
233 peak_bytes: counter_from_value(object.get("peakBytes")),
234 oom_events: counter_from_value(object.get("oomEvents")),
235 oom_kills: counter_from_value(object.get("oomKills")),
236 })
237}
238
239pub fn deserialize_cgroup_memory<'de, D>(deserializer: D) -> Result<Option<CgroupMemory>, D::Error>
241where
242 D: Deserializer<'de>,
243{
244 let value = Option::<Value>::deserialize(deserializer)?;
245 Ok(value.as_ref().and_then(normalize_cgroup_memory))
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn parses_counters_and_treats_max_as_no_limit() {
254 assert_eq!(
255 parse_cgroup_memory_sample("max 1024 0 2"),
256 Some(CgroupMemory {
257 limit_bytes: None,
258 peak_bytes: Some(1024),
259 oom_events: Some(0),
260 oom_kills: Some(2),
261 })
262 );
263 assert_eq!(parse_cgroup_memory_sample("max - - -"), None);
264 assert_eq!(parse_cgroup_memory_sample("1 2 3"), None);
265 }
266
267 #[test]
268 fn normalizes_floats_strings_and_nulls() {
269 let value = serde_json::json!({
270 "limitBytes": 268435456.0,
271 "peakBytes": "1024",
272 "oomEvents": null,
273 "oomKills": -1
274 });
275 assert_eq!(
276 normalize_cgroup_memory(&value),
277 Some(CgroupMemory {
278 limit_bytes: Some(268_435_456),
279 peak_bytes: Some(1024),
280 oom_events: None,
281 oom_kills: None,
282 })
283 );
284 assert_eq!(normalize_cgroup_memory(&Value::Null), None);
285 }
286}