feat(stability): default-on memory-pressure protection (earlyoom)
Some checks failed
Check / eval (push) Has been cancelled
Some checks failed
Check / eval (push) Has been cancelled
modules/nixos/oom.nix — running out of memory now kills the offending process (with a desktop notification via systembus-notify) instead of freezing the desktop for minutes until the kernel OOM killer fires. earlyoom over systemd-oomd, deliberately: oomd kills whole cgroups, and a Hyprland session runs as ONE scope (nothing here spawns per-app systemd scopes, unlike GNOME) — under pressure oomd would take out the entire desktop to save it. earlyoom kills the single largest process before the thrash point. nixpkgs default-enables oomd in an inert state (no slices monitored); it is disabled outright so there is one owner. Session plumbing is --avoid-listed (unanchored — NixOS wrappers rename comm to .foo-wrapped); no --prefer tuning, largest-RSS selection already finds the hog. All mkDefault; opt out with services.earlyoom.enable = false (README note added). Verified: V0 (flake check) + V2 — new checks.oom-protection runNixOSTest, executed: a chunked allocator (686 MB peak, 1 GB VM) is SIGTERM'd by earlyoom in 0.1 s, a bystander unit survives (the process-level granularity the module exists for), and systemd-oomd is asserted inactive. 25 s runtime. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
47
flake.nix
47
flake.nix
@@ -207,6 +207,53 @@
|
||||
'';
|
||||
};
|
||||
|
||||
# Memory-pressure protection (oom.nix): earlyoom must kill a
|
||||
# runaway allocator BEFORE the kernel thrash point — and must not
|
||||
# take anything else with it. A bystander unit survives the kill,
|
||||
# proving the process-level granularity that motivated earlyoom
|
||||
# over cgroup-level systemd-oomd (which would kill the whole
|
||||
# Hyprland session scope). Imports only oom.nix so the VM stays
|
||||
# minimal.
|
||||
oom-protection = pkgs.testers.runNixOSTest {
|
||||
name = "nomarchy-oom-protection";
|
||||
nodes.machine = { ... }: {
|
||||
imports = [ ./modules/nixos/oom.nix ];
|
||||
environment.systemPackages = [ pkgs.python3 ];
|
||||
virtualisation.memorySize = 1024;
|
||||
};
|
||||
testScript = ''
|
||||
machine.wait_for_unit("earlyoom.service")
|
||||
|
||||
# One owner: oomd (nixpkgs default-on, inert) is disabled.
|
||||
machine.fail("systemctl is-active --quiet systemd-oomd.service")
|
||||
# The session avoid-list made it into the daemon invocation.
|
||||
machine.succeed("systemctl cat earlyoom.service | grep -q -- --avoid")
|
||||
|
||||
# A bystander that must survive (stand-in for the session).
|
||||
machine.succeed("systemd-run --unit=bystander sleep infinity")
|
||||
|
||||
# The hog: allocate in 20 MB chunks with a short sleep, so
|
||||
# earlyoom's poll wins the race against the kernel OOM killer.
|
||||
machine.succeed(
|
||||
"systemd-run --unit=hog python3 -c 'import time; "
|
||||
"exec(\"a=[]\\nwhile True:\\n a.append(bytearray(20*1024*1024)); time.sleep(0.05)\")'"
|
||||
)
|
||||
|
||||
# earlyoom (not the kernel) pulled the trigger…
|
||||
machine.wait_until_succeeds(
|
||||
"journalctl -u earlyoom.service | grep -Eiq 'sending SIG(TERM|KILL)'",
|
||||
timeout=120,
|
||||
)
|
||||
# …the hog is gone…
|
||||
machine.wait_until_succeeds(
|
||||
'test "$(systemctl show -p ActiveState --value hog.service)" != "active"',
|
||||
timeout=60,
|
||||
)
|
||||
# …and the bystander is untouched.
|
||||
machine.succeed("systemctl is-active --quiet bystander.service")
|
||||
'';
|
||||
};
|
||||
|
||||
# The distroId question (roadmap "Distro branding"): set
|
||||
# distroId = "nomarchy" so /etc/os-release is honest (ID=nomarchy,
|
||||
# ID_LIKE=nixos). The risk is switch-to-configuration's "is this
|
||||
|
||||
Reference in New Issue
Block a user