Let systemd-oomd kill a runaway app instead of losing the session (#6445)
Nothing stood between "memory is tight" and "processes die at random": the kernel OOM killer only fires after an allocation has already failed, so a machine under pressure thrashes in reclaim until something breaks somewhere fatal — seen in practice as Hyprland taking SIGBUS mid-memcpy from a client shm pool that memory pressure was tearing down. Enable systemd-oomd, which keys on PSI stall time rather than free pages, and mark app.slice — and only app.slice — as its kill candidate. Hyprland runs in session.slice, so the compositor is structurally ineligible as a victim: oomd takes the app that caused the pressure and the session survives. Thresholds (50% stall over 20s, Fedora's desktop defaults) live in an oomd.conf.d drop-in; candidacy ships as a vendor drop-in under /usr/lib/systemd/user so existing users get it on package upgrade with no per-user seeding. New installs enable the daemon from enable-services.sh; a migration covers existing ones, restarting an already-running oomd so it doesn't keep stale thresholds until reboot. Deliberately not earlyoom: it triggers only when free RAM and free swap are both under threshold, so Omarchy's large, mostly-idle swapfiles keep it asleep through exactly this failure. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5844e02d2e
commit
1ea910f662
@@ -0,0 +1,16 @@
|
|||||||
|
# Make user apps the only thing systemd-oomd is allowed to kill.
|
||||||
|
#
|
||||||
|
# Hyprland runs in session.slice, as wayland-wm@hyprland.desktop.service, while
|
||||||
|
# everything launched through uwsm-app lands in app.slice/app-*.scope. Marking
|
||||||
|
# only app.slice as a kill candidate means the compositor is structurally
|
||||||
|
# ineligible: oomd takes the browser or terminal that caused the pressure, and
|
||||||
|
# the session survives to show the notification about it. Setting this on
|
||||||
|
# user@.service instead would put the compositor back in the candidate pool.
|
||||||
|
#
|
||||||
|
# Swap kill is a backstop for the slower shape of the same problem, where swap
|
||||||
|
# fills before pressure spikes. It uses the global SwapUsedLimit (90%) from
|
||||||
|
# /etc/systemd/oomd.conf.d/10-omarchy.conf, which also carries the pressure
|
||||||
|
# thresholds.
|
||||||
|
[Slice]
|
||||||
|
ManagedOOMMemoryPressure=kill
|
||||||
|
ManagedOOMSwap=kill
|
||||||
@@ -105,6 +105,7 @@ default/** ──► omarchy-settings /usr/share/omarchy
|
|||||||
├─ xdg-terminal-exec/*.list /usr/share/xdg-terminal-exec/
|
├─ xdg-terminal-exec/*.list /usr/share/xdg-terminal-exec/
|
||||||
├─ applications/mimeapps.list /usr/share/applications/mimeapps.list
|
├─ applications/mimeapps.list /usr/share/applications/mimeapps.list
|
||||||
├─ systemd/user/*.{service,path} /usr/lib/systemd/user/
|
├─ systemd/user/*.{service,path} /usr/lib/systemd/user/
|
||||||
|
├─ systemd/user/app.slice.d/10-oomd.conf /usr/lib/systemd/user/app.slice.d/
|
||||||
├─ systemd/system-sleep/unmount-fuse /usr/lib/systemd/system-sleep/
|
├─ systemd/system-sleep/unmount-fuse /usr/lib/systemd/system-sleep/
|
||||||
├─ systemd/zram-generator.conf.d/90-omarchy.conf /usr/lib/systemd/zram-generator.conf.d/
|
├─ systemd/zram-generator.conf.d/90-omarchy.conf /usr/lib/systemd/zram-generator.conf.d/
|
||||||
├─ fonts/omarchy/omarchy.ttf /usr/share/fonts/omarchy/
|
├─ fonts/omarchy/omarchy.ttf /usr/share/fonts/omarchy/
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Without an OOM daemon there is nothing between "memory is tight" and
|
||||||
|
# "processes start dying at random". The kernel OOM killer only fires once an
|
||||||
|
# allocation has already failed outright, which is far too late for the
|
||||||
|
# desktop: the machine thrashes in reclaim until something fails somewhere
|
||||||
|
# fatal. Seen in practice as Hyprland taking SIGBUS mid-memcpy from a Wayland
|
||||||
|
# client's shm pool, because the client was being torn down by the same memory
|
||||||
|
# pressure. One app misbehaving should not cost the whole session.
|
||||||
|
#
|
||||||
|
# systemd-oomd keys on PSI stall time rather than on free pages, so it acts
|
||||||
|
# while the machine is still thrashing instead of after an allocation failed.
|
||||||
|
# Deliberately not earlyoom: that fires only when free RAM *and* free swap are
|
||||||
|
# both under threshold, so a large, mostly-unused swapfile keeps it asleep
|
||||||
|
# through exactly this failure.
|
||||||
|
#
|
||||||
|
# 50% means half of a ten-second window was spent stalled on memory reclaim.
|
||||||
|
# Held for 20s straight, the desktop is already unusable and losing one app is
|
||||||
|
# the cheaper outcome. Which cgroups are eligible is set separately, in
|
||||||
|
# /usr/lib/systemd/user/app.slice.d/10-oomd.conf.
|
||||||
|
[OOM]
|
||||||
|
DefaultMemoryPressureDurationSec=20s
|
||||||
|
DefaultMemoryPressureLimit=50%
|
||||||
@@ -14,3 +14,7 @@ systemctl enable NetworkManager.service
|
|||||||
systemctl mask NetworkManager-wait-online.service
|
systemctl mask NetworkManager-wait-online.service
|
||||||
systemctl enable power-profiles-daemon.service
|
systemctl enable power-profiles-daemon.service
|
||||||
systemctl enable sddm.service
|
systemctl enable sddm.service
|
||||||
|
# Kill one runaway app scope instead of letting reclaim thrashing take the
|
||||||
|
# whole session down. [Install] pulls in systemd-oomd.socket via Also=, which
|
||||||
|
# is what the user manager reports app.slice candidacy over.
|
||||||
|
systemctl enable systemd-oomd.service
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
echo "Let systemd-oomd kill a runaway app instead of the whole session"
|
||||||
|
|
||||||
|
# New installs get this from install/config/enable-services.sh. Existing ones
|
||||||
|
# have never had an OOM daemon, so nothing stands between memory pressure and
|
||||||
|
# the session falling over.
|
||||||
|
|
||||||
|
as_root() {
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Machine-wide, so a second user on the same box finds it already done.
|
||||||
|
if systemctl is-enabled --quiet systemd-oomd.service 2>/dev/null; then
|
||||||
|
# Already enabled means it started before the package delivered our
|
||||||
|
# thresholds in /etc/systemd/oomd.conf.d/, and oomd only reads that at
|
||||||
|
# startup. Restart so it doesn't run with stale limits until reboot.
|
||||||
|
as_root systemctl try-restart systemd-oomd.service >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
as_root systemctl enable --now systemd-oomd.service >/dev/null 2>&1 ||
|
||||||
|
echo "Could not enable systemd-oomd.service; memory pressure will still take the session down."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pick up /usr/lib/systemd/user/app.slice.d/10-oomd.conf without waiting for
|
||||||
|
# the next login. That drop-in is what marks app.slice as a kill candidate;
|
||||||
|
# until the user manager reloads and reports it, oomd is running with nothing
|
||||||
|
# to act on. An `omarchy update` over SSH or from a TTY has no user manager to
|
||||||
|
# reload, and there the next graphical login picks it up on its own.
|
||||||
|
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||||
@@ -76,3 +76,34 @@ grep -F 'omarchy-fcitx5.service' "$first_run_units" >/dev/null ||
|
|||||||
grep -F 'fcitx5' "$ROOT/default/hypr/autostart.lua" >/dev/null &&
|
grep -F 'fcitx5' "$ROOT/default/hypr/autostart.lua" >/dev/null &&
|
||||||
fail "fcitx5 is autostarted from Hyprland; an unsupervised launch dies silently and takes every compose sequence with it"
|
fail "fcitx5 is autostarted from Hyprland; an unsupervised launch dies silently and takes every compose sequence with it"
|
||||||
pass "fcitx5 runs supervised, so a lost input method comes back instead of killing XCompose until logout"
|
pass "fcitx5 runs supervised, so a lost input method comes back instead of killing XCompose until logout"
|
||||||
|
|
||||||
|
oomd_slice="$ROOT/default/systemd/user/app.slice.d/10-oomd.conf"
|
||||||
|
grep -Fx 'ManagedOOMMemoryPressure=kill' "$oomd_slice" >/dev/null ||
|
||||||
|
fail "nothing is a kill candidate, so systemd-oomd watches the machine thrash and never acts"
|
||||||
|
grep -Fx 'ManagedOOMSwap=kill' "$oomd_slice" >/dev/null ||
|
||||||
|
fail "no swap backstop for the slower shape of the same failure"
|
||||||
|
|
||||||
|
# Hyprland lives in session.slice/wayland-wm@hyprland.desktop.service. Marking
|
||||||
|
# any ancestor of that as a kill candidate puts the compositor back in the
|
||||||
|
# victim pool, which is the crash this whole thing exists to prevent.
|
||||||
|
candidates=$(grep -rlE '^ManagedOOM(MemoryPressure|Swap)=kill' "$ROOT/default/systemd" "$ROOT/etc/systemd" 2>/dev/null || true)
|
||||||
|
[[ $candidates == "$oomd_slice" ]] ||
|
||||||
|
fail "systemd-oomd kill candidacy is set outside app.slice, which can select the compositor: $candidates"
|
||||||
|
pass "only user app scopes are systemd-oomd kill candidates"
|
||||||
|
|
||||||
|
oomd_conf="$ROOT/etc/systemd/oomd.conf.d/10-omarchy.conf"
|
||||||
|
grep -Fx 'DefaultMemoryPressureLimit=50%' "$oomd_conf" >/dev/null ||
|
||||||
|
fail "no pressure limit; the 60% default rides thrashing longer than a desktop stays usable"
|
||||||
|
grep -Fx 'DefaultMemoryPressureDurationSec=20s' "$oomd_conf" >/dev/null ||
|
||||||
|
fail "no pressure duration set for the tightened limit"
|
||||||
|
pass "systemd-oomd acts on sustained memory stall"
|
||||||
|
|
||||||
|
grep -Fx 'systemctl enable systemd-oomd.service' "$ROOT/install/config/enable-services.sh" >/dev/null ||
|
||||||
|
fail "new installs ship the oomd drop-ins with the daemon that reads them disabled"
|
||||||
|
|
||||||
|
oomd_migration=$(grep -rl 'systemd-oomd.service' "$ROOT/migrations" | head -n 1 || true)
|
||||||
|
[[ -n $oomd_migration ]] ||
|
||||||
|
fail "existing installs never enable systemd-oomd; enable-services.sh only runs at install time"
|
||||||
|
grep -F 'systemctl --user daemon-reload' "$oomd_migration" >/dev/null ||
|
||||||
|
fail "migration leaves the user manager unaware of app.slice candidacy until the next login"
|
||||||
|
pass "existing installs enable systemd-oomd and report app.slice without a relogin"
|
||||||
|
|||||||
Reference in New Issue
Block a user