fix(firstleg): atomically finalize PDA completion

This commit is contained in:
2026-09-01 11:11:43 +08:00
parent de0eddeee8
commit b5bc77b153
10 changed files with 181 additions and 46 deletions
@@ -8,6 +8,7 @@ const loadingPickService = read('../../firstleg/src/main/java/com/baiyue/firstle
const completionCommand = read('../../firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingCompletionCommandEntity.java')
const completionWorker = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletionWorker.java')
const loadingPickTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java')
const completionWorkerTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletionWorkerTest.java')
const settlementTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java')
const unifiedSql = read('../../deploy/sql/头程数据库统一增量-MySQL8.sql')
const finalizeHttp = read('phase3-loading-finalize.mjs')
@@ -15,8 +16,6 @@ const pdaPage = read('../../../apps/pda-uniapp/pages/picking/picking.vue')
const pdaCompletionState = read('../../../apps/pda-uniapp/utils/loading-completion-state.js')
const pdaCompletionTests = read('../../../apps/pda-uniapp/tests/loading-completion-state.test.mjs')
const RECOVERY_TASK_ID = '2092817774453780481'
function retryDecision({ operationStatus, sameRequest, finalTaskStatus }) {
if (!sameRequest) return 'CONFLICT'
if (operationStatus === 'PROCESSING') return 'PROCESSING'
@@ -74,7 +73,8 @@ test('durable worker processes inbound settlements incrementally with retry back
assert.match(completionWorker, /15L <<|nextRetryAt|retryAt/)
assert.match(completionWorker, /"FAILED"\s*:\s*"RETRYABLE"/)
assert.match(loadingPickService, /checkpointAtLeast[\s\S]{0,1800}SETTLED/)
assert.match(loadingPickTests, /(?:transient|瞬时)[\s\S]{0,1500}(?:backoff|retry|退避)/i)
assert.match(completionWorkerTests, /transientFailureIsReleasedForBackoffRetry/)
assert.match(completionWorkerTests, /"RETRYABLE"/)
assert.match(loadingPickTests, /FAILED[\s\S]{0,1500}(?:same operationId|同operationId|resume|恢复)/i)
})
@@ -89,27 +89,30 @@ test('a different operationId is rejected while the completion command is proces
})
test('fifteen completed Warehouse settlements can be reconciled after Firstleg rollback', () => {
assert.match(settlementTests, /15|fifteen/i)
assert.match(settlementTests, /(?:partial success|部分成功|local rollback|本地回滚)/i)
assert.match(settlementTests, /(?:settlementTarget|COMPLETED|replaySettlement)/)
assert.match(loadingPickTests, /(?:resume|recover|reconcile|恢复|核平)[\s\S]{0,2000}COMPLETE_LOADING/i)
assert.match(loadingPickTests, /workerProcessesFifteenInboundOwnersInStableOrderAndFinalizesOnce/)
assert.match(loadingPickTests, /IntStream\.range\(0, 15\)/)
assert.match(settlementTests, /reusesWarehouseSettlementTargetWhenRetryingAfterLocalRollback/)
assert.match(settlementTests, /warehouse\.settlementTarget\("internal-token", 31L, "PDA-SETTLE-1-31"\)/)
assert.match(settlementTests, /targetBusinessId\(\) == 999L/)
assert.match(loadingPickTests, /workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements/)
})
test('recovery fixture preserves the observed fifteen-settlement partial success', () => {
assert.equal(RECOVERY_TASK_ID, '2092817774453780481')
assert.match(finalizeHttp, new RegExp(RECOVERY_TASK_ID))
assert.match(finalizeHttp, /15[\s\S]{0,600}(?:Warehouse|settlement)/i)
assert.match(finalizeHttp, /(?:LOADING|v1)[\s\S]{0,600}(?:恢复|recovery|resume)/i)
test('recovery contract is fixture-independent and uses durable checkpoints', () => {
assert.doesNotMatch(finalizeHttp, /2092817774453780481/)
assert.match(loadingPickService, /checkpointAtLeast\(checkpoint, "SETTLED"\)/)
assert.match(loadingPickService, /checkpointAtLeast\(checkpoint, "INBOUND_COMPLETED"\)/)
assert.match(loadingPickTests, /workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements/)
})
test('final acceptance reconciles task allocation reservation occupancy and one completion audit', () => {
for (const fact of ['task', 'allocation', 'reservation', 'occupancy']) {
test('final acceptance reconciles terminal task and one completion audit after owner checkpoints', () => {
for (const fact of ['task', 'plan', 'inbound', 'operation']) {
assert.match(finalizeHttp, new RegExp(fact, 'i'), `missing final ${fact} reconciliation`)
}
assert.match(finalizeHttp, /COMPLETE_LOADING/)
assert.match(finalizeHttp, /(?:length === 1|恰有一次|只写一条)/)
assert.match(finalizeHttp, /(?:PROCESSING|处理中)/)
assert.match(finalizeHttp, /(?:elapsed|latency|耗时|毫秒|ms)/i)
assert.match(finalizeHttp, /concurrentCompleteOperations\.length === 1/)
assert.match(finalizeHttp, /异步完成命令收敛到终态/)
assert.match(loadingPickService, /completionTransaction\.executeWithoutResult/)
assert.match(loadingPickService, /private void finalizeCompletion/)
})
test('PDA treats accepted completion as processing, returns to list, and never claims completion early', () => {