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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N
This commit is contained in:
Krzysztof Rudnicki 2026-10-19 13:20:08 +02:00
parent f4f480c1ac
commit b5a76d6b45
2 changed files with 20 additions and 2 deletions

View File

@ -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

View File

@ -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"]