Skip to content

feat(rpc): add timeout and circuit breaker for blockchain RPC calls - #1036

Open
retkatmun wants to merge 1 commit into
Smartdevs17:mainfrom
retkatmun:feat/rpc-timeout-circuit-breaker
Open

feat(rpc): add timeout and circuit breaker for blockchain RPC calls#1036
retkatmun wants to merge 1 commit into
Smartdevs17:mainfrom
retkatmun:feat/rpc-timeout-circuit-breaker

Conversation

@retkatmun

Copy link
Copy Markdown

Summary

Closes #941

Implements production-ready timeout and circuit breaker protection for all external blockchain RPC calls in SubTrackr.


Problem

Without this fix, any slow or unreachable RPC endpoint causes:

  • Indefinite hangsprovider.getBalance() / getGasPrice() never rejects; the caller waits forever
  • Cascading failures — one bad node blocks gas estimation, balance checks, stream creation
  • No fallback — a single URL was hard-coded with no secondary provider

What changed

New files

File Purpose
backend/services/shared/rpcTimeout.ts withRpcTimeout (AbortController+jitter), wrapWithTimeout (Promise.race), typed errors, type guards, defaultTimeoutForChain
backend/services/shared/rpcResilienceMiddleware.ts ResilientEthersProvider (extends MonitoringJsonRpcProvider) + factory + singleton registry
src/services/rpcProvider.ts Client-side (React Native) ResilientJsonRpcProvider — per-URL circuit breaker + timeout + ordered URL fallback
backend/services/shared/__tests__/rpcTimeout.test.ts Unit tests — 25+ cases
backend/services/shared/__tests__/rpcResilienceMiddleware.test.ts Unit tests — factory, registry, send override
backend/services/shared/__tests__/walletServiceRpc.integration.test.ts Integration tests — 7 suites
backend/benchmark/rpcBenchmark.ts 9 benchmarks with budget gating
docs/rpc-resilience.md Architecture, API reference, config, error types, test commands

Modified files

File Change
src/services/walletService.ts getProvider() now calls getOrCreateResilientProvider() instead of new JsonRpcProvider()
backend/services/shared/index.ts Export rpcTimeout and rpcResilienceMiddleware public API

Architecture

walletService.getProvider(chainId)
    │
    └─► ResilientJsonRpcProvider        [src/services/rpcProvider.ts]
          ├─ All fallback URLs from EVM_RPC_URLS (primary + 1–2 backups)
          ├─ Per-URL circuit breaker  (closed → open → half-open → closed)
          └─ Per-call AbortController deadline (timeout + jitter)

Circuit breaker states

  CLOSED ──[N consecutive failures]──► OPEN
     ▲                                  │ recoveryTimeoutMs
     │ probe succeeds           ◄────── HALF-OPEN
     │                                  │
     └──────────────────────────────────┘ probe fails → back to OPEN

Key API

// Timeout with AbortController (signal-aware, no leaked Promises)
const block = await withRpcTimeout(
  (signal) => fetch(url, { signal }).then(r => r.json()),
  { timeoutMs: 10_000, jitterMs: 500, endpointUrl: url }
);

// Wrap third-party Promise (not signal-aware)
const gasPrice = await wrapWithTimeout(
  provider.getGasPrice(),
  { timeoutMs: 10_000 }
);

// Client-side resilient provider (walletService)
const provider = getOrCreateResilientProvider(1, [
  'https://cloudflare-eth.com',
  'https://rpc.ankr.com/eth',
  'https://eth.llamarpc.com',
]);
const balance = await provider.getBalance(address); // protected

Tests

  • Unit (rpcTimeout.test.ts): withRpcTimeout, wrapWithTimeout, type guards, defaultTimeoutForChain, error types — 25+ cases
  • Unit (rpcResilienceMiddleware.test.ts): factory, registry, send timeout, health snapshot, resetCircuits
  • Integration (walletServiceRpc.integration.test.ts): 7 suites
    • Timeout fires and bubbles as typed error
    • Circuit breaker trips after threshold, recovers after window
    • Ordered URL fallback — primary fails, secondary succeeds
    • Per-URL circuit state in ResilientJsonRpcProvider
    • Registry singleton preserves circuit state across calls
    • Manual operator reset
    • Audit log and dashboard correctness

Performance benchmarks

Metric Budget Result
withRpcTimeout overhead < 1 ms avg
wrapWithTimeout overhead < 1 ms avg
Circuit breaker closed-path < 1 ms avg
p95 all in-process operations < 2 ms
defaultTimeoutForChain > 100 000 ops/s

Acceptance criteria

  • Feature implemented with full functionality
  • Unit tests with >80% coverage of new code
  • Integration tests for critical paths
  • No regression (walletService public API unchanged; ResilientJsonRpcProvider is a drop-in for JsonRpcProvider)
  • Documentation updated (docs/rpc-resilience.md)
  • Performance benchmarks met

…martdevs17#941)

- Add backend/services/shared/rpcTimeout.ts
  withRpcTimeout() — AbortController-based deadline with optional jitter;
  wrapWithTimeout() — Promise.race wrapper for non-signal-aware calls;
  RpcCallTimeoutError / RpcCallCancelledError typed errors;
  isRpcTimeout() / isRpcCancelled() type guards;
  defaultTimeoutForChain() — chain-aware defaults (Eth 10s, Polygon/Arb 15s)

- Add backend/services/shared/rpcResilienceMiddleware.ts
  ResilientEthersProvider extends MonitoringJsonRpcProvider, adds per-send
  AbortController timeout + RpcCircuitBreakerService integration;
  createResilientProvider() factory; getOrCreateResilientProvider() singleton
  registry; ProviderHealthSnapshot / EndpointHealth types

- Add src/services/rpcProvider.ts (client-side, React Native safe)
  ResilientJsonRpcProvider — drop-in for JsonRpcProvider with per-URL
  circuit breaker (closed→open→half-open→closed) + AbortController timeout
  + ordered URL fallback; getOrCreateResilientProvider() singleton registry

- Update src/services/walletService.ts
  WalletServiceManager.getProvider() now calls getOrCreateResilientProvider()
  with all fallback URLs from EVM_RPC_URLS instead of new JsonRpcProvider()

- Update backend/services/shared/index.ts
  Export rpcTimeout and rpcResilienceMiddleware public API

- Add unit tests: rpcTimeout.test.ts (25+ cases), rpcResilienceMiddleware.test.ts
- Add integration tests: walletServiceRpc.integration.test.ts (7 suites —
  timeout, circuit trips/recovers, URL fallback, per-URL circuit, registry,
  manual reset, audit trail)
- Add benchmarks: backend/benchmark/rpcBenchmark.ts (9 benchmarks, budget
  gated: overhead <1ms avg, p95 <2ms, defaultTimeoutForChain >100k ops/s)
- Add docs/rpc-resilience.md

Closes Smartdevs17#941
@drips-wave

drips-wave Bot commented Aug 27, 2026

Copy link
Copy Markdown

@retkatmun Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add timeout and circuit breaker for external blockchain RPC calls

1 participant