Clear the stranded tmpfs cache at startup
The session map is memory-only, so session directories surviving a restart can never be served and never be evicted -- and the work root is a tmpfs, so that is leaked RAM until reboot. The restart that ships the TTFB fix would have stranded 1.56 GB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1137,3 +1137,12 @@ fast mux keeping ranges and seeking, `--first-byte-grace` at `--wait-timeout`
|
|||||||
restoring strict mode, a failed producer with bytes being served but 502 in strict
|
restoring strict mode, a failed producer with bytes being served but 502 in strict
|
||||||
mode, a failed producer with no bytes always 502, `--growing` meaning no wait, and
|
mode, a failed producer with no bytes always 502, `--growing` meaning no wait, and
|
||||||
`/healthz` reporting the mode. 353 pass.
|
`/healthz` reporting the mode. 353 pass.
|
||||||
|
|
||||||
|
### A restart used to strand the cache
|
||||||
|
|
||||||
|
The session map is in memory only, so every session directory left in the work root
|
||||||
|
after a restart is unreachable (nothing can find it) *and* unevictable (the cache
|
||||||
|
budget only sums tracked sessions). The work root is a tmpfs, so that is leaked RAM
|
||||||
|
until the next reboot — the restart that shipped the fix above would have stranded
|
||||||
|
1.56 GB. `reset_work_root()` now clears it at startup and logs what it reclaimed.
|
||||||
|
Session directories only; a stray file in the work root is left alone.
|
||||||
|
|||||||
@@ -735,6 +735,27 @@ def make_handler(mgr, wait_timeout, first_byte_grace=FIRST_BYTE_GRACE):
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def reset_work_root(path):
|
||||||
|
"""Clear leftover session directories at startup. Returns bytes reclaimed.
|
||||||
|
|
||||||
|
The session map is in memory only, so anything already in the work root is
|
||||||
|
unreachable after a restart: it can never be served (no session to find) and
|
||||||
|
never be evicted (the cache accounting only sums tracked sessions). Since the
|
||||||
|
work root is a tmpfs, leaving it there leaks RAM until the next reboot -- a
|
||||||
|
restart with 1.56 GB cached stranded exactly that much.
|
||||||
|
"""
|
||||||
|
reclaimed = 0
|
||||||
|
for name in os.listdir(path) if os.path.isdir(path) else []:
|
||||||
|
stale = os.path.join(path, name)
|
||||||
|
if not os.path.isdir(stale):
|
||||||
|
continue
|
||||||
|
out = os.path.join(stale, "out.mp4")
|
||||||
|
if os.path.exists(out):
|
||||||
|
reclaimed += os.path.getsize(out)
|
||||||
|
shutil.rmtree(stale, ignore_errors=True)
|
||||||
|
return reclaimed
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser(
|
ap = argparse.ArgumentParser(
|
||||||
description="just-in-time YouTube streaming proxy for Jellyfin")
|
description="just-in-time YouTube streaming proxy for Jellyfin")
|
||||||
@@ -783,6 +804,11 @@ def main():
|
|||||||
_access_log = args.access_log
|
_access_log = args.access_log
|
||||||
|
|
||||||
os.makedirs(args.work, exist_ok=True)
|
os.makedirs(args.work, exist_ok=True)
|
||||||
|
stranded = reset_work_root(args.work)
|
||||||
|
if stranded:
|
||||||
|
log(f"cleared {stranded / 2**30:.2f} GB of untracked cache from "
|
||||||
|
f"{args.work} left by a previous run")
|
||||||
|
|
||||||
mgr = Manager(args.work, args.max_pipelines,
|
mgr = Manager(args.work, args.max_pipelines,
|
||||||
int(args.cache_gb * 2**30), args.no_fetch, args.growing,
|
int(args.cache_gb * 2**30), args.no_fetch, args.growing,
|
||||||
args.max_retries, args.max_starts, args.starts_window)
|
args.max_retries, args.max_starts, args.starts_window)
|
||||||
|
|||||||
@@ -484,6 +484,32 @@ def test_concurrency_cap(tmp_path):
|
|||||||
assert len(manager.start_log) == 2
|
assert len(manager.start_log) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_startup_clears_untracked_cache(tmp_path):
|
||||||
|
"""The work root is a tmpfs and the session map is memory-only, so anything
|
||||||
|
left by a previous run is unreachable *and* unevictable -- it would leak RAM
|
||||||
|
until the next reboot."""
|
||||||
|
work = tmp_path / "work"
|
||||||
|
(work / "abcdefghijk").mkdir(parents=True)
|
||||||
|
(work / "abcdefghijk" / "out.mp4").write_bytes(b"x" * 5000)
|
||||||
|
(work / "bcdefghijkl").mkdir()
|
||||||
|
(work / "bcdefghijkl" / "out.mp4").write_bytes(b"y" * 3000)
|
||||||
|
(work / "loose.txt").write_text("not a session")
|
||||||
|
|
||||||
|
reclaimed = proxy.reset_work_root(str(work))
|
||||||
|
|
||||||
|
assert reclaimed == 8000
|
||||||
|
assert not (work / "abcdefghijk").exists()
|
||||||
|
assert not (work / "bcdefghijkl").exists()
|
||||||
|
# A stray file is not a session directory and is left alone.
|
||||||
|
assert (work / "loose.txt").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_startup_on_a_clean_work_root_is_a_no_op(tmp_path):
|
||||||
|
work = tmp_path / "empty"
|
||||||
|
work.mkdir()
|
||||||
|
assert proxy.reset_work_root(str(work)) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_no_fetch_mode_refuses_everything(manager_factory):
|
def test_no_fetch_mode_refuses_everything(manager_factory):
|
||||||
manager = manager_factory(no_fetch=True, max_starts=99)
|
manager = manager_factory(no_fetch=True, max_starts=99)
|
||||||
session, refusal = manager.get(vid(30))
|
session, refusal = manager.get(vid(30))
|
||||||
|
|||||||
Reference in New Issue
Block a user