mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
Bottleneck 2.19.5 (frozen upstream dependency, no release since 2019) has a bug in LocalDatastore#_startHeartbeat() (node_modules/bottleneck/lib/ LocalDatastore.js:29,56): the guard `if (this.heartbeat == null && ...)` only (re)creates the periodic reservoir-refresh interval the first time it runs. Every later call -- including the one updateSettings() itself triggers internally -- falls into the else branch and does clearInterval(this.heartbeat) WITHOUT resetting the reference back to null. Because the stale reference sticks around, every future _startHeartbeat() call keeps taking the same dead else branch: the periodic reservoir refresh is gone forever after the first manual updateSettings() call on a limiter. Every limiter created by this file starts with a live heartbeat (buildLimiterDefaults() always sets reservoirRefreshInterval/ reservoirRefreshAmount), so the very first updateFromHeaders() / updateFromResponseBody() / applyRequestQueueSettings() call against a limiter permanently kills its refresh. In production this wedges the request queue once the reservoir hits 0: an auto-enrolled apikey connection accumulates its default 60 requests, the reservoir zeroes, the queue freezes for ~120s, the watchdog fires a synthetic 502 (RATE_LIMIT_QUEUE_WEDGED), the connection cools down and gets excluded from weighted combo pools -- turning a configured 70/30 split into ~50/50. Add applyLimiterSettings(), a module-local wrapper around limiter.updateSettings() that nulls the stale heartbeat reference and re-invokes _startHeartbeat() afterward so it takes the "start a fresh interval" branch again. Route all 5 updateSettings() call sites through it (updateAllLimiterSettings, both updateFromHeaders() branches, loadPersistedLimits(), and updateFromResponseBody()). updateAllLimiterSettings is now async and awaited by its two callers (initializeRateLimits, applyRequestQueueSettings); the sync call sites use the existing trackAsyncOperation() fire-and-forget tracking pattern. tests/integration/combo-matrix/weighted.test.ts is the E2E proof: the "weighted: 70/30" case now passes with zero WEDGED/RATE_LIMIT_QUEUE/502 log lines across 200 sequential requests (previously the wedge/recovery cycle inflated its runtime and skewed the distribution toward ~50/50). Refs #8213