From 090f57bfb1dde67f38855c7810904ce3451b1ec3 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 5 Oct 2026 16:05:06 +0800 Subject: [PATCH] fix(firstleg): stop retrying deterministic completion preflight conflicts --- .../service/LoadingCompletionWorker.java | 9 +++- .../service/LoadingCompletionWorkerTest.java | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletionWorker.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletionWorker.java index 39659b1ef..c03263fc5 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletionWorker.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletionWorker.java @@ -1,5 +1,6 @@ package com.baiyue.firstleg.service; +import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.entity.LoadingCompletionCommandEntity; import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository; @@ -63,7 +64,13 @@ public class LoadingCompletionWorker { } private static boolean isDeterministicLocalBusinessFailure(String stage, Throwable cause) { - return cause instanceof BizException && java.util.Set.of( + if (!(cause instanceof BizException business)) { + return false; + } + if ("COMPLETED_EVENT_READY".equals(stage)) { + return business.getResultCode() == ResultCode.CONFLICT; + } + return java.util.Set.of( "OWNER_DISCOVERY", "OWNER_READY", "PLAN_GATE", "TASK_REVIEW", "PLAN_COMPLETE", "COMPLETED_EVENT").contains(stage); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletionWorkerTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletionWorkerTest.java index 519c17fc1..264f0d2c2 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletionWorkerTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletionWorkerTest.java @@ -81,6 +81,57 @@ class LoadingCompletionWorkerTest { verify(picks).failCompletion(20L); } + @Test + void completedEventPreflightBusinessFailureIsTerminalOnFirstAttempt() { + LoadingCompletionCommandEntity command = command(0); + when(commands.due(10)).thenReturn(List.of(command)); + when(commands.claim(eq(900L), any(String.class), any(LocalDateTime.class))).thenReturn(true); + doThrow(new LoadingPickService.CompletionProcessingException("COMPLETED_EVENT_READY", + new BizException(ResultCode.CONFLICT, "FBA warehouse internal code is missing"))) + .when(loadingPicks).processCompletion(eq(command), any(String.class)); + when(commands.fail(eq(900L), any(String.class), eq("FAILED"), any(LocalDateTime.class), + eq("COMPLETED_EVENT_READY"), eq(ResultCode.CONFLICT.code()), eq(null), eq(null))).thenReturn(true); + + worker.dispatch(); + + verify(commands).fail(eq(900L), any(String.class), eq("FAILED"), any(LocalDateTime.class), + eq("COMPLETED_EVENT_READY"), eq(ResultCode.CONFLICT.code()), eq(null), eq(null)); + verify(picks).failCompletion(20L); + } + + @Test + void completedEventPreflightUnexpectedFailureRemainsRetryable() { + LoadingCompletionCommandEntity command = command(0); + when(commands.due(10)).thenReturn(List.of(command)); + when(commands.claim(eq(900L), any(String.class), any(LocalDateTime.class))).thenReturn(true); + doThrow(new LoadingPickService.CompletionProcessingException("COMPLETED_EVENT_READY", + new IllegalStateException("temporary database failure"))) + .when(loadingPicks).processCompletion(eq(command), any(String.class)); + + worker.dispatch(); + + verify(commands).fail(eq(900L), any(String.class), eq("RETRYABLE"), any(LocalDateTime.class), + eq("COMPLETED_EVENT_READY"), eq("S0500"), eq(null), eq(null)); + verify(picks, org.mockito.Mockito.never()).failCompletion(org.mockito.ArgumentMatchers.anyLong()); + } + + @Test + void completedEventPreflightWarehouseUnavailableBusinessFailureRemainsRetryable() { + LoadingCompletionCommandEntity command = command(0); + when(commands.due(10)).thenReturn(List.of(command)); + when(commands.claim(eq(900L), any(String.class), any(LocalDateTime.class))).thenReturn(true); + // LoadingPalletLabelService wraps warehouse Feign/circuit-breaker failures in this BizException. + doThrow(new LoadingPickService.CompletionProcessingException("COMPLETED_EVENT_READY", + new BizException(ResultCode.SERVICE_UNAVAILABLE, "warehouse master data temporarily unavailable"))) + .when(loadingPicks).processCompletion(eq(command), any(String.class)); + + worker.dispatch(); + + verify(commands).fail(eq(900L), any(String.class), eq("RETRYABLE"), any(LocalDateTime.class), + eq("COMPLETED_EVENT_READY"), eq(ResultCode.SERVICE_UNAVAILABLE.code()), eq(null), eq(null)); + verify(picks, org.mockito.Mockito.never()).failCompletion(org.mockito.ArgumentMatchers.anyLong()); + } + private static LoadingCompletionCommandEntity command(int attemptCount) { LoadingCompletionCommandEntity command = new LoadingCompletionCommandEntity(); command.setId(900L);