Fix #93: bound repoBus ring #102

Merged
crueber merged 2 commits from fix/issue-93 into main 2026-09-05 03:04:50 +00:00
Owner

Fixes #93: repoBus.ring in internal/notify/stream.go grew one entry per repo ever published, never evicted.

Shape (matches the existing subscriber-cleanup discipline): evict-on-last-subscriber (unsubscribe drops the repo's replay ring alongside its subs entry) + LRU cap of RepoBusMaxRepos (256) retained repos for the publish-with-no-subscribers path. Eviction prefers idle repos, drops replay rings only (live channels never closed; ring rebuilds on next publish). Recency state rides the pre-existing bus mutex — no new locks, no timers, no goroutines.

Behavior nuance (documented in Decisions, docs/features/06_notifications.md): publish-then-subscribe replay is unchanged; a subscriber attaching after a fully idle period, or to an LRU-evicted repo, replays nothing and starts from the live tail. Durable history is the feature timeline/API, never this ring. Worst case now 256 repos x 64 frames.

Regression tests (deterministic, no sleeps): publish-to-N + unsubscribe-all empties the ring; LRU cap evicts oldest-idle first and retains newest; subscribed repos keep replay while idle ones are evicted; all-subscribed overflow keeps live delivery working.

Verification: gofmt/vet clean; go test ./internal/notify/ -race green (incl. -count=10 on TestRepoRing*); coverage 97.3% (gate >=95%).

Fixes #93: `repoBus.ring` in internal/notify/stream.go grew one entry per repo ever published, never evicted. Shape (matches the existing subscriber-cleanup discipline): evict-on-last-subscriber (unsubscribe drops the repo's replay ring alongside its subs entry) + LRU cap of RepoBusMaxRepos (256) retained repos for the publish-with-no-subscribers path. Eviction prefers idle repos, drops replay rings only (live channels never closed; ring rebuilds on next publish). Recency state rides the pre-existing bus mutex — no new locks, no timers, no goroutines. Behavior nuance (documented in Decisions, docs/features/06_notifications.md): publish-then-subscribe replay is unchanged; a subscriber attaching after a fully idle period, or to an LRU-evicted repo, replays nothing and starts from the live tail. Durable history is the feature timeline/API, never this ring. Worst case now 256 repos x 64 frames. Regression tests (deterministic, no sleeps): publish-to-N + unsubscribe-all empties the ring; LRU cap evicts oldest-idle first and retains newest; subscribed repos keep replay while idle ones are evicted; all-subscribed overflow keeps live delivery working. Verification: gofmt/vet clean; go test ./internal/notify/ -race green (incl. -count=10 on TestRepoRing*); coverage 97.3% (gate >=95%).
repoBus.ring grew one entry per repo ever published and was never
deleted. Unsubscribe now drops the repo's replay ring alongside its subs
entry; publishes past RepoBusMaxRepos (256) repos evict the LRU idle
repo (subscribed repos last; channels never touched). Replay for
publish-then-subscribe is unchanged; post-idle attach starts from the
live tail (timeline/API remains the durable backfill).

Regression tests: publish-to-N + unsubscribe-all empties the ring, LRU
cap evicts oldest-idle first, subscribed repos keep replay, all-
subscribed overflow keeps live delivery. internal/notify 97.3% cover,
-race clean.
One publish adds at most one repo key, so one eviction restores the
256-repo bound; if-not-for guarantees no lock-held spin if the
ring/last invariant ever diverged. Behavior-identical; -race clean,
97.3% cover.
Sign in to join this conversation.
No description provided.