fix(rate-limit): stop the test-context abort racing the decision (#6236) - #6405
Merged
koala73 merged 1 commit intoAug 10, 2026
Merged
Conversation
…la73#6236) The endpoint limiter arms two deadlines: the SDK's availability-first decision race (ENDPOINT_RATE_LIMIT_TIMEOUT_MS) and an AbortSignal.timeout that cancels the underlying Upstash fetch just before it (ENDPOINT_REDIS_ABORT_TIMEOUT_MS). The abort must land first or the transport is left pending in the isolate, which is what tests/rate-limit.test.mts pins. The ordering held in production but not under the node test runner. Production's 500ms gap dwarfs the few milliseconds the Upstash client needs to build a request and call the signal factory, and the abort timer does not start until that call while the decision timer starts at limit(). The NODE_TEST_CONTEXT pair mirrored the production ratio at 25/20, which left a 5ms gap against an arming cost measured at 8-71ms, so as little as 1.1ms of headroom survived. Under --test-concurrency=16 that inverted and the suite went red on an assertion about the transport, not about the limiter. Widen the test-context decision deadline to 250ms so the gap exceeds the arming cost rather than mirroring a ratio that only works when arming is free. The abort stays at 20ms, so failure paths still resolve fast: the one test that waits out the full decision deadline costs ~120ms more and nothing else changes.
|
@Yigtwxx is attempting to deploy a commit to the World Monitor Team on Vercel. A member of the Team first needs to authorize it. |
25 tasks
Owner
|
Thank you @Yigtwxx |
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
Fixes the CI flake that turned
unitred on #6044 (job): 22600 pass, 1 fail, and the one failure wastests/rate-limit.test.mts— a file that PR does not touch.The endpoint limiter arms two deadlines and their order is load-bearing:
The abort has to fire before the SDK's availability-first race resolves, or the decision returns while the Upstash fetch is still pending in the isolate — which is exactly what the assertion checks.
The two timers do not start at the same moment.
timeout: ENDPOINT_RATE_LIMIT_TIMEOUT_MS(:565) starts whenrl.limit()is called, butsignal: () => AbortSignal.timeout(ENDPOINT_REDIS_ABORT_TIMEOUT_MS)(:560) is a per-request factory the Upstash client only invokes when it builds the request. So the real ordering isarmingCost + abortMsagainstdecisionMs, and the configured gap has to cover the arming cost.In production it does, comfortably: the gap is 500 ms and arming is a few milliseconds. The test-context pair mirrored the production ratio instead of the production margin, leaving 5 ms — and under the node test runner arming costs far more than that, because each case re-imports the module through
tsxwith a cache-busting query.I instrumented the exact path with the test's own stub fetch, 10 runs on an idle machine:
1.1 ms of headroom is not a margin, it is a coin flip, and
--test-concurrency=16on a 2-core runner is what flips it.The fix, and the alternative I rejected
Widen the test-context decision deadline to
250, leaving the abort at20. The gap becomes 230 ms — larger than the worst arming cost observed — and the comment now says why the test pair deliberately is not the production ratio scaled down.The alternative was to leave the constants alone and make the assertion poll for the abort with a bounded deadline. I rejected it because it changes what the test claims: the name is "abort ... before failing closed", and an eventual-cancellation assertion would pass even if the ordering regressed for real. Fixing the margin keeps the original claim and makes it deterministic.
Scaling the production ratio down (e.g.
250/225) would also be wrong here: it restores a 25 ms gap, which is still inside the observed arming range.Verification
The new guard is mutation-proven — reverting
250to25with everything else intact:Suites:
Cost of the wider deadline, measured both ways on the same machine:
Only the one case that genuinely waits out the decision deadline (
fail closed when Redis ignores abort until the SDK timeout) pays for it; every other path still resolves on the 20 ms abort.Other gates:
npm run lint:rate-limit-policiesis not runnable on Windows locally —scripts/enforce-rate-limit-policies.mjsbuilds afile:///C:/C:/...URL and fails withERR_MODULE_NOT_FOUNDon a clean checkout ofmaintoo, before this change. It is unaffected either way: no policy table is touched.Out of scope
5_000/4_500are unchanged. The bug is in the test-context pair only.signalfactory. Arming the abort atlimit()time instead of at request-build time would remove the dependency on arming cost entirely, but it means reaching past the Upstash client's own request lifecycle. The margin fix is the smaller change and it closes the observed failure.getRatelimit) andcheckScopedRateLimit, which do not use these deadlines.api/_rate-limit.jsmirror, which only references these constants in a comment.Type of change
Affected areas
/api/*) — the endpoint rate limiter's test-context deadlines only; production behaviour is byte-identicaluniton the full suiteChecklist
NODE_TEST_CONTEXT; there is no runtime or user-visible surface to exercise.api/rss-proxy.jsallowlist (if adding feeds) — N/A, no feeds added.npm run typecheck)Documentation Alignment Checklist
N/A — no published documentation claim changes. Nothing here is documented, published, or read by a consumer: the values are internal test-context constants and the new export is test-only. Listed for completeness: