fix: keep shadow work off legacy threads
This commit is contained in:
@@ -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<InboundShadowObservation> first =
|
||||
executor.submit(() -> gateway.planInbound("TASK-3", "TRAY-3"));
|
||||
assertThat(planningStarted.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
Future<InboundShadowCompletion> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, String> 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<String, String> 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() {
|
||||
|
||||
Reference in New Issue
Block a user