Skip to content

fix(graphrag): bound SignalStore.LogClusters (close the last in-memory RAM leak)#120

Merged
aksOps merged 1 commit into
mainfrom
fix/bound-logclusters
Jun 18, 2026
Merged

fix(graphrag): bound SignalStore.LogClusters (close the last in-memory RAM leak)#120
aksOps merged 1 commit into
mainfrom
fix/bound-logclusters

Conversation

@aksOps

@aksOps aksOps commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What

Bounds SignalStore.LogClusters — the one unbounded structure found on the in-memory hot path during a CPU/RAM audit.

It was an insert-only map keyed by lc_{service}_{drain-template-id}, explicitly skipped by Prune (with a comment claiming it was "bounded upstream by the Drain template LRU"). That's only partly true: the key carries the service (so N services multiply it) and template IDs shift as Drain generalizes, so each evolving log shape mints new keys while the old LogClusterNodes are never removed. Net: the map grows with every distinct (service, template-id) pair ever seen — unbounded by the shared 50k Drain LRU and by log volume.

This is precisely the structure exercised when INFO logs are kept in-memory but not stored (the current STORE_MIN_SEVERITY=WARN default), under a high-log-shape-cardinality stream.

Fix

Extend SignalStore.Prune to bound LogClusters exactly like it already bounds Metrics:

  • TTL — evict clusters idle past the 24h signalRetention cutoff. This is what kills the accretion leak: orphaned (stale template-id) clusters age out.
  • CapmaxLogClustersPerTenant=10000 backstop, oldest-LastSeen first.
  • Evicted clusters' EMITTED_BY / LOGGED_DURING edges are swept (collected by cluster id, so cap-evicted-but-still-fresh clusters don't leave dangling edges).

CPU/RAM picture (the question this answers)

With this merged, the in-memory path is fully bounded:

  • CPU — fixed 16-worker pool fed by an event queue that drops (metered) when full, so arrival above throughput is shed, never queued unbounded.
  • RAMGOMEMLIMIT (75% of cgroup/host budget) paces GC, under per-tenant caps+TTLs on TraceStore (500k + 30m/1h TTL), SignalStore metrics (2k) + now log clusters (10k), the topology LRU (100k), TSDB cardinality, and 24h idle-tenant eviction. Designed per-tenant ceiling ≈ 200–300 MB (TraceStore-dominated).

Verification

go build/vet, golangci-lint exit 0, go test -race ./internal/graphrag (109), full suite 679 pass / 28 pkgs, gofmt clean. Rewrote the test that asserted the old never-pruned behavior; added a cap-eviction test.

🤖 Generated with Claude Code

LogClusters was the one unbounded structure on the in-memory hot path: an
insert-only map keyed by service×Drain-template-ID, explicitly skipped by
Prune. Template IDs shift as Drain generalizes and the key carries the
service, so the map grew with every distinct (service, template-id) pair ever
seen — unbounded by the shared 50k Drain LRU and by log volume. Under a
sustained high-shape log stream (exactly the case when INFO is kept in-memory
but not persisted) this leaks RAM.

Extend SignalStore.Prune to bound LogClusters the same way it bounds Metrics:
- TTL: evict clusters with LastSeen older than the 24h signalRetention cutoff
  (ages out the template-ID accretion as orphaned clusters go stale).
- Cap: maxLogClustersPerTenant=10000 backstop, oldest-LastSeen first.
- Evicted clusters' EMITTED_BY / LOGGED_DURING edges are swept (collected by
  cluster id; removed even when not yet time-stale, e.g. cap-evicted).

Prune signature gains maxLogClusters; refresh.go passes the new constant.

Tests: rewrote _SweepsStaleLogClusterEdgesOnly (which asserted the old
never-pruned behavior) into _DropsStaleLogClustersKeepsFresh; added
_LogClusterCapEvictsOldest. Verified: go build/vet, golangci-lint exit 0,
go test -race ./internal/graphrag (109), full suite 679, gofmt clean.

Closes the last unbounded structure on the in-memory path. CPU stays bounded
by the fixed 16-worker pool + drop-when-full event queue; RAM by GOMEMLIMIT
(75% budget) + per-tenant caps/TTLs on TraceStore (500k+TTL), SignalStore
metrics (2k) + now log clusters (10k), topology LRU (100k), TSDB cardinality,
and 24h idle-tenant eviction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LDQVJrixs2nJoea67a8pEG
@sonarqubecloud

Copy link
Copy Markdown

@aksOps aksOps merged commit 90b42e4 into main Jun 18, 2026
17 checks passed
@aksOps aksOps deleted the fix/bound-logclusters branch June 18, 2026 11:30
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.

1 participant