fix(r5): address the eighth /code-review high round — 7 findings
The worst one was, again, the previous round's own fix. - Round 7 stopped awaiting the HLS sweep so startup could serve immediately — which also made it run CONCURRENTLY with playback, and `sweep_orphans` snapshotted the live-session set BEFORE listing the root and rmtree'd outside the lock. Since `start_session` reuses a directory path for the same (key, offset, tag), a stale name can become a live session at any moment. Two guards now: nothing whose mtime is newer than this process's start is ever deleted, and the live check + rmtree happen together under the lock, one directory at a time. - The sweep task is awaited after cancel, so shutdown can't leave it pending. - `_rate_limiter` used a truthiness test on its timestamp, so a `now()` of exactly 0.0 read as "never ran" — unreachable in production, but `now` exists to be injected and a test clock starting at 0 is the obvious choice. - The RSS total-outage error now carries the first underlying error: "all N channels failed" is also what ONE dead feed id looks like on a small instance, and a red card the user can't act on is its own kind of dishonest. - The WebKit fullscreen handling moved into `lib/fullscreen` and PlexPlayer uses it too — it had the same unprefixed-only reads in three places, including an Escape branch that would have closed the whole player instead of leaving fullscreen. - The `--max` CSS comment claimed `inset: 0` did the sizing; Tailwind's `w-full` is still on the element and wins. Comment now states the real mechanism. Verified in real Chrome (the pane can't composite or do native fullscreen): - F fills the 1920x889 viewport; the exit pill renders top-right on the letterbox band and overlaps NO YouTube control — the collision this round suspected does not reproduce in this embed (its own buttons sit bottom-left / bottom-right). - Four real ArrowDown presses while maximised: persisted volume 100 -> 80, with the level flash visible on screen. - A real click on the exit pill un-maximises and returns focus to the card. - lib/fullscreen driven from a real click: enter (innerHeight 889 -> 1024), the change listener fires exactly once per transition, exit returns to 889. Backend suite 150 -> 158; the new sweep tests are mutation-checked, and they read their cutoff from the filesystem rather than a clock (the container VM and the bind-mounted host disagree by enough to make a clock-based assertion flaky).
This commit is contained in:
@@ -47,6 +47,10 @@ _SESSION_IDLE_S = 600 # reap a session with no access for this long
|
||||
# fail. Covered by test_plex_stream.py — this regex is the only thing standing between the sweep
|
||||
# and someone else's data.
|
||||
_SESSION_DIR_RE = re.compile(r"^\d+_\d+(_[^_]+_[+-]\d+\.\d{2})?$")
|
||||
# When this process started. `sweep_orphans` only deletes directories older than this: everything
|
||||
# newer belongs to a session THIS run created, and since `start_session` reuses a directory path
|
||||
# for the same (rating_key, offset, tag), "stale name" is not the same as "stale directory".
|
||||
_PROCESS_START = time.time()
|
||||
|
||||
_lock = threading.Lock()
|
||||
_sessions: dict[str, "HlsSession"] = {}
|
||||
@@ -268,7 +272,7 @@ def wait_for(path: Path, timeout: float = 20.0) -> bool:
|
||||
return path.exists()
|
||||
|
||||
|
||||
def sweep_orphans() -> int:
|
||||
def sweep_orphans(older_than: float | None = None) -> int:
|
||||
"""Delete OUR segment directories that no live session owns.
|
||||
|
||||
Sessions live only in THIS process, so at startup every session directory under `_HLS_ROOT` is
|
||||
@@ -278,18 +282,39 @@ def sweep_orphans() -> int:
|
||||
|
||||
Only entries whose name matches `_SESSION_DIR_RE` are touched: the root is admin-configurable,
|
||||
so anything else under it belongs to someone else and is left alone. Blocking (rmtree) — call
|
||||
it off the event loop."""
|
||||
with _lock:
|
||||
live = {s.dir for s in _sessions.values()}
|
||||
it off the event loop.
|
||||
|
||||
TWO guards against deleting a LIVE session, because this now runs CONCURRENTLY with request
|
||||
handling (the caller stopped awaiting it so startup isn't held up):
|
||||
* `older_than` (default: this process's start) — a directory touched since we booted was made
|
||||
by THIS run, so it is by definition not a leftover. This is the guard that matters, because
|
||||
`start_session` REUSES the same path for the same (key, offset, tag): a stale directory can
|
||||
become a live one at any moment.
|
||||
* the live-session check and the rmtree happen together under `_lock`, so a session can't be
|
||||
registered in the gap between them. The lock is held for one directory at a time — long
|
||||
enough to be atomic, short enough that a boot-time sweep of a big backlog doesn't stall
|
||||
playback."""
|
||||
cutoff = _PROCESS_START if older_than is None else older_than
|
||||
dropped = 0
|
||||
try:
|
||||
entries = list(_HLS_ROOT.iterdir())
|
||||
except OSError: # the root doesn't exist yet — nothing to sweep
|
||||
return 0
|
||||
for p in entries:
|
||||
if p.is_dir() and p not in live and _SESSION_DIR_RE.match(p.name):
|
||||
if not _SESSION_DIR_RE.match(p.name):
|
||||
continue
|
||||
if not p.is_dir():
|
||||
continue
|
||||
try:
|
||||
if p.stat().st_mtime >= cutoff:
|
||||
continue # this run made (or reused) it — never ours to delete
|
||||
except OSError: # vanished under us (a concurrent restart of that session)
|
||||
continue
|
||||
with _lock:
|
||||
if any(s.dir == p for s in _sessions.values()):
|
||||
continue
|
||||
shutil.rmtree(p, ignore_errors=True)
|
||||
dropped += 1
|
||||
dropped += 1
|
||||
return dropped
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user