Skip to content

runtime: bound the status-watch set with a capped, expiring policy#438

Open
mfw78 wants to merge 1 commit into
feat/fold-tail-hygienefrom
feat/m2-watch-set-bound
Open

runtime: bound the status-watch set with a capped, expiring policy#438
mfw78 wants to merge 1 commit into
feat/fold-tail-hygienefrom
feat/m2-watch-set-bound

Conversation

@mfw78

@mfw78 mfw78 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

Bounds VenueRegistry's status-watch set with a configurable cap and per-entry expiry (WatchLimit), plumbed through ModuleLimits/[limits.watch] in engine_config.rs and wired into the registry builder from the supervisor.

Why

The watch set grew unboundedly and was polled O(N) per cadence with no eviction, invisible at echo scale but unbounded on a non-instant-settling venue. A configurable cap plus expiry closes this before the CoW adapter lands, without dropping live pending watches: at the cap a new watch is refused and logged; expired entries evict first and a successful poll pushes the deadline out a full window.

Closes #321

Testing

  • cargo test -p nexum-runtime (venue_registry + engine_config unit tests), including cap-refusal, expiry-eviction, deadline-refresh-on-poll, and zero-config saturation cases.

AI Assistance

Implemented with Claude Code.

The registry's watch list grew without bound and was polled O(N) per
cadence. Each watch now carries an eviction deadline and the set carries
a cap, both operator-configurable via [limits.watch]; expired entries
evict unpolled, at the cap a new watch is refused and logged rather
than a live watch dropped, and every successful non-terminal poll
pushes the deadline a full window out, so only a venue silent for the
whole window expires.

@lgahdl lgahdl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid overall — verified the concurrency and ordering claims hold up: watch() prunes expired entries before the cap check in one critical section (no reject-while-stale-entries-sit-unevicted scenario), the cap check is strict < (no off-by-one), all mutation sites hold short non-overlapping locks never spanning an .await, and zero/unset config correctly clamps up to a sane default rather than capping at literally 0.

One real edge case worth a look, flagged here since it's on an unchanged line just outside this diff's hunks: crates/nexum-runtime/src/host/venue_registry.rs:649 (record_polled_status, the return None; inside the encode-failure branch). On a status-body encode failure, the function returns early before reaching the expires_at refresh added at line 659 — so a venue that's actively responding but whose status chronically fails to encode never gets its eviction deadline pushed out, and will eventually be silently evicted by prune_expired as if it "went silent," even though it's live. The doc comment above (line 623, "the entry is left untouched for the next cadence") reads as benign but the actual effect is the expiry clock keeps ticking down unrefreshed. Consider refreshing expires_at regardless of encode outcome, or note this caveat explicitly in the doc comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants