Skip to content

fix(health-probe): bound gRPC clientPool to stop memory leak - #54

Merged
nimrod-teich merged 1 commit into
mainfrom
fix/grpc-clientpool-leak
Jun 2, 2026
Merged

nimrod-teich merged 1 commit into
mainfrom
fix/grpc-clientpool-leak

Conversation

@nimrod-teich

Copy link
Copy Markdown
Contributor

Summary

  • Root-cause fix for the info-mainnet memory leak. The health-probe clientPool created one gRPC client per provider endpoint and never closed or evicted any of them. Each client owns a live HTTP/2 channel (socket, keepalive timers, native buffers, file descriptors); provider endpoints churn continuously (restarts, re-IPs, stake changes each epoch), so the pool grew without bound.
  • On the 512 MB info-mainnet Fargate task this showed up as a monotonic heap climb — ~40% → 97% over ~36 h with no GC recovery — ending in repeated OOM-kills (exit 137) and task restarts (visible as the ~05/28 cliff and clustered task replacements in the ECS service events).
  • Fix: bound the pool with an LRU cap (HEALTH_PROBE_CLIENT_POOL_MAX, default 256) and client.close() the least-recently-used channel on overflow. A cache hit re-inserts the key so actively-probed endpoints stay warm. Client creation is now injectable so the eviction/close behaviour is unit-tested without opening a real channel.

probeProvider and production behaviour are otherwise unchanged.

Evidence (CloudWatch, ECS info-mainnet/api, us-east-1)

Window Avg memory Shape
36 h 205 → 470 MB monotonic staircase, min also rising (retained heap, not load)
now 498 / 512 MB ~97% of hard cap, prior OOM-cycles confirmed in service events

Leak located at apps/api/src/services/grpc-probe.ts — clientPool keyed by endpoint, driven by the in-process runProbeLoop (ENABLE_HEALTH_PROBE=true in mainnet.tfvars).

Test plan

  • pnpm --filter @info/api typecheck — clean
  • New grpc-probe-pool.test.ts — 7 cases: client reuse, distinct endpoints, pool stays ≤ cap, every evicted client .close()d exactly once, LRU touch keeps recent endpoints warm, reset closes all
  • Full pnpm --filter @info/api test — 134/134 passing

Deploy note

This is the code fix; info-mainnet is still leaking until this image ships. A --force-new-deployment can reset the heap immediately if an OOM is imminent before merge.

Follow-up (not in this PR)

Secondary unbounded cache ibcTraceCache (apps/api/src/rpc/pricing.ts:171) — slow, not the OOM driver; can be bounded separately.

The health-probe clientPool created one gRPC client per provider endpoint
and never closed or evicted any of them. Each client owns a live HTTP/2
channel (socket, keepalive timers, native buffers, file descriptors), and
provider endpoints churn continuously (restarts, re-IPs, stake changes), so
the pool grew without bound. On the 512MB info-mainnet Fargate task this
manifested as a monotonic heap climb (~40%→97% over ~36h, no GC recovery)
ending in repeated OOM-kills and task restarts.

Bound the pool with an LRU cap (HEALTH_PROBE_CLIENT_POOL_MAX, default 256)
and close the least-recently-used channel on overflow; a cache hit re-inserts
the key so active endpoints stay warm. Client creation is now injectable so
the eviction/close behaviour is unit-tested without opening a real channel.
@nimrod-teich
nimrod-teich merged commit 5537216 into main Jun 2, 2026
2 checks passed
@nimrod-teich
nimrod-teich deleted the fix/grpc-clientpool-leak branch June 2, 2026 12:56
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