fix(lastleg): read committed order command replays after lock waits

This commit is contained in:
2026-10-03 12:13:34 +08:00
parent 34cac9c42a
commit eadb431f33
6 changed files with 100 additions and 8 deletions
@@ -372,7 +372,7 @@ export async function verifyReceivedOrderHold(context) {
&& originalBalances.every(row=>id(row.customerId)===customerId && id(row.overseasWarehouseId)===warehouseId)
&& originalPositions.every(row=>id(row.customerId)===customerId && id(row.overseasWarehouseId)===warehouseId))
const proof = owned.holdInventoryProof = { marker:context.marker, orderId, customerId, warehouseId,
balanceIds, positionIds, snapshots:{}, status:'RUNNING', restored:false }
balanceIds, positionIds, snapshots:{}, concurrentCommands:[], status:'RUNNING', restored:false }
const orderPath = query(`${ORDERS}/${encodeURIComponent(orderId)}/hold`, {customerId})
const inventoryPath = '/api/platform/v1/last-leg/inventory'
let uncertain = false, otherApplied = false, active = null, baseline, otherHeld, otherBody
@@ -419,14 +419,24 @@ export async function verifyReceivedOrderHold(context) {
}
const stock = value => ({balances:value.balances.map(holdStableStock),positions:value.positions.map(holdStableStock)})
const expectStock = (actual,expected,label) => require(label,sameHoldFacts(stock(actual),stock(expected)))
const orderCommand = async (held,version,suffix) => {
const orderCommand = async (held,version,suffix,copies=1) => {
const body={held,chargeable:held,reason:`${context.marker} ${suffix}`,expectedVersion:Number(version)}
const headers={'Idempotency-Key':`${attemptKey}-${suffix}`}
const data=await request('PUT',held?'暂扣本轮单票尾程订单':'解除本轮单票尾程暂扣',orderPath,body,headers)
// Settle both writes before inspecting or recovering: one rejected promise does not
// mean its sibling has stopped modifying the order.
const outcomes=await Promise.allSettled(Array.from({length:copies},()=>
request('PUT',held?'暂扣本轮单票尾程订单':'解除本轮单票尾程暂扣',orderPath,body,headers)))
const failed=outcomes.find(result=>result.status==='rejected')
if(failed) { if(copies>1) uncertain=true;throw failed.reason }
const replies=outcomes.map(result=>result.value),data=replies[0]
// A malformed successful response is not sufficient authority for a recovery mutation.
if(id(data?.orderId)!==orderId || data.held!==held || Number(data.version)!==Number(version)+1) uncertain=true
require('暂扣命令返回本订单预期版本及计费状态',!uncertain && data.chargeable===held && (held || data.reason==null))
active=held?{version:Number(data.version),reason:body.reason}:null
if(copies>1) {
proof.concurrentCommands.push({held,copies,expectedVersion:Number(version),replies})
require('并发同键命令返回相同订单状态和唯一新版本',replies.every(reply=>sameHoldFacts(reply,data)))
}
return {body,headers,data}
}
const releaseOther = async () => {
@@ -461,7 +471,7 @@ export async function verifyReceivedOrderHold(context) {
const allHeld=shiftHoldStock(otherHeld,null,true)
for(let cycle=1;cycle<=2;cycle++) {
const before=await read(`C${cycle}_BEFORE`)
const held=await orderCommand(true,before.order.version,`F32-C${cycle}-HOLD`)
const held=await orderCommand(true,before.order.version,`F32-C${cycle}-HOLD`,cycle===1?2:1)
const during=await read(`C${cycle}_HELD`)
require('暂扣只推进单票版本并保留计费原因',during.order.detained===true && Number(during.order.version)===Number(held.data.version)
&& during.order.holdReason===held.body.reason && during.order.heldChargeable===true)
@@ -473,7 +483,7 @@ export async function verifyReceivedOrderHold(context) {
await reject('暂扣尾程订单不能生成人工计划',`${PLANS}/manual`,manualPlanBody(representative,held.data.version,context.marker),
{'Idempotency-Key':`${attemptKey}-F32-C${cycle}-PLAN`},'POST')
require('人工计划拒绝后暂扣与库存不变',sameHoldFacts(await read(`C${cycle}_PLAN_REJECTED`),during))
const released=await orderCommand(false,during.order.version,`F32-C${cycle}-RELEASE`)
const released=await orderCommand(false,during.order.version,`F32-C${cycle}-RELEASE`,cycle===1?2:1)
const after=await read(`C${cycle}_RELEASED`)
require('解除暂扣清空计费与原因并推进版本',after.order.detained===false && after.order.heldChargeable===false
&& after.order.holdReason==null && Number(after.order.version)===Number(released.data.version))