From b5a76d6b4561ab85512e34315f4e3c129ac8bf52 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 19 Oct 2026 13:20:08 +0200 Subject: [PATCH] shadow: forget expired warnings so varying messages cannot grow memory The once-per-window warning map kept every distinct error forever; an error whose text varies per tick would grow the daemon without bound. Entries older than the window are now dropped before each check. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N --- steam_backlog_enforcer/_shadow.py | 8 ++++++-- steam_backlog_enforcer/tests/test_shadow_guard.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/steam_backlog_enforcer/_shadow.py b/steam_backlog_enforcer/_shadow.py index 2d762d9..978d17b 100644 --- a/steam_backlog_enforcer/_shadow.py +++ b/steam_backlog_enforcer/_shadow.py @@ -128,8 +128,12 @@ def _should_warn(exc: BaseException) -> bool: """Whether *exc* was not already logged within :data:`WARN_EVERY_SECONDS`.""" key = f"{type(exc).__name__}: {exc}" now = time.monotonic() - last = _last_warned.get(key) - if last is not None and now - last < WARN_EVERY_SECONDS: + # Forget expired errors first: a message that varies per tick (a JSON + # offset, say) must not grow a long-lived daemon's memory without bound. + expired = [k for k, at in _last_warned.items() if now - at >= WARN_EVERY_SECONDS] + for old in expired: + del _last_warned[old] + if key in _last_warned: return False _last_warned[key] = now return True diff --git a/steam_backlog_enforcer/tests/test_shadow_guard.py b/steam_backlog_enforcer/tests/test_shadow_guard.py index bc3532d..256cb21 100644 --- a/steam_backlog_enforcer/tests/test_shadow_guard.py +++ b/steam_backlog_enforcer/tests/test_shadow_guard.py @@ -129,3 +129,17 @@ def test_tick_completes_and_enforced_count_is_untouched( assert state.seconds == seconds assert state.last_tick_at == T0 assert state.shadow_per_source == {shadow: 3.0} + + +def test_errors_older_than_the_window_are_forgotten() -> None: + """A message that varies per tick must not grow the daemon's memory.""" + window = _shadow.WARN_EVERY_SECONDS + clock = [0.0, 1.0, window + 2.0] + errors = [ValueError(f"char {i}") for i in range(3)] + with ( + patch.object(_shadow, "read_now", side_effect=errors), + patch.object(_shadow.time, "monotonic", side_effect=clock), + ): + for _ in errors: + observe_shadow({}, stamp=T0) + assert list(_shadow._last_warned) == ["ValueError: char 2"]