Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses the checkout-race-condition incident in the checkout-api demo app by preventing overselling during concurrent checkouts and by making the concurrent repro scenario handle expected out-of-stock outcomes without failing the whole run.
Changes:
- Add a post-delay (commit-time) inventory availability guard in
reserveStockand ensure reservation metadata reflects the committed snapshot version. - Update concurrent checkout scenario execution to use
Promise.allSettled, treatingOutOfStockas an expected rejection 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 commit-time availability check to prevent negative inventory and aligns returned reservation version/token with the committed record. |
| demo_app/src/checkout/submit-order.ts | Switches concurrent execution to allSettled, filters expected OutOfStock failures, and rethrows unexpected errors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 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}`); |
There was a problem hiding this comment.
The inline comment says the delay is an "Intentional bug" that makes the pre-check stale, but the new post-delay record.available guard means the function is no longer intentionally buggy in that way. Please update/remove the comment so it reflects the current behavior (e.g., explain why the delay exists in the repro without implying the implementation is still incorrect).
| const hardFailure = settled.find( | ||
| (result) => | ||
| result.status === "rejected" && | ||
| !(result.reason instanceof Error && result.reason.message.startsWith("OutOfStock:")) | ||
| ); |
There was a problem hiding this comment.
OutOfStock handling is currently based on parsing Error.message.startsWith("OutOfStock:"), which is brittle and can easily break if the message format changes (or gets localized). Prefer a dedicated error type (e.g., class OutOfStockError extends Error { sku }) or a structured discriminator (like error.code === "OUT_OF_STOCK") and check that instead.
| 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] : [])); |
There was a problem hiding this comment.
Switching from Promise.all to Promise.allSettled and swallowing OutOfStock rejections changes the concurrent repro from exit code 1 to exit code 0 (via demo_app/scripts/repro-checkout-race.ts). If any automation (e.g., the incident spec's "failing" command expectation) relies on the concurrent repro failing to confirm the incident, it will now report a mismatch—please make sure the corresponding repro expectations/metadata are updated to match the new behavior.
| 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] : [])); | |
| return Promise.all(requests.map((request) => processCheckoutWorker(request))); |
Summary
Resolve
checkout-race-conditionforcheckout-apiwith a validated ReplayX patch candidate.Changed Files
Validation
Rollback
Revert the live inventory guard and concurrent checkout settlement handling.