fix(midnight-liquidation): add discovery cooldown breaker and rate cap - #100
Draft
BZahory wants to merge 1 commit into
Draft
fix(midnight-liquidation): add discovery cooldown breaker and rate cap#100BZahory wants to merge 1 commit into
BZahory wants to merge 1 commit into
Conversation
CRTR-2857: fetchWithRetry honors Retry-After within one discovery pass, but the runner re-fired a full scan (up to 100 pages) every ~2s block with no cross-tick memory of the throttle, sustaining a 429 storm against the Blue API's shared WAF budget. Two bot-side defenses, env-tunable: - withDiscoveryCooldown latches discovery off after a failed pass for min(max, max(serverRetryAfter, base*2^(failures-1))); latched ticks run with zero candidates and auto-resume when the window expires. fetchWithRetry now throws HttpRetryExhaustedError (message unchanged) so the breaker can seed from the server's own Retry-After. - A token bucket caps candidate-page requests (retries included), since the endpoint's CDN (stale-while-revalidate) can mask a throttled origin as a 200 that would never trip the breaker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Incident hardening for CRTR-2857 (Sev 1, 2026-07-25): the midnight-liquidation bot's discovery
traffic drove a sustained 429 retry storm against the Blue API GraphQL edge (WAF rule
RateLimitMidnightLiquidationBot, ~33 rps global budget shared across all bot instances via thecandidates endpoint's server-side fan-out). ~91% of requests were WAF-blocked for 5+ hours and the
bot went effectively blind.
The structural gap:
fetchWithRetryhonors a 429'sRetry-Afterwithin one discovery pass,but the runner re-fires a full scan (up to
MAX_DISCOVERY_PAGES = 100pages,include_maturedalways on) every ~2s block with no memory that the last pass was throttled. Under a throttle, that
re-hammering is the outage.
Two bot-side defenses, both env-tunable:
Cross-tick discovery circuit breaker (
withDiscoveryCooldown): a failed discovery passlatches discovery off for
min(max, max(serverRetryAfter, base·2^(failures−1)))— seeded fromthe server's own
Retry-Afterwhen it sent one (surfaced via the newHttpRetryExhaustedErrorin@repo/utils), with the exponential ramp as a floor so adegenerate short header can't defeat the breaker. Latched ticks log
discover.cooldownand runwith zero candidates — which the tick already tolerates (discovery is a coverage source, never a
correctness dependency). The first successful pass resets the latch
(
discover.cooldown_reset). Defaults: base 60s, cap 600s (above the WAF's ownRetry-After: 300).Client-side rate cap on candidate-page fetches: a token bucket (
createTokenBucket, thesame idiom as the venue-quoting
HTTP_RPSlimiter) gates every page request, retries included.This is load-bearing, not belt-and-braces: the candidates endpoint sits behind CloudFront with
Cache-Control: public, max-age=2, stale-while-revalidate=1(verified live), so a throttledorigin can be masked as a
200served stale — in which case no error ever surfaces and thebreaker alone would never trip. Defaults: 2 rps, burst 4 — conservative because the WAF counter
is
CONSTANT(one global budget shared by all instances, amplified by the server-side GraphQLfan-out).
Fail-safe reasoning
During a throttle the bot is already ~90%+ blind, so backing off costs approximately nothing in
coverage, lets the origin recover, and discovery auto-resumes when the window expires. Backing off
a throttled endpoint is strictly correct. Failures still propagate to the tick's existing
discover.errorpath, and pending-queue maintenance is unaffected (it runs outside the tick).New env knobs (all optional)
DISCOVERY_COOLDOWN_BASE_MS600000disables the exponential floor (serverRetry-Afterstill honored).DISCOVERY_COOLDOWN_MAX_MS600000DISCOVERY_HTTP_RPS2DISCOVERY_HTTP_BURST4@repo/utilschangefetchWithRetrynow throwsHttpRetryExhaustedError(carryingstatus+ parsedretryAfterMs)when the retry budget is exhausted on a 429/5xx. The message is byte-identical to the previous
plain
Error, and all existing consumers (blue-liquidation, monitor-bot) assert on messages only —verified by the full suite.
Root-cause attribution caveat
The WAF evidence keys on "the midnight bot's auth string", but this bot sends no auth — it
calls the public REST candidates endpoint, and the keyed GraphQL traffic comes from that endpoint's
server-side fan-out. Per the integration team, the legacy Blue/Midnight bots (running in parallel)
DO hold API keys, so the keyed volume may be partly or wholly theirs rather than this bot's.
Distinguishing needs Platform data (WAF source IPs: markets-API backend egress vs legacy-bot
egress). This hardening is correct and low-cost regardless of the split — this bot's cross-tick
discovery volume was unbounded either way.
Out of scope / follow-up
dangerous (12–18× origin step; cf. the 2026-06-27 V8-heap OOM on this key).
need to re-scan the full universe every block). Tracked as a separate refactor, not done here.
withDiscoveryCooldownand hoist it into@repo/bot-kitso blue-liquidation (samefetchWithRetryre-hammering exposure) can reuse it; write a TIB capturing the circuit-breakerpattern choice and the default calibration against the WAF budget.
Verification
bun test: 844 pass / 2 pre-existing fork-suite failures (requireRPC_URL_8453+anvil; fail identically on a clean tree).
Retry-Afterseeding + floor,reset-on-success; rate-cap slot ordering (retries included);
HttpRetryExhaustedErrorfields +message stability; config knob defaults/overrides/validation. Break-one-assertion check done.
bun run --filter @morpho-org/midnight-liquidation typecheck,--filter @repo/utils typecheck:clean.
bun lint: 0 warnings.bun format: applied.bun run --filter @morpho-org/monitor-bot test(affected via@repo/utils): 222 pass.@repo/utilsconsumer safety all verified clean.🤖 Generated with Claude Code