Repository navigation
fix(oom): prevent unbounded memory growth causing heap OOM crashes - #2965
Conversation
…stry
Root cause: Node.js process crashes with OOM (JavaScript heap out of memory)
within 5 minutes of intensive use. Heap exhausted at ~250MB.
Three fixes targeting the memory leak sources identified during investigation:
1. comboMetrics.ts — Add eviction + TTL for metrics/shadowMetrics Maps
- MAX_METRICS_ENTRIES = 500 (LRU eviction via lastUsedAt)
- METRICS_TTL_MS = 1 hour
- Cleanup interval every 5 minutes (unref'd)
- Size-cap checks in recordComboRequest, recordComboShadowRequest, recordComboIntent
2. usage.ts — Proactive TTL purging for 6 passive subscription caches
- SUB_CACHE_TTL_MS = 10 minutes
- Cleanup interval every 5 minutes (unref'd)
- Purges: geminiCliSubCache, antigravitySubCache, antigravityAvailableModelsCache,
antigravityCreditProbeCache
- Inflight Maps left alone (self-clean on Promise resolution)
3. providerRegistry.ts — Lazy Proxy for 212 provider entries
- REGISTRY renamed to _REGISTRY_EAGER, wrapped in lazy Proxy
- Individual entries materialized on first access only (_registryCache Map)
- _byAlias, _unsupportedParamsMap, _passthroughProviderIds all made lazy
- getRegisteredProviders() returns pre-computed _registryKeys (no eager iteration)
Also noted: Dockerfile hardcodes --max-old-space-size=256 (too low for production).
.env.example documents OMNIROUTE_MEMORY_MB but no script reads it — entrypoint
should be updated separately to respect this setting.
TypeScript: zero new errors introduced (pre-existing mcp-server/server.ts errors
confirmed on main branch)
…verride Dockerfile: OMNIROUTE_MEMORY_MB=1024 (was hardcoded 256MB). NODE_OPTIONS now references OMNIROUTE_MEMORY_MB so users can override via docker run -e OMNIROUTE_MEMORY_MB=2048. check-permissions.sh: entrypoint reads OMNIROUTE_MEMORY_MB and builds NODE_OPTIONS dynamically. This was documented in .env.example but never actually implemented — the entrypoint just did exec without processing the variable. Combined with the first commit's cache eviction fixes, this should prevent the OOM crashes that killed the process within 5 minutes of intensive use.
There was a problem hiding this comment.
Code Review
This pull request introduces memory management improvements, including configurable Node memory limits via OMNIROUTE_MEMORY_MB, a lazy registry proxy for provider configurations, and proactive TTL cache eviction mechanisms in comboMetrics and usage services. Feedback highlights several critical issues: the shell script incorrectly overrides the runtime memory limit due to flag ordering; the metrics eviction and cleanup logic fails to cover shadowMetrics and risks premature eviction; the lazy registry proxy adds complexity without actual memory savings since the registry is eagerly traversed at startup; and the lack of new tests for the modified production code violates Repository Style Guide Rule 9.
Code Review SummaryStatus: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Other Observations (not in diff)No issues found in unchanged code. Files Reviewed (4 files)
Reviewed by nemotron-3-super-120b-a12b-20230311:free · 62,614 tokens |
1. check-permissions.sh: swap NODE_OPTIONS flag order so runtime OMNIROUTE_MEMORY_MB override wins (last flag takes precedence) 2. comboMetrics.ts: evictOldestMetric(targetMap) now accepts the target map as parameter; cleanup timer iterates BOTH metrics AND shadowMetrics; null lastUsedAt falls back to Date.now() instead of epoch 0 (prevents premature eviction of intent-only entries) 3. providerRegistry.ts: remove Proxy wrapper — adds CPU/complexity overhead with zero memory savings since _REGISTRY_EAGER is already fully allocated and generator functions iterate all entries at startup. Simple re-export instead. 4. usage.ts: use per-cache TTL constants in cleanup timer (ANTIGRAVITY_MODELS_CACHE_TTL_MS=1min, others=5min) instead of single 10min SUB_CACHE_TTL_MS for all caches. 5. Add 18 unit tests for comboMetrics memory management covering: basic CRUD, shadow metrics, intent tracking, eviction at capacity, strategy tracking, and reset operations.
fix(oom): prevent unbounded memory growth causing heap OOM crashes
…ory-leak-caches fix(oom): prevent unbounded memory growth causing heap OOM crashes
1. check-permissions.sh: swap NODE_OPTIONS flag order so runtime OMNIROUTE_MEMORY_MB override wins (last flag takes precedence) 2. comboMetrics.ts: evictOldestMetric(targetMap) now accepts the target map as parameter; cleanup timer iterates BOTH metrics AND shadowMetrics; null lastUsedAt falls back to Date.now() instead of epoch 0 (prevents premature eviction of intent-only entries) 3. providerRegistry.ts: remove Proxy wrapper — adds CPU/complexity overhead with zero memory savings since _REGISTRY_EAGER is already fully allocated and generator functions iterate all entries at startup. Simple re-export instead. 4. usage.ts: use per-cache TTL constants in cleanup timer (ANTIGRAVITY_MODELS_CACHE_TTL_MS=1min, others=5min) instead of single 10min SUB_CACHE_TTL_MS for all caches. 5. Add 18 unit tests for comboMetrics memory management covering: basic CRUD, shadow metrics, intent tracking, eviction at capacity, strategy tracking, and reset operations.
…ory-leak-caches fix(oom): prevent unbounded memory growth causing heap OOM crashes
…ory-leak-caches fix(oom): prevent unbounded memory growth causing heap OOM crashes
1. check-permissions.sh: swap NODE_OPTIONS flag order so runtime OMNIROUTE_MEMORY_MB override wins (last flag takes precedence) 2. comboMetrics.ts: evictOldestMetric(targetMap) now accepts the target map as parameter; cleanup timer iterates BOTH metrics AND shadowMetrics; null lastUsedAt falls back to Date.now() instead of epoch 0 (prevents premature eviction of intent-only entries) 3. providerRegistry.ts: remove Proxy wrapper — adds CPU/complexity overhead with zero memory savings since _REGISTRY_EAGER is already fully allocated and generator functions iterate all entries at startup. Simple re-export instead. 4. usage.ts: use per-cache TTL constants in cleanup timer (ANTIGRAVITY_MODELS_CACHE_TTL_MS=1min, others=5min) instead of single 10min SUB_CACHE_TTL_MS for all caches. 5. Add 18 unit tests for comboMetrics memory management covering: basic CRUD, shadow metrics, intent tracking, eviction at capacity, strategy tracking, and reset operations.
…ory-leak-caches fix(oom): prevent unbounded memory growth causing heap OOM crashes
…ory-leak-caches fix(oom): prevent unbounded memory growth causing heap OOM crashes
1. check-permissions.sh: swap NODE_OPTIONS flag order so runtime OMNIROUTE_MEMORY_MB override wins (last flag takes precedence) 2. comboMetrics.ts: evictOldestMetric(targetMap) now accepts the target map as parameter; cleanup timer iterates BOTH metrics AND shadowMetrics; null lastUsedAt falls back to Date.now() instead of epoch 0 (prevents premature eviction of intent-only entries) 3. providerRegistry.ts: remove Proxy wrapper — adds CPU/complexity overhead with zero memory savings since _REGISTRY_EAGER is already fully allocated and generator functions iterate all entries at startup. Simple re-export instead. 4. usage.ts: use per-cache TTL constants in cleanup timer (ANTIGRAVITY_MODELS_CACHE_TTL_MS=1min, others=5min) instead of single 10min SUB_CACHE_TTL_MS for all caches. 5. Add 18 unit tests for comboMetrics memory management covering: basic CRUD, shadow metrics, intent tracking, eviction at capacity, strategy tracking, and reset operations.
…ory-leak-caches fix(oom): prevent unbounded memory growth causing heap OOM crashes
Problem
The Node.js process crashes with
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memorywithin ~5 minutes of intensive use.Root causes identified during investigation:
comboMetrics.ts—metricsandshadowMetricsMaps grow unboundedly (no TTL, no eviction, no cleanup timer)usage.ts— 6 subscription/model caches with passive-only TTL (entries never proactively purged)providerRegistry.ts— all 212 provider entries materialized eagerly at module load, even if unusedChanges
1.
open-sse/services/comboMetrics.ts— Cache eviction + TTLMAX_METRICS_ENTRIES = 500with LRU eviction vialastUsedAtMETRICS_TTL_MS = 1 hoursetIntervalevery 5 minutes (.unref()'d)recordComboRequest,recordComboShadowRequest,recordComboIntent2.
open-sse/services/usage.ts— Proactive TTL purgingSUB_CACHE_TTL_MS = 10 minutessetIntervalevery 5 minutes (.unref()'d)3.
open-sse/config/providerRegistry.ts— Lazy ProxyREGISTRYwrapped in aProxy— individual provider entries materialized on first access only_byAlias,_unsupportedParamsMap,_passthroughProviderIdsall deferred until first lookupgetRegisteredProviders()returns pre-computed key list without eager iteration4.
scripts/check-permissions.sh— Entrypoint integrationOMNIROUTE_MEMORY_MBand buildsNODE_OPTIONSdynamically.env.examplebut never actually implementedVerification
mcp-server/server.tserrors confirmed on main).unref()on all timers so they don't prevent clean process exit