Skip to content

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

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

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

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 18:53
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

Fixes the demo app’s checkout-api oversell race by validating inventory availability at commit time and by treating expected OutOfStock failures as non-fatal in the concurrent repro path.

Changes:

  • Add a post-delay “commit-time” inventory availability guard in reserveStock to prevent oversell under concurrency.
  • Return reservation metadata (token/version) from the committed inventory record rather than the pre-check snapshot.
  • Update the concurrent scenario runner to use Promise.allSettled, ignore expected OutOfStock rejections, and return only successful checkout results.

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 commit-time stock validation and returns committed snapshot/version metadata.
demo_app/src/checkout/submit-order.ts Makes concurrent repro tolerant of OutOfStock by using allSettled and filtering expected failures.

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

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

const record = getInventoryRecord(sku);

if (record.available < quantity) {
throw new Error(`OutOfStock: ${sku}`);
}
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 about the delay being an “Intentional bug” is now misleading/outdated. With the new post-delay record.available guard, the stale pre-check no longer causes oversell on its own, so the comment should be updated to describe the delay as a concurrency simulation while commit-time validation prevents stale snapshot writes.

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.

This logic treats stock exhaustion as a “soft” failure by checking Error.message.startsWith("OutOfStock:"). Keying behavior off an error message prefix is brittle (message changes, localization, non-Error throws) and can misclassify failures. Prefer throwing a dedicated OutOfStockError (or an Error with a stable code property) from reserveStock and checking that here.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +26
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;
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.

With this change, the concurrent scenario no longer throws when one worker hits OutOfStock, so demo_app/scripts/repro-checkout-race.ts will exit 0 for the concurrent repro. The incident fixture incidents/checkout-race-condition.json still marks that command as the failing repro with expectedExitCode: 1, and the orchestrator uses that expectation to determine whether the repro is confirmed. If the intent is to fix the incident in-place, update the incident command expectations (or swap which command is considered failing vs healthy) to keep the pipeline/demo consistent.

Suggested change
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;
const rejectedResult = settled.find((result) => result.status === "rejected");
if (rejectedResult && rejectedResult.status === "rejected") {
throw rejectedResult.reason;

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