feat(fleet): give the media servers an I/O share that actually exists
A Plex section scan walked 50k files at ~850 r/s and nothing bounded it. `IOWeight=` in a quadlet looks like the fix and does nothing: systemd delegates `cpu memory pids` to user@.service and NOT io, and it drops a resource line it cannot honour without a word — the container boots healthy and its cgroup has no io.weight file at all. Same family as RequiresMountsFor= (inert in the user manager) and ConditionPathIsMountPoint= (fail-dead). Place the two system drop-ins that make the controller reachable (user.slice IOAccounting=yes puts io in user.slice/cgroup.subtree_control, measured immediately; user@1000.service DelegateControllers gains io and lands at the next reboot, because restarting the user manager would kill every rootless container on the host), then ask for the share: IOWeight=10 on jellyfin and plex. 10 is a convention, not a measured optimum — proportional share, only bites under contention, only requirement is far below the default 100. ensure_user_io_delegate places both from git and prints NOTE: io is not delegated … inert until the next reboot, so the quadlet line never reads as live config. Verified on the host: subtree_control flipped to `cpu io memory pids`, the drop-in is attached and parsed (systemctl cat), and the podman 5.7 generator emits IOWeight=10 for both units (bare key; UnitConfigItem= not needed). Two limits recorded instead of hidden: a container's weight covers its whole cgroup (scan and playback share it), and the comparison happens inside user.slice, so it cannot push a rootless unit below a root unit such as frank-archon — that pairing is user.slice's own weight, a separate call. Docs: Quadlet Lifecycle 4b (the three-command read of the chain), Pi-hole (all rootless containers share one 10.0.2.100, so one chatty container throttles DNS for the host — 8 events today), E2E Suite gotcha 7 (the gate depends on the runner's resolver: 21 reds on a lost wired link, 37/37 minutes later). Seam: an in-repo IOWeight= may only exist when both drop-ins exist AND the deployer places them. ./tests/run.sh: 789 ok, exit 0.
This commit is contained in:
1 parent
6d274acc47
commit
183a6d5442
9 files changed
+146
-4
No files matched your search
@@ -1208,14 +1208,47 @@ ensure_media_view() {
|
||||
# array below stays read-only and every sidecar Jellyfin writes lands on the
|
||||
# NVMe.
|
||||
# Raise it BEFORE either container starts — a start against a bare view
|
||||
# directory would let a scan clear the library database. The units also carry
|
||||
# ConditionPathIsMountPoint, so a missing view skips the start rather than
|
||||
# damaging anything. A relayout must be followed by `media-view.sh reap`
|
||||
# directory would let a scan clear the library database. Each unit gates on
|
||||
# scripts/wait-for-mount.sh, which waits and then refuses the start. A
|
||||
# relayout must be followed by `media-view.sh reap`
|
||||
# (see the Media Relayout page), which this is not.
|
||||
run_privileged bash scripts/media-view.sh install
|
||||
bash scripts/media-view.sh verify
|
||||
}
|
||||
|
||||
ensure_user_io_delegate() {
|
||||
# The media servers ask for an I/O share (IOWeight= in their quadlets) so a
|
||||
# library scan cannot starve playback or the array's other reader. Two system
|
||||
# drop-ins are what make that request real, and both are host state, so the
|
||||
# deployer places them:
|
||||
# user.slice.d/io-accounting.conf io enters user.slice's subtree
|
||||
# user@1000.service.d/io-delegate.conf the user manager gets the controller
|
||||
# Without the first, `io` never appears in user.slice/cgroup.subtree_control,
|
||||
# so no container under it can have an io.weight file at all; without the
|
||||
# second the user manager never asks for the controller. systemd hands
|
||||
# user@.service cpu/memory/pids by default and drops a line it cannot honour
|
||||
# without a word — IOWeight= on its own is inert config.
|
||||
local src dst
|
||||
for src in \
|
||||
dankserver/host/systemd/user.slice.d/io-accounting.conf \
|
||||
dankserver/host/systemd/user@1000.service.d/io-delegate.conf; do
|
||||
dst=/etc/systemd/system/${src#dankserver/host/systemd/}
|
||||
run_privileged mkdir -p "$(dirname "$dst")"
|
||||
if ! run_privileged cmp -s "$src" "$dst"; then
|
||||
run_privileged install -m 644 "$src" "$dst"
|
||||
run_privileged systemctl daemon-reload
|
||||
fi
|
||||
done
|
||||
# Accounting lands immediately; the delegation is read when user@1000.service
|
||||
# starts, and restarting that unit would kill every rootless container on this
|
||||
# host. So the share becomes real at the next reboot — say so out loud rather
|
||||
# than let a quadlet line read as live config.
|
||||
if ! systemctl show user@1000.service -p DelegateControllers | grep -qw io; then
|
||||
echo "NOTE: io is not delegated to user@1000.service yet - IOWeight= is"
|
||||
echo "NOTE: inert until the next reboot (see [[Quadlet Lifecycle]])"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_provider_keys() {
|
||||
# Jellyfin's metadata-provider keys live in the container-owned plugin
|
||||
# configuration under the data root, never in git; scripts/jellyfin-providers.sh
|
||||
@@ -1237,6 +1270,7 @@ ensure_provider_keys() {
|
||||
|
||||
deploy_jellyfin() {
|
||||
ensure_media_view
|
||||
ensure_user_io_delegate
|
||||
ensure_provider_keys
|
||||
cp "dankserver/jellyfin/quadlet/jellyfin.container" "$quadlet_unit_dir/"
|
||||
systemctl --user daemon-reload
|
||||
@@ -1326,6 +1360,7 @@ deploy_copyparty() {
|
||||
|
||||
deploy_plex() {
|
||||
ensure_media_view
|
||||
ensure_user_io_delegate
|
||||
cp "dankserver/plex/quadlet/plex.container" "$quadlet_unit_dir/"
|
||||
systemctl --user daemon-reload
|
||||
if ! systemctl --user is-active -q plex.service; then
|
||||
|
||||
Reference in new issue
Block a user