use std::path::{Path, PathBuf};
pub const IMPORT_BYTES_PER_OBJECT: u64 = 512;
pub const IMPORT_FIXED_BYTES: u64 = 512 * 1024 * 1024;
pub const MARGIN_PERCENT: u64 = 20;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LimitSource {
Box,
Cgroup(PathBuf),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MemoryLimit {
pub bytes: u64,
pub source: LimitSource,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CgroupMemory {
pub max: u64,
pub current: u64,
pub reclaimable: u64,
}
impl CgroupMemory {
pub fn available(&self) -> u64 {
self.max
.saturating_sub(self.current)
.saturating_add(self.reclaimable)
.min(self.max)
}
}
impl MemoryLimit {
pub fn read() -> Option<MemoryLimit> {
Self::read_from(Path::new("/proc/meminfo"), own_cgroup_dir().as_deref())
}
pub fn read_from(meminfo: &Path, cgroup_dir: Option<&Path>) -> Option<MemoryLimit> {
let total = std::fs::read_to_string(meminfo)
.ok()
.and_then(|t| crate::dashboard::probe::parse_meminfo(&t))
.map(|(total, _)| total)
.filter(|t| *t > 0);
let cgroup = cgroup_dir.and_then(tightest_cgroup);
match (total, cgroup) {
(Some(total), Some((dir, c))) if c.max < total => Some(MemoryLimit {
bytes: c.max,
source: LimitSource::Cgroup(dir),
}),
(None, Some((dir, c))) => Some(MemoryLimit {
bytes: c.max,
source: LimitSource::Cgroup(dir),
}),
(Some(total), _) => Some(MemoryLimit {
bytes: total,
source: LimitSource::Box,
}),
(None, None) => None,
}
}
pub fn cgroup_dir(&self) -> Option<&Path> {
match &self.source {
LimitSource::Cgroup(dir) => Some(dir),
LimitSource::Box => None,
}
}
pub fn words(&self) -> String {
let source = match self.source {
LimitSource::Box => "this box's MemTotal",
LimitSource::Cgroup(_) => "this server's cgroup memory.max",
};
format!("{} ({source})", crate::budget::human(self.bytes))
}
}
pub fn own_cgroup_dir() -> Option<PathBuf> {
let own = std::fs::read_to_string("/proc/self/cgroup").ok()?;
let rel = own
.lines()
.find_map(|line| line.strip_prefix("0::"))?
.trim()
.trim_start_matches('/');
Some(Path::new("/sys/fs/cgroup").join(rel))
}
pub fn tightest_cgroup(leaf: &Path) -> Option<(PathBuf, CgroupMemory)> {
let mut best: Option<(PathBuf, CgroupMemory)> = None;
let mut dir = Some(leaf);
while let Some(d) = dir {
if !d.join("memory.max").exists() {
break;
}
if let Some(c) = cgroup_memory_in(d) {
if best.as_ref().is_none_or(|(_, b)| c.max < b.max) {
best = Some((d.to_path_buf(), c));
}
}
dir = d.parent();
}
best
}
pub fn cgroup_memory_in(dir: &Path) -> Option<CgroupMemory> {
let max = std::fs::read_to_string(dir.join("memory.max")).ok()?;
let max: u64 = match max.trim() {
"max" => return None,
n => n.parse().ok()?,
};
let current: u64 = std::fs::read_to_string(dir.join("memory.current"))
.ok()?
.trim()
.parse()
.ok()?;
let stat = std::fs::read_to_string(dir.join("memory.stat")).unwrap_or_default();
let field = |name: &str| -> u64 {
stat.lines()
.find_map(|l| l.strip_prefix(name).and_then(|r| r.strip_prefix(' ')))
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0)
};
let reclaimable = (field("active_file") + field("inactive_file"))
.saturating_sub(field("file_dirty") + field("file_writeback"));
Some(CgroupMemory {
max,
current,
reclaimable,
})
}
pub fn cgroup_meminfo(c: &CgroupMemory) -> String {
format!(
"MemTotal: {} kB\nMemAvailable: {} kB\n",
c.max / 1024,
c.available() / 1024
)
}
pub const fn import_need(objects: u64) -> u64 {
IMPORT_FIXED_BYTES + objects * IMPORT_BYTES_PER_OBJECT
}
pub const fn import_ceiling(limit: u64) -> u64 {
limit - limit / 100 * MARGIN_PERCENT
}
pub fn import_room(path: &str, objects: u64, limit: &MemoryLimit) -> Result<(), String> {
let need = import_need(objects);
let ceiling = import_ceiling(limit.bytes);
if need <= ceiling {
return Ok(());
}
Err(format!(
"[import-memory] {path}: the fetched pack has {objects} objects, and indexing it needs \
about {} of memory; this server may use {}, and an import keeps {MARGIN_PERCENT} % of \
that free, so at most {}. Nothing was imported and the repository was not kept. A \
server with more memory imports it.",
crate::budget::human(need),
limit.words(),
crate::budget::human(ceiling),
))
}
#[cfg(test)]
mod tests {
use super::*;
const GIB: u64 = 1024 * 1024 * 1024;
const LINUX_OBJECTS: u64 = 11_988_094;
fn fake_box(dir: &Path, total: u64) -> PathBuf {
let p = dir.join("meminfo");
std::fs::write(
&p,
format!(
"MemTotal: {} kB\nMemFree: 1 kB\nMemAvailable: {} kB\n",
total / 1024,
total / 1024
),
)
.unwrap();
p
}
fn fake_cgroup(dir: &Path, max: &str, current: u64, stat: &str) -> PathBuf {
let cg = dir.join("cg");
std::fs::create_dir_all(&cg).unwrap();
std::fs::write(cg.join("memory.max"), format!("{max}\n")).unwrap();
std::fs::write(cg.join("memory.current"), format!("{current}\n")).unwrap();
std::fs::write(cg.join("memory.stat"), stat).unwrap();
cg
}
#[test]
fn a_cgroup_limit_below_the_box_is_the_limit() {
let d = tempfile::tempdir().unwrap();
let meminfo = fake_box(d.path(), 500 * GIB);
let cg = fake_cgroup(d.path(), &(6 * GIB).to_string(), GIB, "anon 1\n");
let limit = MemoryLimit::read_from(&meminfo, Some(&cg)).expect("a limit");
assert_eq!(
limit.bytes,
6 * GIB,
"a 6 GiB cgroup on a 500 GiB host may use 6 GiB: {limit:?}"
);
assert_eq!(limit.source, LimitSource::Cgroup(cg.clone()));
assert!(
limit.words().contains("cgroup memory.max"),
"{}",
limit.words()
);
}
#[test]
fn without_a_cgroup_limit_the_box_is_the_limit() {
let d = tempfile::tempdir().unwrap();
let meminfo = fake_box(d.path(), 16 * GIB);
let cg = fake_cgroup(d.path(), "max", GIB, "");
assert_eq!(
MemoryLimit::read_from(&meminfo, Some(&cg)),
Some(MemoryLimit {
bytes: 16 * GIB,
source: LimitSource::Box
})
);
let loose = fake_cgroup(d.path(), &(64 * GIB).to_string(), GIB, "");
assert_eq!(
MemoryLimit::read_from(&meminfo, Some(&loose)).map(|l| l.bytes),
Some(16 * GIB)
);
assert_eq!(
MemoryLimit::read_from(&meminfo, None).map(|l| l.source),
Some(LimitSource::Box)
);
}
#[test]
fn linux_fits_the_mini_with_the_margin_and_the_model_covers_the_measurement() {
for (measured, what) in [
(5172 * 1024 * 1024, "the Mini VM's anonymous peak"),
(6_238_872 * 1024, "the anon-rss of the OOM kill"),
] {
assert!(
import_need(LINUX_OBJECTS) >= measured,
"the budget for linux ({}) is below {what} ({})",
crate::budget::human(import_need(LINUX_OBJECTS)),
crate::budget::human(measured)
);
}
let mini = MemoryLimit {
bytes: 16 * GIB,
source: LimitSource::Box,
};
assert_eq!(import_room("alice/linux", LINUX_OBJECTS, &mini), Ok(()));
}
#[test]
fn linux_in_a_6g_cgroup_is_refused_by_name_before_the_index() {
let d = tempfile::tempdir().unwrap();
let meminfo = fake_box(d.path(), 500 * GIB);
let cg = fake_cgroup(d.path(), &(6 * GIB).to_string(), GIB, "");
let limit = MemoryLimit::read_from(&meminfo, Some(&cg)).unwrap();
let err =
import_room("alice/linux", LINUX_OBJECTS, &limit).expect_err("must not fit 6 GiB");
for word in [
"[import-memory]",
"alice/linux",
"11988094 objects",
"6.0G",
"cgroup memory.max",
"4.8G",
] {
assert!(err.contains(word), "the refusal must say {word:?}: {err}");
}
assert_eq!(import_room("alice/small", 100_000, &limit), Ok(()));
}
#[test]
fn a_parent_cgroups_limit_is_the_limit() {
let d = tempfile::tempdir().unwrap();
let meminfo = fake_box(d.path(), 500 * GIB);
let slice = fake_cgroup(d.path(), &(6 * GIB).to_string(), GIB, "anon 1\n");
let leaf = fake_cgroup(&slice, "max", GIB, "anon 1\n");
let limit = MemoryLimit::read_from(&meminfo, Some(&leaf)).expect("a limit");
assert_eq!(limit.bytes, 6 * GIB, "the slice's MemoryMax");
assert_eq!(
limit.cgroup_dir(),
Some(slice.as_path()),
"named as the slice's"
);
assert!(import_room("alice/linux", LINUX_OBJECTS, &limit).is_err());
let tight = fake_cgroup(&slice.join("cg"), &(2 * GIB).to_string(), GIB, "anon 1\n");
assert_eq!(
MemoryLimit::read_from(&meminfo, Some(&tight))
.unwrap()
.bytes,
2 * GIB
);
}
#[test]
fn a_4g_box_refuses_linux() {
let twin = MemoryLimit {
bytes: 4 * GIB,
source: LimitSource::Box,
};
let err = import_room("alice/linux", LINUX_OBJECTS, &twin).unwrap_err();
assert!(err.contains("MemTotal"), "{err}");
}
#[test]
fn the_cgroup_meminfo_is_the_cgroups_memory() {
let d = tempfile::tempdir().unwrap();
let cg = fake_cgroup(
d.path(),
&(6 * GIB).to_string(),
6 * GIB - 100 * 1024 * 1024,
"anon 5000000000\nactive_file 209715200\ninactive_file 104857600\nfile_dirty 104857600\nfile_writeback 0\n",
);
let c = cgroup_memory_in(&cg).expect("a limited cgroup");
assert_eq!(c.available(), 300 * 1024 * 1024);
let text = cgroup_meminfo(&c);
assert_eq!(
crate::dashboard::probe::parse_meminfo(&text),
Some((6 * GIB, 300 * 1024 * 1024)),
"{text}"
);
assert_eq!(cgroup_memory_in(&fake_cgroup(d.path(), "max", 1, "")), None);
}
}