Description
ChainStateCache has no maximum size and no TTL-based eviction sweep — entries are only ever removed by an explicit call to invalidate(key). In a long-running relayer process, the cache Map grows without bound.
Location
engine-bridge/src/chain-state-cache.ts:19-83
Current Behavior
export class ChainStateCache {
private cache = new Map<string, CacheItem<any>>();
...
async getSwr<T>(key, fetcher, options?) {
...
if (item) {
const isStale = now - item.updatedAt > staleTimeMs;
if (isStale && !item.isRevalidating) { ... background revalidate ... }
return item.data;
}
const data = await fetcher(this.rpc);
this.cache.set(key, { data, updatedAt: Date.now(), isRevalidating: false });
return data;
}
invalidate(key: string): void {
this.cache.delete(key);
}
}
Every distinct key ever passed to getSwr creates a permanent entry in this.cache. There is no maxEntries, no periodic sweep of stale/unused keys, and no clear(). The only removal path is a caller manually invoking invalidate(key) for a key it already knows about.
Expected Behavior
The cache should bound its own memory footprint — e.g. an LRU cap, or a periodic sweep that drops entries whose updatedAt exceeds some multiple of staleTimeMs and haven't been read recently — so that a process serving a growing/rotating set of keys (e.g. per-account or per-ledger cache keys) doesn't accumulate memory indefinitely.
Repro / Evidence
Calling getSwr with N distinct keys over the life of the process (e.g. once per unique account or ledger sequence queried) results in this.cache.size === N permanently — confirmed by reading getSwr/invalidate: there is no code path other than invalidate that ever calls this.cache.delete.
Impact
engine-bridge is described as a long-running relayer/bridge process (RPC failover, event propagation, nonce management). Any cache key derived from unbounded input (account IDs, ledger sequences, tx hashes) turns this into a slow, unbounded memory leak — an availability risk for a process meant to run continuously.
Suggested Fix
Add a maxEntries (LRU eviction) or a periodic sweep (e.g. setInterval clearing entries older than N * staleTimeMs with no recent reads), configurable via SwrOptions/constructor, similar to the existing staleTimeMs option.
Acceptance Criteria
Definition of Done
Labels: bug
Description
ChainStateCachehas no maximum size and no TTL-based eviction sweep — entries are only ever removed by an explicit call toinvalidate(key). In a long-running relayer process, the cache Map grows without bound.Location
engine-bridge/src/chain-state-cache.ts:19-83Current Behavior
Every distinct
keyever passed togetSwrcreates a permanent entry inthis.cache. There is nomaxEntries, no periodic sweep of stale/unused keys, and noclear(). The only removal path is a caller manually invokinginvalidate(key)for a key it already knows about.Expected Behavior
The cache should bound its own memory footprint — e.g. an LRU cap, or a periodic sweep that drops entries whose
updatedAtexceeds some multiple ofstaleTimeMsand haven't been read recently — so that a process serving a growing/rotating set of keys (e.g. per-account or per-ledger cache keys) doesn't accumulate memory indefinitely.Repro / Evidence
Calling
getSwrwith N distinct keys over the life of the process (e.g. once per unique account or ledger sequence queried) results inthis.cache.size === Npermanently — confirmed by readinggetSwr/invalidate: there is no code path other thaninvalidatethat ever callsthis.cache.delete.Impact
engine-bridgeis described as a long-running relayer/bridge process (RPC failover, event propagation, nonce management). Any cache key derived from unbounded input (account IDs, ledger sequences, tx hashes) turns this into a slow, unbounded memory leak — an availability risk for a process meant to run continuously.Suggested Fix
Add a
maxEntries(LRU eviction) or a periodic sweep (e.g.setIntervalclearing entries older thanN * staleTimeMswith no recent reads), configurable viaSwrOptions/constructor, similar to the existingstaleTimeMsoption.Acceptance Criteria
ChainStateCacheexposes a bound (max entries and/or max age) on total cache size.engine-bridge/src/__tests__/inserts more than the bound's worth of distinct keys and assertscachesize stays capped.Definition of Done
eslintwarnings introduced.Labels:
bug