Skip to content

fix: move Stellar network calls outside database transactions#86

Merged
DeFiVC merged 1 commit into
ChainLearnOfficial:mainfrom
PrimeFactor-Dev:fix/stellar-transaction-connection-pool
Jul 24, 2026
Merged

fix: move Stellar network calls outside database transactions#86
DeFiVC merged 1 commit into
ChainLearnOfficial:mainfrom
PrimeFactor-Dev:fix/stellar-transaction-connection-pool

Conversation

@PrimeFactor-Dev

Copy link
Copy Markdown
Contributor

Closes #75

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Summary

Move Stellar network calls (invokeContract) outside database transactions to prevent holding connections for 30+ seconds. The claimReward and credentialService.mint functions were executing Stellar RPC calls, simulation, and Horizon submission inside db.transaction, which could exhaust the 10-connection pool under concurrent load.

Motivation / Context

Both claimReward and credentialService.mint call invokeContract inside a db.transaction. The Stellar calls have WRITE_TIMEOUT_MS = 30_000 with 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)

  • Validate submission, quiz, and user data
  • Check eligibility (passing score, no existing claim)
  • Set reward_pending = true to prevent concurrent claims
  • Release database connection

Phase 2: Execute Stellar transaction (no DB connection held)

  • Call invokeContract for the Stellar transaction
  • Handle errors (circuit breaker, bad_seq, etc.)
  • Database connection is not held during this potentially slow operation

Phase 3: Update result (quick DB transaction)

  • Mark claim as completed or failed
  • Update user credits
  • Release database connection

Added new reward_pending boolean field to quiz_submissions table 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

  • Unit tests: Updated process-reward-claim.test.ts and concurrent-safety.test.ts to verify two-phase behavior
  • TypeScript compilation: Passes with no errors
  • Lint: Passes (only pre-existing warnings)
  • All 11 concurrent safety tests pass
  • All 6 process-reward-claim tests pass

Tradeoffs

  • Added complexity: Two-phase commit requires tracking pending state
  • Consistency: The Redis lock (withLock) still prevents concurrent claims on the same submission, maintaining consistency
  • Edge case: If process crashes between Phase 2 and Phase 3, the claim will be in pending state. The reward_pending index helps identify these for manual recovery or automated cleanup.

Out of Scope

  • Automated recovery for claims stuck in pending state (could be a follow-up)
  • Pool size configuration changes (the fix addresses the root cause)

Breaking Changes

No. The reward_pending field defaults to false and is backward compatible.

Risks and Rollback

Risks:

  • Claims could theoretically be left in pending state if the process crashes between Stellar execution and DB update
  • The Redis lock TTL (30s) may need adjustment if Stellar calls consistently take longer

Rollback:

  1. Revert the migration: ALTER TABLE quiz_submissions DROP COLUMN reward_pending;
  2. Revert code changes

Checklist

Self-Review

  • I have read the entire diff line by line as if a stranger wrote it
  • No debug code remains
  • No hardcoded secrets, tokens, API keys, or internal URLs
  • Naming is consistent with the existing codebase
  • Error handling is present and produces meaningful messages
  • Edge cases are addressed (pending state, error handling)

Testing

  • All existing tests pass locally
  • New tests added for new logic (reward_pending checks)
  • Edge cases and failure paths are tested

CI / Pipeline

  • TypeScript compilation passes
  • Lint passes (no new errors)

Documentation

  • JSDoc comments updated to explain two-phase approach

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 DeFiVC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

  1. 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.

  2. Minor observation: In processRewardClaim, the error handler inside Phase 2 catches general errors and marks rewardFailed = 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.

@DeFiVC
DeFiVC merged commit 3837eb4 into ChainLearnOfficial:main Jul 24, 2026
2 checks passed
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.

Stellar work inside database transaction holds connections for 30+ seconds

2 participants