Skip to content

test: load-test concurrent intent accept for race conditions - #162

Open
Paranoa-dev wants to merge 1 commit into
stellar-vortex-protocol:mainfrom
Paranoa-dev:test/concurrent-accept-race
Open

test: load-test concurrent intent accept for race conditions#162
Paranoa-dev wants to merge 1 commit into
stellar-vortex-protocol:mainfrom
Paranoa-dev:test/concurrent-accept-race

Conversation

@Paranoa-dev

Copy link
Copy Markdown

Load-test the persistence layer for concurrent accept/fill races

Summary

This PR adds a race-condition load test for concurrent accept() and fill() calls on the same intent, and fixes the underlying non-atomic read-check-update pattern in IntentsController by introducing atomic conditional-update methods in IntentsService.

Problem

The current IntentsController.accept() and IntentsController.fill() follow a read → check → update pattern:

const intent = this.intentsService.get(id);          // Read
if (intent.state !== "open") throw new Conflict();   // Check
this.intentsService.update(id, { state: "accepted" }); // Write

This is safe in a single-threaded Node.js process with no await between steps, but once state lives in a shared database across multiple backend instances (horizontal scaling), this pattern breaks — two instances could both read "open" state and both succeed in accepting the same intent.

Solution

1. Atomic conditional-update methods (src/intents/intents.service.ts)

Added acceptIfOpen() and fillIfAccepted() — synchronous, atomic check-and-update methods that mirror the DB pattern:

-- acceptIfOpen
UPDATE intents SET state='accepted', solver=$1, deadline=now+300
WHERE id=$2 AND state='open'
RETURNING *

-- fillIfAccepted
UPDATE intents SET state='filled', fillAmount=$1, txHash=$2, filledAt=$3
WHERE id=$4 AND state='accepted' AND solver=$5
RETURNING *

Both return null when the preconditions are not met (intent not found, wrong state, wrong solver).

2. Controller refactored (src/intents/intents.controller.ts)

Updated accept() and fill() to use the atomic methods instead of the separate get → check → update pattern. Error handling now checks the return value of the atomic method.

3. Load test (test/load/concurrent-accept.test.ts)

  • HTTP-level tests: Fires 20 concurrent accept() (or fill()) requests at the same intent via supertest, asserts exactly one succeeds (201) and the rest fail (409 Conflict)
  • Unit-level tests: Calls acceptIfOpen() / fillIfAccepted() 50 times synchronously on the same intent, asserts exactly one winner
  • Multi-intent test: Races different solvers for different intents concurrently, verifies all resolve correctly

4. E2e config fixes (test/jest-e2e.json)

Fixed pre-existing issues that prevented e2e tests from running:

  • Disabled ts-jest diagnostics for missing type declarations (@types/joi, @stellar/stellar-sdk)
  • Added moduleNameMapper mock for @stellar/stellar-sdk (ESM-only package incompatible with Jest's CJS resolver)
  • Extended test regex to include load test files

Test Results

Unit tests:  26 passed (3 suites)
E2e tests:   23 passed (5 suites)
Load tests:   5 passed (1 suite)
Total:       54 tests passing

Migration Path

When moving to a real database, replace acceptIfOpen() and fillIfAccepted() with parameterized SQL using WHERE state = '...' clauses. The in-memory implementation already guarantees atomicity and serves as the reference contract.

Checklist

  • Load test runs concurrent accept() calls for the same intent
  • Asserts only one wins
  • DB-level conditional update pattern (atomic methods)
  • Unit tests for atomic methods
  • npm run lint passes
  • npm run typecheck passes
  • npm test passes
  • npm run test:e2e passes

Closes #63

- Add atomic acceptIfOpen() and fillIfAccepted() methods to IntentsService
  that mirror DB-level conditional updates (UPDATE ... WHERE state='open')
- Refactor IntentsController to use atomic methods, eliminating the
  non-atomic read-check-update pattern in accept() and fill()
- Add concurrent HTTP load tests (20 parallel requests) asserting only
  one accept/fill succeeds per intent
- Add unit tests for atomic service methods with simulated concurrency
- Fix e2e test config: disable diagnostics, add stellar-sdk mock,
  include load test files in test runner

Closes stellar-vortex-protocol#63
@drips-wave

drips-wave Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Paranoa-dev 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.

Load-test the persistence layer for concurrent accept/fill races

1 participant