Skip to content

feat(backend): replace fixed getTransaction polling with exponential backoff#189

Open
leocagli wants to merge 1 commit into
Stellar-Ecosystem:mainfrom
leocagli:feat/gettransaction-exponential-backoff
Open

feat(backend): replace fixed getTransaction polling with exponential backoff#189
leocagli wants to merge 1 commit into
Stellar-Ecosystem:mainfrom
leocagli:feat/gettransaction-exponential-backoff

Conversation

@leocagli

@leocagli leocagli commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #26

Problem

simulateAndSubmit polled getTransaction with a hardcoded for (let i = 0; i < 20; i++) loop and a fixed 1500ms delay — 30s total, no configurability. Under Soroban network congestion transactions can take longer to confirm, causing premature TRANSACTION_TIMEOUT on valid transactions. Additionally, hitting the RPC every 1.5s at a fixed rate wastes capacity when most waits happen in the first few seconds.

Solution

Replaced the fixed loop with a deadline-based exponential backoff:

const pollInitialMs = config.txPoll?.initialDelayMs ?? 1_500;  // same first poll
const pollMaxMs     = config.txPoll?.maxDelayMs     ?? 10_000; // cap per interval
const pollTimeoutMs = config.txPoll?.maxWaitMs      ?? 60_000; // total budget

let delay = pollInitialMs;
const deadline = Date.now() + pollTimeoutMs;

while (Date.now() < deadline) {
  getResult = await server.getTransaction(hash);
  if (getResult.status !== 'NOT_FOUND') break;
  const remaining = deadline - Date.now();
  await sleep(Math.min(delay, remaining));  // never overshoot deadline
  delay = Math.min(delay * 2, pollMaxMs);
}

Default behavior: 1.5s → 3s → 6s → 10s → 10s … (capped) for up to 60s total. Same first-poll latency as before; 7× fewer RPC calls under congestion vs. 1.5s fixed.

Configurable via env vars (documented in .env.example):

  • TX_POLL_INITIAL_DELAY_MS (default 1500)
  • TX_POLL_MAX_DELAY_MS (default 10000)
  • TX_POLL_MAX_WAIT_MS (default 60000)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Chores
    • Transaction confirmation polling is now configurable with exponential backoff strategy. Adjust polling parameters via environment variables to customize initial delay, maximum delay, and total wait timeout for transaction submission.

…backoff

Closes Stellar-Ecosystem#26

simulateAndSubmit polled getTransaction 20 times with a fixed 1.5s delay
(30s total). Under network congestion this could time out on transactions
that succeed after ~35s, while also hammering the RPC every 1.5s regardless
of load.

Changes:
- contract.js: while-loop with deadline instead of for-loop with fixed count.
  Delay starts at initialDelayMs and doubles each round up to maxDelayMs.
  Remaining time is respected: the last sleep is capped to not overshoot the
  deadline. Defaults: 1.5s initial, 10s max, 60s total — same first poll as
  before, but far fewer calls under congestion.
- config.js: txPoll.initialDelayMs / maxDelayMs / maxWaitMs (parsePositiveInt
  with sane defaults; warns on NaN so a typo cannot silently disable limits)
- .env.example: documented TX_POLL_INITIAL_DELAY_MS / MAX_DELAY_MS / MAX_WAIT_MS
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The fixed 20-iteration, 1.5s-delay polling loop in simulateAndSubmit is replaced with a deadline-based while loop using exponential backoff. Three new txPoll config fields (initialDelayMs, maxDelayMs, maxWaitMs) are added to config.js, sourced from new TX_POLL_* env vars documented in .env.example.

Changes

Exponential Backoff for getTransaction Polling

Layer / File(s) Summary
txPoll config and env var documentation
backend/src/config.js, backend/.env.example
config gains a txPoll object with initialDelayMs (1500), maxDelayMs (10 000), and maxWaitMs (60 000) parsed from TX_POLL_INITIAL_DELAY_MS, TX_POLL_MAX_DELAY_MS, and TX_POLL_MAX_WAIT_MS via parsePositiveInt; .env.example documents these vars with example values.
Deadline-based exponential backoff polling loop
backend/src/lib/contract.js
simulateAndSubmit replaces the fixed for loop (20 × 1.5s) with a while loop that computes an absolute deadline from config.txPoll.maxWaitMs, sleeps with exponentially increasing delays capped by maxDelayMs and remaining deadline time, and throws TRANSACTION_TIMEOUT when the deadline passes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 No more counting hops — just watch the clock!
I'll backoff gently, tick by tick,
Exponential patience does the trick.
If the deadline's missed, I'll squeak the error clear,
A smarter poller now lives here! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements the core requirement from issue #26 to replace fixed 20×1.5s polling with configurable exponential backoff. However, the PR does not include automated test coverage updates for the touched backend path as required by acceptance criteria. Add or update automated tests for the simulateAndSubmit polling logic in backend/src/lib/contract.js to cover the new exponential backoff behavior and deadline enforcement.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing fixed polling with exponential backoff for getTransaction in the backend.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing exponential backoff polling: configuration parsing in config.js, polling logic in contract.js, and documentation in .env.example. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/lib/contract.js (1)

64-87: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add automated tests for the new backoff/deadline behavior before merge.

This path changed significantly, but no accompanying test updates are included for deadline stop, exponential growth, max-delay cap, and timeout emission behavior.

As per coding guidelines, the stated acceptance criteria require “Automated coverage is added or updated for the touched backend path.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/lib/contract.js` around lines 64 - 87, Add automated tests for
the polling logic in the transaction handling code. Create tests that verify the
deadline stop behavior (the while loop exits when Date.now() >= deadline),
exponential backoff growth (delay doubles each iteration up to the max), the
max-delay cap enforcement (delay is capped at pollMaxMs), and timeout behavior
when the polling deadline is exceeded. The tests should mock
server.getTransaction to simulate various response scenarios and verify that the
remaining time calculation accounts for elapsed time correctly. These tests are
required per the coding guidelines to ensure the touched polling path with
deadline and exponential backoff logic is properly covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/lib/contract.js`:
- Around line 64-70: The delay variable is initialized to pollInitialMs without
checking if it exceeds the configured maximum. When initialDelayMs is greater
than maxDelayMs, the first polling wait will violate the configured maximum
delay cap. Clamp the delay variable initialization by taking the minimum of
pollInitialMs and pollMaxMs to ensure the first wait respects the configured
maximum delay threshold.

---

Outside diff comments:
In `@backend/src/lib/contract.js`:
- Around line 64-87: Add automated tests for the polling logic in the
transaction handling code. Create tests that verify the deadline stop behavior
(the while loop exits when Date.now() >= deadline), exponential backoff growth
(delay doubles each iteration up to the max), the max-delay cap enforcement
(delay is capped at pollMaxMs), and timeout behavior when the polling deadline
is exceeded. The tests should mock server.getTransaction to simulate various
response scenarios and verify that the remaining time calculation accounts for
elapsed time correctly. These tests are required per the coding guidelines to
ensure the touched polling path with deadline and exponential backoff logic is
properly covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d58f7d9b-705c-4cde-b78a-dd1e442e0284

📥 Commits

Reviewing files that changed from the base of the PR and between 6fb6da9 and d053a7e.

📒 Files selected for processing (3)
  • backend/.env.example
  • backend/src/config.js
  • backend/src/lib/contract.js

Comment on lines +64 to +70
const pollInitialMs = config.txPoll?.initialDelayMs ?? 1_500;
const pollMaxMs = config.txPoll?.maxDelayMs ?? 10_000;
const pollTimeoutMs = config.txPoll?.maxWaitMs ?? 60_000;

let getResult;
for (let i = 0; i < 20; i++) {
let delay = pollInitialMs;
const deadline = Date.now() + pollTimeoutMs;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp the initial delay to the configured max before first sleep.

Line 69 initializes delay to pollInitialMs, so when initialDelayMs > maxDelayMs, the first wait exceeds the configured max cap.

Suggested fix
-  let delay = pollInitialMs;
+  let delay = Math.min(pollInitialMs, pollMaxMs);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/lib/contract.js` around lines 64 - 70, The delay variable is
initialized to pollInitialMs without checking if it exceeds the configured
maximum. When initialDelayMs is greater than maxDelayMs, the first polling wait
will violate the configured maximum delay cap. Clamp the delay variable
initialization by taking the minimum of pollInitialMs and pollMaxMs to ensure
the first wait respects the configured maximum delay threshold.

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

Labels

None yet

Projects

None yet

1 participant