From bb3b93a79f47d4dba95e7d21f6aae5846a61c621 Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Wed, 12 Aug 2026 09:19:59 +0800 Subject: [PATCH] fix: keep shadow work off legacy threads --- .../shadow/InboundShadowCompletion.java | 49 +++++++ .../shadow/OutboundShadowCompletion.java | 46 ++++++ .../shadow/ShadowObservationGateway.java | 135 ++++++++++-------- .../api/service/impl/ToWmsServiceImpl.java | 23 +-- .../service/impl/OutStockoutServiceImpl.java | 39 ++--- .../shadow/ShadowObservationGatewayTest.java | 90 +++++++----- .../impl/ToWmsServiceImplShadowTest.java | 22 +-- .../OutStockoutServiceImplShadowTest.java | 39 ++--- 8 files changed, 274 insertions(+), 169 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/InboundShadowCompletion.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/OutboundShadowCompletion.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/InboundShadowCompletion.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/InboundShadowCompletion.java new file mode 100644 index 0000000..7e9b805 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/InboundShadowCompletion.java @@ -0,0 +1,49 @@ +package com.ruoyi.algorithm.shadow; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public final class InboundShadowCompletion { + private static final InboundShadowCompletion REJECTED = + new InboundShadowCompletion(false, 0L); + + private final boolean accepted; + private final long deadlineNanos; + private final CompletableFuture legacyResult = new CompletableFuture<>(); + + static InboundShadowCompletion accepted(int timeoutSeconds) { + return new InboundShadowCompletion( + true, System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds)); + } + + public static InboundShadowCompletion rejected() { + return REJECTED; + } + + private InboundShadowCompletion(boolean accepted, long deadlineNanos) { + this.accepted = accepted; + this.deadlineNanos = deadlineNanos; + } + + public boolean accepted() { + return accepted; + } + + public void complete(String resultCode, String spaceId) { + if (accepted) { + legacyResult.complete(new LegacyResult(resultCode, spaceId)); + } + } + + LegacyResult awaitLegacyResult() throws Exception { + long remainingNanos = deadlineNanos - System.nanoTime(); + if (remainingNanos <= 0) { + throw new TimeoutException("Shadow inbound observation deadline expired"); + } + return legacyResult.get(remainingNanos, TimeUnit.NANOSECONDS); + } + + record LegacyResult(String resultCode, String spaceId) { + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/OutboundShadowCompletion.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/OutboundShadowCompletion.java new file mode 100644 index 0000000..0953232 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/OutboundShadowCompletion.java @@ -0,0 +1,46 @@ +package com.ruoyi.algorithm.shadow; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public final class OutboundShadowCompletion { + private static final OutboundShadowCompletion REJECTED = + new OutboundShadowCompletion(false, 0L); + + private final boolean accepted; + private final long deadlineNanos; + private final CompletableFuture legacyResult = new CompletableFuture<>(); + + static OutboundShadowCompletion accepted(int timeoutSeconds) { + return new OutboundShadowCompletion( + true, System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds)); + } + + public static OutboundShadowCompletion rejected() { + return REJECTED; + } + + private OutboundShadowCompletion(boolean accepted, long deadlineNanos) { + this.accepted = accepted; + this.deadlineNanos = deadlineNanos; + } + + public boolean accepted() { + return accepted; + } + + public void complete(String resultCode) { + if (accepted) { + legacyResult.complete(resultCode); + } + } + + String awaitLegacyResult() throws Exception { + long remainingNanos = deadlineNanos - System.nanoTime(); + if (remainingNanos <= 0) { + throw new TimeoutException("Shadow outbound observation deadline expired"); + } + return legacyResult.get(remainingNanos, TimeUnit.NANOSECONDS); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationGateway.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationGateway.java index f26207d..70c6449 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationGateway.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationGateway.java @@ -1,11 +1,15 @@ package com.ruoyi.algorithm.shadow; +import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; @Component public class ShadowObservationGateway { @@ -13,88 +17,95 @@ public class ShadowObservationGateway { private final ShadowObservationProperties properties; private final ShadowPlanningEngine engine; - private final AtomicBoolean planning = new AtomicBoolean(); + private final ThreadPoolExecutor executor; public ShadowObservationGateway(ShadowObservationProperties properties, ShadowPlanningEngine engine) { this.properties = Objects.requireNonNull(properties, "properties"); this.engine = engine; + this.executor = new ThreadPoolExecutor( + 1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>(), runnable -> { + Thread thread = new Thread(runnable, "ft546-shadow-observer"); + thread.setDaemon(true); + return thread; + }, new ThreadPoolExecutor.AbortPolicy()); } - public InboundShadowObservation planInbound(String taskNo, String trayNo) { - if (!properties.isEnabled() || engine == null) { - return InboundShadowObservation.empty(); - } - if (!planning.compareAndSet(false, true)) { - return InboundShadowObservation.empty(); + public InboundShadowCompletion observeInbound(String taskNo, String trayNo) { + if (!available()) { + return InboundShadowCompletion.rejected(); } + InboundShadowCompletion completion = + InboundShadowCompletion.accepted(properties.getTimeoutSeconds()); try { - return engine.planInbound(taskNo, trayNo); - } catch (RuntimeException exception) { - LOG.warn("Shadow inbound planning failed open for task {}: {}", taskNo, exception.getMessage()); - return InboundShadowObservation.empty(); - } finally { - planning.set(false); + executor.execute(() -> observeInbound(taskNo, trayNo, completion)); + return completion; + } catch (RejectedExecutionException exception) { + LOG.debug("Shadow inbound observation skipped because worker is busy for task {}", taskNo); + return InboundShadowCompletion.rejected(); } } - public OutboundShadowObservation planProjectOutbound(String stockoutDetailId) { - if (!properties.isEnabled() || engine == null) { - return OutboundShadowObservation.empty(); - } - if (!planning.compareAndSet(false, true)) { - return OutboundShadowObservation.empty(); - } + public OutboundShadowCompletion observeProjectOutbound(String stockoutDetailId) { + return observeOutbound(stockoutDetailId, + () -> engine.planProjectOutbound(stockoutDetailId)); + } + + public OutboundShadowCompletion observeExactStockOutbound(String stockoutDetailId, String stockId) { + return observeOutbound(stockoutDetailId, + () -> engine.planExactStockOutbound(stockoutDetailId, stockId)); + } + + private void observeInbound(String taskNo, String trayNo, InboundShadowCompletion completion) { try { - return engine.planProjectOutbound(stockoutDetailId); - } catch (RuntimeException exception) { - LOG.warn("Shadow outbound planning failed open for detail {}: {}", - stockoutDetailId, exception.getMessage()); - return OutboundShadowObservation.empty(); - } finally { - planning.set(false); + InboundShadowObservation observation = engine.planInbound(taskNo, trayNo); + InboundShadowCompletion.LegacyResult legacy = completion.awaitLegacyResult(); + engine.compareInbound(observation, legacy.resultCode(), legacy.spaceId()); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + LOG.warn("Shadow inbound observation interrupted for task {}", taskNo); + } catch (Exception exception) { + LOG.warn("Shadow inbound observation failed open for task {}: {}", taskNo, exception.getMessage()); } } - public OutboundShadowObservation planExactStockOutbound(String stockoutDetailId, String stockId) { - if (!properties.isEnabled() || engine == null) { - return OutboundShadowObservation.empty(); - } - if (!planning.compareAndSet(false, true)) { - return OutboundShadowObservation.empty(); + private OutboundShadowCompletion observeOutbound(String stockoutDetailId, + OutboundPlanner planner) { + if (!available()) { + return OutboundShadowCompletion.rejected(); } + OutboundShadowCompletion completion = + OutboundShadowCompletion.accepted(properties.getTimeoutSeconds()); try { - return engine.planExactStockOutbound(stockoutDetailId, stockId); - } catch (RuntimeException exception) { - LOG.warn("Shadow exact-stock planning failed open for detail {} stock {}: {}", - stockoutDetailId, stockId, exception.getMessage()); - return OutboundShadowObservation.empty(); - } finally { - planning.set(false); + executor.execute(() -> { + try { + OutboundShadowObservation observation = planner.plan(); + engine.compareOutbound(observation, completion.awaitLegacyResult()); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + LOG.warn("Shadow outbound observation interrupted for detail {}", stockoutDetailId); + } catch (Exception exception) { + LOG.warn("Shadow outbound observation failed open for detail {}: {}", + stockoutDetailId, exception.getMessage()); + } + }); + return completion; + } catch (RejectedExecutionException exception) { + LOG.debug("Shadow outbound observation skipped because worker is busy for detail {}", stockoutDetailId); + return OutboundShadowCompletion.rejected(); } } - public void compareInbound(InboundShadowObservation observation, String legacyResultCode, - String legacySpaceId) { - if (!properties.isEnabled() || engine == null || observation == null || !observation.observed()) { - return; - } - try { - engine.compareInbound(observation, legacyResultCode, legacySpaceId); - } catch (RuntimeException exception) { - LOG.warn("Shadow inbound comparison failed open for request {}: {}", - observation.requestId(), exception.getMessage()); - } + private boolean available() { + return properties.isEnabled() && engine != null && !executor.isShutdown(); } - public void compareOutbound(OutboundShadowObservation observation, String legacyResultCode) { - if (!properties.isEnabled() || engine == null || observation == null || !observation.observed()) { - return; - } - try { - engine.compareOutbound(observation, legacyResultCode); - } catch (RuntimeException exception) { - LOG.warn("Shadow outbound comparison failed open for request {}: {}", - observation.requestId(), exception.getMessage()); - } + @PreDestroy + public void shutdown() { + executor.shutdownNow(); + } + + @FunctionalInterface + private interface OutboundPlanner { + OutboundShadowObservation plan(); } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ToWmsServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ToWmsServiceImpl.java index 7038437..da99219 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ToWmsServiceImpl.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ToWmsServiceImpl.java @@ -6,7 +6,7 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; -import com.ruoyi.algorithm.shadow.InboundShadowObservation; +import com.ruoyi.algorithm.shadow.InboundShadowCompletion; import com.ruoyi.algorithm.shadow.ShadowObservationGateway; import com.ruoyi.api.domain.*; import com.ruoyi.api.service.IToErpService; @@ -416,21 +416,21 @@ public class ToWmsServiceImpl implements IToWmsService { map.put("v_tray_code", inJob.getTrayCode()); map.put("v_code", deviceCode); - InboundShadowObservation shadowObservation = InboundShadowObservation.empty(); + InboundShadowCompletion shadowCompletion = InboundShadowCompletion.rejected(); try { - shadowObservation = shadowObservationGateway.planInbound(taskNo, inJob.getTrayCode()); + shadowCompletion = shadowObservationGateway.observeInbound(taskNo, inJob.getTrayCode()); } catch (RuntimeException exception) { - log.warn("Inbound shadow observation failed open for task {}: {}", + log.warn("Inbound shadow submission failed open for task {}: {}", taskNo, exception.getMessage()); } mydoJobMapper.allotInJobSpace(map); String legacyResultCode = map.get("v_result").toString(); if (!"10".equals(legacyResultCode)) { - compareInboundShadow(shadowObservation, legacyResultCode, null); + shadowCompletion.complete(legacyResultCode, null); return AjaxResult.error(map.get("v_resultmsg").toString()); }else{ MydoJob inJob2 = mydoJobMapper.selectInJobByTaskNo(taskNo); - compareInboundShadow(shadowObservation, legacyResultCode, + shadowCompletion.complete(legacyResultCode, inJob2 == null ? null : inJob2.getCargoSpaceId()); return AjaxResult.success(inJob2); } @@ -440,15 +440,4 @@ public class ToWmsServiceImpl implements IToWmsService { } } - private void compareInboundShadow(InboundShadowObservation observation, - String legacyResultCode, String legacySpaceId) { - try { - shadowObservationGateway.compareInbound(observation, legacyResultCode, legacySpaceId); - } catch (RuntimeException exception) { - log.warn("Inbound shadow comparison failed open for request {}: {}", - observation == null ? null : observation.requestId(), exception.getMessage()); - } - } - - } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java index 7ff3613..91c93a9 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java @@ -1,7 +1,7 @@ package com.ruoyi.out.outStockout.service.impl; import cn.hutool.core.util.StrUtil; -import com.ruoyi.algorithm.shadow.OutboundShadowObservation; +import com.ruoyi.algorithm.shadow.OutboundShadowCompletion; import com.ruoyi.algorithm.shadow.ShadowObservationGateway; import com.ruoyi.api.service.IToWcsService; import com.ruoyi.bas.basJobId.service.IBasJobIdService; @@ -188,10 +188,10 @@ public class OutStockoutServiceImpl implements IOutStockoutService map.put("v_stockout_detail_id", detail.getStockoutDetailId()); map.put("v_result", null); map.put("v_resultmsg", null); - OutboundShadowObservation shadowObservation = - planProjectOutboundShadow(detail.getStockoutDetailId()); + OutboundShadowCompletion shadowCompletion = + observeProjectOutboundShadow(detail.getStockoutDetailId()); outStockoutMapper.autoAllot(map); - compareOutboundShadow(shadowObservation, map.get("v_result")); + shadowCompletion.complete(map.get("v_result")); if (!"10".equals(map.get("v_result"))){ stringBuilder.append(",").append(detail.getStockoutDetailId()+map.get("v_resultmsg")); } @@ -218,10 +218,10 @@ public class OutStockoutServiceImpl implements IOutStockoutService for (String stock : stockList) { map.put("v_stockout_detail_id", stockoutDetailId); map.put("v_stock_id", stock); - OutboundShadowObservation shadowObservation = - planExactStockOutboundShadow(stockoutDetailId, stock); + OutboundShadowCompletion shadowCompletion = + observeExactStockOutboundShadow(stockoutDetailId, stock); outStockoutMapper.manualAllot(map); - compareOutboundShadow(shadowObservation, map.get("v_result")); + shadowCompletion.complete(map.get("v_result")); //toWcsService.pushOutMydoJob(); if (!"10".equals(map.get("v_result"))){ stringBuilder.append(",").append(stockoutDetailId+map.get("v_resultString")); @@ -233,32 +233,23 @@ public class OutStockoutServiceImpl implements IOutStockoutService return AjaxResult.success(); } - private OutboundShadowObservation planProjectOutboundShadow(String stockoutDetailId) { + private OutboundShadowCompletion observeProjectOutboundShadow(String stockoutDetailId) { try { - return shadowObservationGateway.planProjectOutbound(stockoutDetailId); + return shadowObservationGateway.observeProjectOutbound(stockoutDetailId); } catch (RuntimeException exception) { - log.warn("Project outbound shadow observation failed open for detail {}: {}", + log.warn("Project outbound shadow submission failed open for detail {}: {}", stockoutDetailId, exception.getMessage()); - return OutboundShadowObservation.empty(); + return OutboundShadowCompletion.rejected(); } } - private OutboundShadowObservation planExactStockOutboundShadow(String stockoutDetailId, String stockId) { + private OutboundShadowCompletion observeExactStockOutboundShadow(String stockoutDetailId, String stockId) { try { - return shadowObservationGateway.planExactStockOutbound(stockoutDetailId, stockId); + return shadowObservationGateway.observeExactStockOutbound(stockoutDetailId, stockId); } catch (RuntimeException exception) { - log.warn("Exact-stock shadow observation failed open for detail {} stock {}: {}", + log.warn("Exact-stock shadow submission failed open for detail {} stock {}: {}", stockoutDetailId, stockId, exception.getMessage()); - return OutboundShadowObservation.empty(); - } - } - - private void compareOutboundShadow(OutboundShadowObservation observation, String legacyResultCode) { - try { - shadowObservationGateway.compareOutbound(observation, legacyResultCode); - } catch (RuntimeException exception) { - log.warn("Outbound shadow comparison failed open for request {}: {}", - observation == null ? null : observation.requestId(), exception.getMessage()); + return OutboundShadowCompletion.rejected(); } } diff --git a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowObservationGatewayTest.java b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowObservationGatewayTest.java index 4aca534..55b5c66 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowObservationGatewayTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowObservationGatewayTest.java @@ -1,5 +1,6 @@ package com.ruoyi.algorithm.shadow; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import java.util.concurrent.CountDownLatch; @@ -11,90 +12,105 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ShadowObservationGatewayTest { + private ShadowObservationGateway gateway; + + @AfterEach + void shutDownGateway() { + if (gateway != null) { + gateway.shutdown(); + } + } @Test void disabledShadowNeverInvokesThePlanningEngine() { ShadowObservationProperties properties = new ShadowObservationProperties(); ShadowPlanningEngine engine = mock(ShadowPlanningEngine.class); - ShadowObservationGateway gateway = new ShadowObservationGateway(properties, engine); + gateway = new ShadowObservationGateway(properties, engine); - InboundShadowObservation observation = gateway.planInbound("TASK-1", "TRAY-1"); + InboundShadowCompletion completion = gateway.observeInbound("TASK-1", "TRAY-1"); + completion.complete("10", "SPACE-1"); - assertThat(observation.observed()).isFalse(); verify(engine, never()).planInbound("TASK-1", "TRAY-1"); } @Test - void planningFailureIsConvertedToAnEmptyObservation() { - ShadowObservationProperties properties = new ShadowObservationProperties(); - properties.setEnabled(true); + void planningFailureIsContainedOnTheBackgroundWorker() { + ShadowObservationProperties properties = enabledProperties(); ShadowPlanningEngine engine = mock(ShadowPlanningEngine.class); when(engine.planInbound("TASK-2", "TRAY-2")) .thenThrow(new IllegalStateException("algorithm objects are unavailable")); - when(engine.planProjectOutbound("DETAIL-2")) - .thenReturn(OutboundShadowObservation.unavailable("W01", "DETAIL-2", "TEST", "completed")); - ShadowObservationGateway gateway = new ShadowObservationGateway(properties, engine); + gateway = new ShadowObservationGateway(properties, engine); - InboundShadowObservation observation = gateway.planInbound("TASK-2", "TRAY-2"); - OutboundShadowObservation followingObservation = gateway.planProjectOutbound("DETAIL-2"); + InboundShadowCompletion completion = gateway.observeInbound("TASK-2", "TRAY-2"); + completion.complete("10", "SPACE-2"); - assertThat(observation.observed()).isFalse(); - assertThat(followingObservation.observed()).isTrue(); + verify(engine, timeout(1000)).planInbound("TASK-2", "TRAY-2"); + verify(engine, never()).compareInbound( + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any()); } @Test void missingEngineFailsOpenForOutboundObservation() { - ShadowObservationProperties properties = new ShadowObservationProperties(); - properties.setEnabled(true); - ShadowObservationGateway gateway = new ShadowObservationGateway(properties, null); + gateway = new ShadowObservationGateway(enabledProperties(), null); - OutboundShadowObservation observation = gateway.planExactStockOutbound("DETAIL-1", "STOCK-1"); + OutboundShadowCompletion completion = + gateway.observeExactStockOutbound("DETAIL-1", "STOCK-1"); + completion.complete("10"); - assertThat(observation.observed()).isFalse(); + assertThat(completion.accepted()).isFalse(); } @Test - void concurrentPlanningIsSkippedInsteadOfAddingDatabaseLoad() throws Exception { - ShadowObservationProperties properties = new ShadowObservationProperties(); - properties.setEnabled(true); + void planningSubmissionNeverWaitsAndConcurrentObservationIsDropped() throws Exception { ShadowPlanningEngine engine = mock(ShadowPlanningEngine.class); CountDownLatch planningStarted = new CountDownLatch(1); CountDownLatch releasePlanning = new CountDownLatch(1); + InboundShadowObservation observation = new InboundShadowObservation( + true, "W01", "TASK-3", "READY", "SPACE-SHADOW", "proposal"); when(engine.planInbound("TASK-3", "TRAY-3")).thenAnswer(invocation -> { planningStarted.countDown(); if (!releasePlanning.await(5, TimeUnit.SECONDS)) { throw new IllegalStateException("test planning was not released"); } - return InboundShadowObservation.unavailable("W01", "TASK-3", "TEST", "completed"); + return observation; }); - when(engine.planProjectOutbound("DETAIL-3")) - .thenReturn(OutboundShadowObservation.unavailable("W01", "DETAIL-3", "TEST", "completed")); - ShadowObservationGateway gateway = new ShadowObservationGateway(properties, engine); - ExecutorService executor = Executors.newSingleThreadExecutor(); + gateway = new ShadowObservationGateway(enabledProperties(), engine); + ExecutorService legacyCaller = Executors.newSingleThreadExecutor(); try { - Future first = - executor.submit(() -> gateway.planInbound("TASK-3", "TRAY-3")); - assertThat(planningStarted.await(5, TimeUnit.SECONDS)).isTrue(); + Future submitted = + legacyCaller.submit(() -> gateway.observeInbound("TASK-3", "TRAY-3")); + InboundShadowCompletion first = submitted.get(1, TimeUnit.SECONDS); + assertThat(first.accepted()).isTrue(); + assertThat(planningStarted.await(1, TimeUnit.SECONDS)).isTrue(); - OutboundShadowObservation concurrent = gateway.planProjectOutbound("DETAIL-3"); + OutboundShadowCompletion concurrent = gateway.observeProjectOutbound("DETAIL-3"); - assertThat(concurrent.observed()).isFalse(); + assertThat(concurrent.accepted()).isFalse(); + concurrent.complete("10"); verify(engine, never()).planProjectOutbound("DETAIL-3"); + + first.complete("10", "SPACE-LEGACY"); releasePlanning.countDown(); - assertThat(first.get(5, TimeUnit.SECONDS).observed()).isTrue(); - OutboundShadowObservation afterRelease = gateway.planProjectOutbound("DETAIL-3"); - - assertThat(afterRelease.observed()).isTrue(); - verify(engine).planProjectOutbound("DETAIL-3"); + verify(engine, timeout(2000)).compareInbound(observation, "10", "SPACE-LEGACY"); } finally { releasePlanning.countDown(); - executor.shutdownNow(); + legacyCaller.shutdownNow(); } } + + private ShadowObservationProperties enabledProperties() { + ShadowObservationProperties properties = new ShadowObservationProperties(); + properties.setEnabled(true); + properties.setTimeoutSeconds(3); + return properties; + } } diff --git a/ruoyi-admin/src/test/java/com/ruoyi/api/service/impl/ToWmsServiceImplShadowTest.java b/ruoyi-admin/src/test/java/com/ruoyi/api/service/impl/ToWmsServiceImplShadowTest.java index a5a46b7..2319601 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/api/service/impl/ToWmsServiceImplShadowTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/api/service/impl/ToWmsServiceImplShadowTest.java @@ -1,6 +1,6 @@ package com.ruoyi.api.service.impl; -import com.ruoyi.algorithm.shadow.InboundShadowObservation; +import com.ruoyi.algorithm.shadow.InboundShadowCompletion; import com.ruoyi.algorithm.shadow.ShadowObservationGateway; import com.ruoyi.api.domain.RequestEnter; import com.ruoyi.common.core.domain.AjaxResult; @@ -30,6 +30,8 @@ class ToWmsServiceImplShadowTest { private MydoJobMapper mydoJobMapper; @Mock private ShadowObservationGateway shadowObservationGateway; + @Mock + private InboundShadowCompletion shadowCompletion; @InjectMocks private ToWmsServiceImpl service; @@ -55,33 +57,31 @@ class ToWmsServiceImplShadowTest { } @Test - void observesBeforeLegacyAndReturnsTheReloadedLegacyJob() { - InboundShadowObservation observation = new InboundShadowObservation( - true, "W01", "JOB-1", "READY", "SPACE-SHADOW", "proposal"); - when(shadowObservationGateway.planInbound("TASK-1", "TRAY-1")).thenReturn(observation); + void submitsObservationBeforeLegacyAndReturnsTheReloadedLegacyJob() { + when(shadowObservationGateway.observeInbound("TASK-1", "TRAY-1")) + .thenReturn(shadowCompletion); AjaxResult result = service.requestEnterByTaskNo(request); assertThat(result.get(AjaxResult.DATA_TAG)).isSameAs(legacyResult); - InOrder order = inOrder(mydoJobMapper, shadowObservationGateway); + InOrder order = inOrder(mydoJobMapper, shadowObservationGateway, shadowCompletion); order.verify(mydoJobMapper).selectInJobByTaskNo("TASK-1"); - order.verify(shadowObservationGateway).planInbound("TASK-1", "TRAY-1"); + order.verify(shadowObservationGateway).observeInbound("TASK-1", "TRAY-1"); order.verify(mydoJobMapper).allotInJobSpace(any()); order.verify(mydoJobMapper).selectInJobByTaskNo("TASK-1"); - order.verify(shadowObservationGateway) - .compareInbound(observation, "10", "SPACE-LEGACY"); + order.verify(shadowCompletion).complete("10", "SPACE-LEGACY"); verify(mydoJobMapper, times(1)).allotInJobSpace(any()); } @Test void shadowExceptionCannotSkipOrReplaceLegacyAllocation() { - when(shadowObservationGateway.planInbound("TASK-1", "TRAY-1")) + when(shadowObservationGateway.observeInbound("TASK-1", "TRAY-1")) .thenThrow(new IllegalStateException("shadow unavailable")); AjaxResult result = service.requestEnterByTaskNo(request); assertThat(result.get(AjaxResult.DATA_TAG)).isSameAs(legacyResult); - verify(shadowObservationGateway).planInbound("TASK-1", "TRAY-1"); + verify(shadowObservationGateway).observeInbound("TASK-1", "TRAY-1"); verify(mydoJobMapper, times(1)).allotInJobSpace(any()); } diff --git a/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java b/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java index 6507910..fc864d8 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java @@ -1,6 +1,6 @@ package com.ruoyi.out.outStockout.service.impl; -import com.ruoyi.algorithm.shadow.OutboundShadowObservation; +import com.ruoyi.algorithm.shadow.OutboundShadowCompletion; import com.ruoyi.algorithm.shadow.ShadowObservationGateway; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.out.outStockout.domain.OutStockout; @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,6 +36,8 @@ class OutStockoutServiceImplShadowTest { private IOutStockoutDetailService outStockoutDetailService; @Mock private ShadowObservationGateway shadowObservationGateway; + @Mock + private OutboundShadowCompletion shadowCompletion; @InjectMocks private OutStockoutServiceImpl service; @@ -51,9 +54,8 @@ class OutStockoutServiceImplShadowTest { @Test void missingShadowProjectStillRunsAutoAllotAndPreservesLegacyWarning() { prepareAutoAllot(); - OutboundShadowObservation missingProject = OutboundShadowObservation.unavailable( - "W01", "DETAIL-1", "SKIPPED_MISSING_PROJECT", "missing project"); - when(shadowObservationGateway.planProjectOutbound("DETAIL-1")).thenReturn(missingProject); + when(shadowObservationGateway.observeProjectOutbound("DETAIL-1")) + .thenReturn(shadowCompletion); doAnswer(invocation -> { @SuppressWarnings("unchecked") HashMap parameters = invocation.getArgument(0); @@ -65,17 +67,17 @@ class OutStockoutServiceImplShadowTest { AjaxResult result = service.autoAllot("OUT-1"); assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo(",DETAIL-1LEGACY-WARNING"); - InOrder order = inOrder(shadowObservationGateway, outStockoutMapper); - order.verify(shadowObservationGateway).planProjectOutbound("DETAIL-1"); + InOrder order = inOrder(shadowObservationGateway, outStockoutMapper, shadowCompletion); + order.verify(shadowObservationGateway).observeProjectOutbound("DETAIL-1"); order.verify(outStockoutMapper).autoAllot(any()); - order.verify(shadowObservationGateway).compareOutbound(missingProject, "20"); + order.verify(shadowCompletion).complete("20"); verify(outStockoutMapper, times(1)).autoAllot(any()); } @Test void shadowExceptionStillRunsAutoAllotAndPreservesLegacySuccess() { prepareAutoAllot(); - when(shadowObservationGateway.planProjectOutbound("DETAIL-1")) + when(shadowObservationGateway.observeProjectOutbound("DETAIL-1")) .thenThrow(new IllegalStateException("shadow unavailable")); doAnswer(invocation -> { @SuppressWarnings("unchecked") @@ -88,18 +90,18 @@ class OutStockoutServiceImplShadowTest { AjaxResult result = service.autoAllot("OUT-1"); assertThat(result.isSuccess()).isTrue(); - verify(shadowObservationGateway).planProjectOutbound("DETAIL-1"); + verify(shadowObservationGateway).observeProjectOutbound("DETAIL-1"); verify(outStockoutMapper, times(1)).autoAllot(any()); } @Test void missingShadowStockStillRunsManualAllotOncePerSelectedStock() { - OutboundShadowObservation missingStock = OutboundShadowObservation.unavailable( - "W01", "DETAIL-1", "SKIPPED_MISSING_STOCK", "missing stock"); - when(shadowObservationGateway.planExactStockOutbound("DETAIL-1", "STOCK-1")) - .thenReturn(missingStock); - when(shadowObservationGateway.planExactStockOutbound("DETAIL-1", "STOCK-2")) - .thenReturn(missingStock); + OutboundShadowCompletion firstCompletion = mock(OutboundShadowCompletion.class); + OutboundShadowCompletion secondCompletion = mock(OutboundShadowCompletion.class); + when(shadowObservationGateway.observeExactStockOutbound("DETAIL-1", "STOCK-1")) + .thenReturn(firstCompletion); + when(shadowObservationGateway.observeExactStockOutbound("DETAIL-1", "STOCK-2")) + .thenReturn(secondCompletion); doAnswer(invocation -> { @SuppressWarnings("unchecked") HashMap parameters = invocation.getArgument(0); @@ -112,9 +114,10 @@ class OutStockoutServiceImplShadowTest { assertThat(result.isSuccess()).isTrue(); verify(outStockoutMapper, times(2)).manualAllot(any()); - verify(shadowObservationGateway).planExactStockOutbound("DETAIL-1", "STOCK-1"); - verify(shadowObservationGateway).planExactStockOutbound("DETAIL-1", "STOCK-2"); - verify(shadowObservationGateway, times(2)).compareOutbound(missingStock, "10"); + verify(shadowObservationGateway).observeExactStockOutbound("DETAIL-1", "STOCK-1"); + verify(shadowObservationGateway).observeExactStockOutbound("DETAIL-1", "STOCK-2"); + verify(firstCompletion).complete("10"); + verify(secondCompletion).complete("10"); } private void prepareAutoAllot() {