Skip to content

fix(checkout-api): resolve checkout-race-condition#21

Open
Spkap wants to merge 1 commit intomainfrom
replayx/incident-checkout-race-001-mo66329d
Open

fix(checkout-api): resolve checkout-race-condition#21
Spkap wants to merge 1 commit intomainfrom
replayx/incident-checkout-race-001-mo66329d

Conversation

@Spkap
Copy link
Copy Markdown
Collaborator

@Spkap Spkap commented Apr 19, 2026

Summary

Resolve checkout-race-condition for checkout-api with a validated ReplayX patch candidate.

Changed Files

  • demo_app/src/inventory/reserve-stock.ts
  • demo_app/src/checkout/submit-order.ts

Validation

  • Concurrent checkout repro: exit 0
  • Sequential checkout sanity: exit 0

Rollback

Revert the live inventory guard and concurrent checkout settlement handling.

Copilot AI review requested due to automatic review settings April 19, 2026 19:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses the demo app’s checkout-api concurrent checkout race by preventing overselling during reserveStock, and by adjusting the concurrent checkout scenario to treat expected OutOfStock outcomes as non-fatal.

Changes:

  • Add a post-delay “live inventory” availability guard before committing inventory updates.
  • Return reservation metadata based on the committed inventory record version rather than the pre-delay snapshot.
  • In concurrent mode, switch from Promise.all to Promise.allSettled and ignore expected OutOfStock: failures while still surfacing unexpected failures.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
demo_app/src/inventory/reserve-stock.ts Adds a post-delay availability check and returns committed (record) snapshot version/token to avoid stale snapshot metadata.
demo_app/src/checkout/submit-order.ts Uses allSettled to allow concurrent scenario to succeed even when one request fails with OutOfStock, while rethrowing unexpected failures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 18 to +23
// Intentional bug: this delay makes the pre-check stale under concurrent checkout attempts.
await delay(25);

const record = getInventoryRecord(sku);

if (record.available < quantity) {
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

The inline comment above the artificial delay now claims this is an “Intentional bug”, but the newly added post-delay record.available check changes the behavior to prevent the stale pre-check from overselling. Please update the comment to reflect the current intent (e.g., that the delay simulates a stale pre-check and the post-delay guard is the actual fix), otherwise future readers may reintroduce the race when “fixing” the comment/code.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +23
const hardFailure = settled.find(
(result) =>
result.status === "rejected" &&
!(result.reason instanceof Error && result.reason.message.startsWith("OutOfStock:"))
);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

The OutOfStock detection here relies on parsing Error.message with startsWith("OutOfStock:"). This is brittle (message wording changes, wrapping, localization) and couples submit-order to reserve-stock’s string format. Prefer a typed error (e.g., class OutOfStockError extends Error { sku }) or an error code property so callers can branch reliably without string matching.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +29
const settled = await Promise.allSettled(requests.map((request) => processCheckoutWorker(request)));

const hardFailure = settled.find(
(result) =>
result.status === "rejected" &&
!(result.reason instanceof Error && result.reason.message.startsWith("OutOfStock:"))
);

if (hardFailure && hardFailure.status === "rejected") {
throw hardFailure.reason;
}

return settled.flatMap((result) => (result.status === "fulfilled" ? [result.value] : []));
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

runCheckoutRaceScenario now returns only fulfilled results in concurrent mode, which can make the returned array shorter than the requests array. Since this function is exported and used by the server/script, consider returning a structure that preserves per-request outcomes (e.g., { successes, failures } or an array of discriminated union results) so callers can distinguish “one succeeded, one OutOfStock” from other scenarios instead of silently dropping failures.

Copilot uses AI. Check for mistakes.
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.

2 participants