fix(discord): bound REST entity cache to prevent unbounded Map growth#77952
Conversation
|
Codex review: needs maintainer review before merge. Reviewed May 31, 2026, 2:04 PM ET / 18:04 UTC. Summary PR surface: Source +42, Tests +77. Total +119 across 2 files. Reproducibility: yes. via source inspection, but not by executing a repro in this read-only review. Current main only deletes an expired entry for the requested key and never visits other keys or caps the Map, so N unique successful fetches can retain N entries. Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the focused cache cap and sweep with the current safe expiry guards intact after required checks pass; broader Discord cache audits should stay in separate items. Do we have a high-confidence way to reproduce the issue? Yes via source inspection, but not by executing a repro in this read-only review. Current main only deletes an expired entry for the requested key and never visits other keys or caps the Map, so N unique successful fetches can retain N entries. Is this the best way to solve the issue? Yes, this is the right owner boundary: DiscordEntityCache owns these REST entity objects, and the PR bounds that private map without adding user config or changing the Client fetch API. AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against beb499b4d1c0. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +42, Tests +77. Total +119 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
This comment was marked as low quality.
This comment was marked as low quality.
|
@byungskers good point. TTL already bounds the working set here (30s window), so under cap pressure FIFO and LRU effectively converge — but the LRU swap is small and worth measuring. Will park it for a follow-up PR with hit-rate telemetry rather than gate this fix on it. Thanks for the review! |
The expiry-sweep test relied on wall-clock async scheduling staying under ttlMs=1ms between two consecutive awaits. On a loaded worker that gap can exceed 1ms, letting maybeSweepExpired drop u1 before the size==2 precondition. Switch the two timing-sensitive tests to vitest fake timers so Date.now() is frozen across awaits and advanced explicitly. Addresses review feedback on openclaw#77952.
ddb14ac to
2779d23
Compare
|
Rebased onto current @bryce-d-greybeard — flagging for review when convenient (Discord REST entity-cache bound). |
|
This pull request has been automatically marked as stale due to inactivity. |
DiscordEntityCache held REST fetches (`user:<id>`, `channel:<id>`, `guild:<id>`, `member:<gid>:<uid>`) in a Map with a read-time TTL check on the exact same key. Different keys never triggered any sweep, so the cache grew unbounded for the lifetime of the gateway as the bot touched distinct users/channels/guilds — a slow leak proportional to entity cardinality. - Add a throttled write-time sweep (default once per 30s) that walks the Map and drops expired entries. - Add a hard `maxEntries` cap (default 5,000) with FIFO eviction using Map insertion order. - Expose optional `maxEntries` / `sweepIntervalMs` constructor params and a read-only `size` getter. Existing `ttlMs?`, public `fetchUser`/`fetchChannel`/`fetchGuild`/ `fetchMember` surface, and `invalidateForGatewayEvent` semantics are unchanged.
The expiry-sweep test relied on wall-clock async scheduling staying under ttlMs=1ms between two consecutive awaits. On a loaded worker that gap can exceed 1ms, letting maybeSweepExpired drop u1 before the size==2 precondition. Switch the two timing-sensitive tests to vitest fake timers so Date.now() is frozen across awaits and advanced explicitly. Addresses review feedback on openclaw#77952.
2779d23 to
539aa4d
Compare
|
Maintainer pass done on head 539aa4d. Changes:
Verification:
Known note:
|
Closes #77975
Summary
DiscordEntityCache(extensions/discord/src/internal/entity-cache.ts) cacheduser:<id>,channel:<id>,guild:<id>, andmember:<gid>:<uid>REST fetches in aMapwith a 30s TTL, but only checked the TTL on an exact-same-key re-fetch insidefetchCached. Different keys never triggered any sweep, so on a long-lived gateway the cache grew unbounded across the bot's lifetime — every distinct user/channel/guild touched stayed retained past its TTL until process restart. A slow leak proportional to entity cardinality.This change adds:
maybeSweepExpired, default once per 30s) that walks the Map and drops expired entries.maxEntriescap (default 5,000) enforced via FIFO drop usingMapinsertion order.maxEntries/sweepIntervalMsfor callers that want to tune.sizegetter for diagnostics.The existing
ttlMs?constructor option, public fetch surface, andinvalidateForGatewayEventsemantics are unchanged.Verification
entity-cache.test.ts: 4 cases — cap enforcement, post-TTL sweep, no-sweep before interval, ttlMs=0 disables caching. All pass.oxfmt --checkclean on the touched files.Real behavior proof
DiscordEntityCache.entriesis aMapwith read-time TTL check only and no write-time eviction or hard size cap, so on a long-lived gateway the cache grows unbounded as differentuser:/channel:/guild:/member:keys are touched. The 30s TTL is never enforced for keys that aren't re-fetched.DiscordEntityCachewas driven by a fakeRequestClientreturning{ id }for every route, so the cache exercises real eviction code paths under high cardinality without going through any test framework./tmp/discord-cache-demo.tsthat imports the patched cache, runs 100,000 unique-key fetches against the cap, then runs a sweep-window scenario withttlMs=10/sweepIntervalMs=0. Executed viapnpm exec tsx /tmp/discord-cache-demo.ts— direct node runtime output, no mocking framework involved.maxEntries=5000, the cache stays pinned at 5,000 entries across 100,000 unique fetches (oldest-first eviction holds the cap). WithttlMs=10msandsweepIntervalMs=0, one insert past the TTL window sweeps the prior 456 stale entries down to 1, confirming the write-time expiry sweep works.Map/cache change with no network surface, and the live runtime demo exercises the same eviction code paths under higher cardinality than a real bot would see.