fix: move Stellar network calls outside database transactions#86
Conversation
The claimReward and credentialService.mint functions were calling invokeContract (which involves network calls to Stellar RPC, simulation, and Horizon submission) inside a db.transaction. With WRITE_TIMEOUT_MS of 30s and retry logic, a single call could hold a database connection for 30+ seconds. With a pool of only 10 connections, this could exhaust the pool under concurrent load. This fix implements a two-phase approach: 1. Validate and mark as pending in a quick DB transaction 2. Execute Stellar transaction outside DB (no connection held) 3. Update DB with result in a separate quick transaction A new reward_pending field tracks claims in progress to prevent concurrent claims on the same submission.
DeFiVC
left a comment
There was a problem hiding this comment.
Review
PR Description
Excellent — detailed, well-structured, covers motivation, implementation approach, tradeoffs, risks, and rollback plan. One of the better PR descriptions I've seen.
CI
All green: Lint & Typecheck pass, Test pass.
Issue Linkage
Correctly closes #75. PR scope matches issue requirements exactly.
Code Review
Migration (0004_add_reward_pending.sql): Clean. Uses IF NOT EXISTS for safety, partial index on reward_pending = true is the right choice — only indexes pending claims, not all submissions.
Schema (schema.ts): Straightforward addition. No issues.
credential.service.ts: Solid two-phase refactor. Validation + data collection in Phase 1 (quick tx), Stellar execution in Phase 2 (no connection held), DB insert in Phase 3. The Redis lock (withLock) still provides concurrency control. One observation: if the process crashes between Phase 2 and Phase 3, the credential insert fails and the transaction never completes. There's no pending state tracking like in reward.service.ts — but this is acceptable since the lock prevents concurrent attempts and the insert simply wouldn't happen.
reward.service.ts: Well implemented. The two-phase approach is cleaner here with explicit rewardPending state tracking. All error paths (circuit breaker, general failure, bad_seq) properly clean up pending state before returning/throwing. The return null pattern for early exits in Phase 1 is a reasonable design choice.
Tests: Updated to reflect two-transaction flow. Mock setup changes are appropriate for the new structure.
Concerns
-
Pending state stuck claims: As noted in the PR description, if the process crashes between Phase 2 and Phase 3, claims stay in
reward_pending = true. No automated recovery is included. This is acceptable for now, but worth tracking as a follow-up — a background job that retries or resets stuck pending claims would be valuable. -
Minor observation: In
processRewardClaim, the error handler inside Phase 2 catches general errors and marksrewardFailed = true+ clears pending state, but the original code just re-throws. The new behavior is an improvement (cleaner state management).
Verdict
This is a well-executed PR. The two-phase approach correctly addresses the root cause (holding DB connections during Stellar network calls). Tests pass, CI is green, and the implementation is minimal and focused.
Approve.
Closes #75
Type of Change
Summary
Move Stellar network calls (
invokeContract) outside database transactions to prevent holding connections for 30+ seconds. TheclaimRewardandcredentialService.mintfunctions were executing Stellar RPC calls, simulation, and Horizon submission insidedb.transaction, which could exhaust the 10-connection pool under concurrent load.Motivation / Context
Both
claimRewardandcredentialService.mintcallinvokeContractinside adb.transaction. The Stellar calls haveWRITE_TIMEOUT_MS = 30_000with retry logic and circuit breaker, meaning a single call could hold a database connection for up to 30+ seconds. With a connection pool of only 10, this could exhaust the pool and block all other database operations.Detailed Changes
Implemented a two-phase approach for reward claims and credential minting:
Phase 1: Validate and reserve (quick DB transaction)
reward_pending = trueto prevent concurrent claimsPhase 2: Execute Stellar transaction (no DB connection held)
invokeContractfor the Stellar transactionPhase 3: Update result (quick DB transaction)
Added new
reward_pendingboolean field toquiz_submissionstable with an index for efficient pending claim lookups.Current Behavior vs. New Behavior
Before: Database connections held for 30+ seconds during Stellar network calls, risking pool exhaustion under concurrent load.
After: Database connections only held during quick validation and update operations (< 100ms typically). Stellar network calls execute without holding database connections.
Testing
process-reward-claim.test.tsandconcurrent-safety.test.tsto verify two-phase behaviorTradeoffs
withLock) still prevents concurrent claims on the same submission, maintaining consistencyreward_pendingindex helps identify these for manual recovery or automated cleanup.Out of Scope
Breaking Changes
No. The
reward_pendingfield defaults tofalseand is backward compatible.Risks and Rollback
Risks:
Rollback:
ALTER TABLE quiz_submissions DROP COLUMN reward_pending;Checklist
Self-Review
Testing
CI / Pipeline
Documentation