fix(redis): wait out the cold-start connect before failing closed - #60
polylane[bot] wants to merge 3 commits into
Conversation
Co-authored-by: polylane[bot] <277585245+polylane[bot]@users.noreply.github.com>
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Superseded by a newer Polylane verdict for Caution Hold this merge. Moderate impact. Why. getRedisClient() returns null while the client it just created is still connecting, and the new fail-closed branch in checkMcpRateLimit (lib/integrations/mcp/rate-limit.ts:62-67) reads that null as an outage whenever REDIS_URL is set. The MCP limiter is only reached from authorizeToolCall for tools/call, so the call that creates the Redis client is itself a tool call: the first MCP tool call handled by every fresh serverless instance now returns an error where main served it, and lib/collections/link-reachability.ts:111-118 refuses probes on the same state. flowchart LR
A["first tools/call on a fresh instance"] --> B["getRedisClient()"]
B --> C{"client exists?"}
C -->|"no"| D["createClient + connect(), not awaited"]
D --> E["isReady false -> null"]
C -->|"yes, not ready"| E
E --> F{"REDIS_URL set?"}
F -->|"yes"| G["unavailable -> tool call errors"]
F -->|"no"| H["allowed"]
Reaches Before merging: Fail closed only for an established client that has disconnected: await connect() in getRedisClient() (or expose a connection state so "connecting" is not "unreachable"), keeping a real outage fail-closed. Evidence · 5 stepsTrigger: the first MCP tools/call (or first check-links invocation) handled by a freshly created serverless instance while REDIS_URL is set — the call that creates the Redis client always observes isReady=false (Deterministic, 1 rejection per fresh instance (lib/common/redis.ts:107-117 returns null after starting connect()), not a measured rate: Vercel's metrics API returned 'the team does not have Observability Plus' and runtime function logs are not exposed, so no production numerator/denominator exists; the deployment event stream is the only readable source.)
Traffic: Observability gap: Vercel metrics unavailable for this workspace (returned reason: no Observability Plus) and runtime function log lines are not exposed; readable evidence is the current production deployment manifest of dpl_5XnF2219xJ3VhHRP4DumSDnHT5sR, which lists /mcp and /mcp/prompt as dynamic functions, and the project's env keys, which include REDIS_URL. The likelihood rests on the deterministic per-instance code path rather than measured request rates. Blast radius · 3 resources
Also considered · 2 refuted
Vercel build route manifest — /mcp deployed as a dynamic function · ├ ƒ /mcpFull log (3 of 100 lines)├ ƒ /mcp
├ ƒ /mcp/prompt
ƒ (Dynamic) server-rendered on demandAnalysed against 1 Project and 1 repository
Polylane analysed Did this help? React 👍 or 👎 so the next review is sharper. |
| }; | ||
| } | ||
| if (rateLimit.status === "unavailable") { | ||
| return { result: rateLimitUnavailableResult() }; |
There was a problem hiding this comment.
The new rejection path logs nothing identifying which user's call was refused, so an operator cannot tell whose traffic is being turned away during the outage.
| return { result: rateLimitUnavailableResult() }; | |
| return { result: rateLimitUnavailableResult(auth.userId) }; |
| function rateLimitUnavailableResult(): CallToolResult { | ||
| log.warn("rate limit unavailable; rejecting request"); |
There was a problem hiding this comment.
Adds the user identifier to the rate-limit-unavailable event so rejected calls can be attributed to a user after the fact.
| function rateLimitUnavailableResult(): CallToolResult { | |
| log.warn("rate limit unavailable; rejecting request"); | |
| function rateLimitUnavailableResult(userId: string): CallToolResult { | |
| log.warn("rate limit unavailable; rejecting request", { userId }); |
Co-authored-by: polylane[bot] <277585245+polylane[bot]@users.noreply.github.com>
Verdict: the review is correct, and the pull request is updatedThe finding holds against the code. What changed
Validation
Remaining work, unchanged by this diff: the Redis endpoint still has no health check or alert, and the warning now emitted is only usable if the runtime log stream is collected, which this account cannot read (no queryable metrics, no log drain permission). Whether Pushed in 29f4d16. |
|
Tip Expected to resolve the linked issue. Linked issue: iss_0b88eaf19001i1vfn6j66obs — cache-app: Redis is an unmonitored single dependency and its fail-open paths silently remove rate limiting The concern raised on an earlier push appears addressed as of Delta over 29f4d16 adds only userId/bucket/amount fields to existing warn logs — no request-path change. Vercel's metrics API returns nothing for this account; the head build completed clean and cache-app's log digest shows 0 error events/hour (~72h). Also considered · 1 plausible, 4 refuted
cache-app head deployment build events · Build Completed in /vercel/output [36s]
Deployment completedFull log (9 of 192 lines)├ ƒ /mcp
├ ƒ /mcp/prompt
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
Build Completed in /vercel/output [36s]
Deploying outputs...
Deployment completed
Creating build cache...
Created build cache: 1mAnalysed against 1 Project and 1 repository
Polylane analysed Rate this review: 👍 helpful · 👎 not helpful · 😕 confusing Tip Improvements: #63. Merge it into your branch before merging this pull request. |
| : { status: "allowed" }; | ||
| } catch (error) { | ||
| log.warn("MCP rate limit counter failed; failing closed", { | ||
| bucket: bucket.name, |
There was a problem hiding this comment.
The fail-closed counter log records the bucket and error but not which token's user hit it, so an outage's blast radius on the MCP surface cannot be attributed from the event.
| bucket: bucket.name, | |
| bucket: bucket.name, | |
| userId, |
|
Polylane opened #62 with observability improvements for the code this pull request changes. It targets this pull request's branch, so you can merge it into your branch to fold the improvements in before this pull request lands. Polylane keeps it updated as this pull request evolves. If the changes are not wanted, close it and Polylane will not open another one for this pull request. |
…probe count (#62) Co-authored-by: Gilberto <37253958+gilsmt@users.noreply.github.com> Co-authored-by: polylane[bot] <277585245+polylane[bot]@users.noreply.github.com>
| bucket: { name: string }, | ||
| retryAfterSeconds: number | ||
| ): CallToolResult { | ||
| log.warn(`rate limit hit (${bucket.name})`, { retryAfterSeconds }); |
There was a problem hiding this comment.
The bucket name is interpolated into the message today, so read and write limit hits cluster as two separate templates; passing it as a field makes rate-limit hits countable and filterable by bucket.
| log.warn(`rate limit hit (${bucket.name})`, { retryAfterSeconds }); | |
| log.warn("rate limit hit", { bucket: bucket.name, retryAfterSeconds }); |
|
Polylane opened #63 with observability improvements for the code this pull request changes. It targets this pull request's branch, so you can merge it into your branch to fold the improvements in before this pull request lands. Polylane keeps it updated as this pull request evolves. If the changes are not wanted, close it and Polylane will not open another one for this pull request. |
Fixes: cache-app: Redis is an unmonitored single dependency and its fail-open paths silently remove rate limiting
When Redis is configured but its connection is still being established, the app treated that connecting state as an outage and rejected MCP tool calls and link-reachability probes, so the first call handled by every fresh instance failed. The change waits briefly for the connection to settle before deciding Redis is unavailable, so a cold start succeeds while a genuine outage still fails closed. Deployments without
REDIS_URLare unchanged.What caused this
Affected:
int_0b563423d00164u048hh0sykWhy this fix
The review is correct, and the read is confirmed in the client library.
getRedisClientreturns null whenever the client's socket is not ready, and node-redisisReadyreflects socket readiness whileconnect()settles only once the socket is ready. On a fresh instance the first call therefore always arrived before readiness, and the new fail-closed branches read that null as an outage: the MCP tool call was rejected and the first link-probe batch was refused. The transient connects in milliseconds; a genuine outage does not.This revision separates the two states. Callers that bound abuse now go through a readiness-aware accessor that waits, bounded to one second, for an in-flight connect to settle before deciding Redis is unavailable. A cold start resolves to a usable client; a Redis that is genuinely down or reconnecting still resolves to unavailable and still fails closed, so the throttle on a stolen MCP token and the outbound-probe budget are not weakened. The bound keeps a down dependency from stalling the request.
Two supporting details changed with it. The degraded-state warning no longer fires during the initial connect, so a normal cold start is not logged as an outage; only the loss of an established connection warns. The cache path is untouched and still degrades silently, which is correct for a cache.
Remaining and outside this diff: the Redis endpoint still has no health check or alert, and the warning this adds is only usable if the runtime log stream is collected, which this account cannot read.
Out of scope
REDIS_URLpoints at a single instance with no replica or failover: the environment value is not readable from this workspace.7 files changed (+396/-72)
app/mcp/route.ts: modified, +26/-11lib/collections/link-reachability.test.ts: added, +47/-0lib/collections/link-reachability.ts: modified, +25/-6lib/common/redis.test.ts: added, +62/-0lib/common/redis.ts: modified, +116/-10lib/integrations/mcp/rate-limit.test.ts: added, +75/-0lib/integrations/mcp/rate-limit.ts: modified, +45/-45Generated by Polylane. You can ask follow-ups by mentioning @polylane in a comment.