fix: add RPC timeout to fetchSlab/getSlot to prevent indefinite hangs#153
fix: add RPC timeout to fetchSlab/getSlot to prevent indefinite hangs#1530x-SquidSol wants to merge 1 commit intodcccrypto:mainfrom
Conversation
…nite hangs fetchSlab() from @percolator/sdk and getConnection().getSlot() from @percolator/shared do not accept AbortSignal and have no internal timeout. If the Solana RPC node is slow or unresponsive, requests to /markets/:slab, /api/adl/rankings, and /health hang indefinitely. This adds a shared withRpcTimeout() utility using Promise.race: - Route endpoints (markets, adl): 10s default timeout, returns 504 - Health check: 5s timeout, reports checks.rpc=false on timeout - Both values configurable via RPC_TIMEOUT_MS / HEALTH_RPC_TIMEOUT_MS env vars - Custom RpcTimeoutError class for precise catch handling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughA new RPC timeout utility is introduced to enforce bounded execution durations on RPC calls. The utility is applied to Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/rpc-timeout.ts (1)
15-19: Harden timeout env parsing to avoid invalid runtime values.Line 15 and Line 18 currently rely on
Number(...) || fallback, which can silently accept problematic values (for example, negative/non-finite values). Prefer explicit finite/positive validation before using them as timeouts.Suggested patch
const DEFAULT_RPC_TIMEOUT_MS = 10_000; const DEFAULT_HEALTH_RPC_TIMEOUT_MS = 5_000; -export const RPC_TIMEOUT_MS: number = - Number(process.env.RPC_TIMEOUT_MS) || DEFAULT_RPC_TIMEOUT_MS; +function parseTimeoutMs(raw: string | undefined, fallback: number): number { + const parsed = Number(raw); + if (!Number.isFinite(parsed) || parsed <= 0) return fallback; + return Math.floor(parsed); +} -export const HEALTH_RPC_TIMEOUT_MS: number = - Number(process.env.HEALTH_RPC_TIMEOUT_MS) || DEFAULT_HEALTH_RPC_TIMEOUT_MS; +export const RPC_TIMEOUT_MS: number = parseTimeoutMs( + process.env.RPC_TIMEOUT_MS, + DEFAULT_RPC_TIMEOUT_MS, +); + +export const HEALTH_RPC_TIMEOUT_MS: number = parseTimeoutMs( + process.env.HEALTH_RPC_TIMEOUT_MS, + DEFAULT_HEALTH_RPC_TIMEOUT_MS, +);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/rpc-timeout.ts` around lines 15 - 19, The RPC_TIMEOUT_MS and HEALTH_RPC_TIMEOUT_MS constants currently use Number(...) || fallback which accepts invalid values; change them to explicitly parse and validate the env values (e.g., use parseInt or Number, check Number.isFinite(value) and value > 0, optionally Math.floor to ensure an integer) and only use the parsed value when it is a finite positive number, otherwise fall back to DEFAULT_RPC_TIMEOUT_MS and DEFAULT_HEALTH_RPC_TIMEOUT_MS respectively; update references to RPC_TIMEOUT_MS and HEALTH_RPC_TIMEOUT_MS in the file accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/utils/rpc-timeout.ts`:
- Around line 15-19: The RPC_TIMEOUT_MS and HEALTH_RPC_TIMEOUT_MS constants
currently use Number(...) || fallback which accepts invalid values; change them
to explicitly parse and validate the env values (e.g., use parseInt or Number,
check Number.isFinite(value) and value > 0, optionally Math.floor to ensure an
integer) and only use the parsed value when it is a finite positive number,
otherwise fall back to DEFAULT_RPC_TIMEOUT_MS and DEFAULT_HEALTH_RPC_TIMEOUT_MS
respectively; update references to RPC_TIMEOUT_MS and HEALTH_RPC_TIMEOUT_MS in
the file accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4c7c5e74-9fb3-402f-bc2a-e9df38bd4f35
📒 Files selected for processing (4)
src/routes/adl.tssrc/routes/health.tssrc/routes/markets.tssrc/utils/rpc-timeout.ts
|
Summary
fetchSlab()from@percolator/sdkandgetConnection().getSlot()from@percolator/sharedhave no timeout. If the Solana RPC node is slow or unresponsive, requests to/markets/:slab,/api/adl/rankings, and/healthhang indefinitely — tying up server connections with no upper bound.withRpcTimeout()utility usingPromise.racethat wraps these opaque SDK calls with a configurable deadline.Changes
src/utils/rpc-timeout.tswithRpcTimeout(),RpcTimeoutErrorclass, env-configurable defaultssrc/routes/markets.tsfetchSlab()at L126 with 10s timeout, returns 504 on timeoutsrc/routes/adl.tsfetchSlab()at L176 with 10s timeout, returns 504 on timeoutsrc/routes/health.tsgetSlot()at L29 with 5s timeout, reportschecks.rpc=falseDesign decisions
Promise.race(notAbortSignal) —fetchSlabandgetSlotdon't accept signalsRPC_TIMEOUT_MS/HEALTH_RPC_TIMEOUT_MSenv vars — tunable on Railway without redeployRpcTimeoutError— enablesinstanceofchecks and Sentry groupingTest plan
tsc --noEmitpasses (zero type errors)vitest runpasses (186/186 tests)/markets/:slabwith an unreachable RPC to verify 504 responseRpcTimeoutErrorseparately from generic 500s🤖 Generated with Claude Code
Summary by CodeRabbit