Skip to content

[BUG] ChainStateCache grows unbounded — no max size or TTL eviction sweep #166

Description

@N-thnI

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

  • AC-1: ChainStateCache exposes a bound (max entries and/or max age) on total cache size.
  • AC-2: A new test in engine-bridge/src/__tests__/ inserts more than the bound's worth of distinct keys and asserts cache size stays capped.
  • AC-3: Existing SWR (stale-while-revalidate) behavior for keys within the bound is unchanged.

Definition of Done

  • Fix merged with all AC items checked.
  • New test passes in CI.
  • No new eslint warnings introduced.

Labels: bug

Activity

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

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions