fix: keep shadow work off legacy threads

This commit is contained in:
zgw
2026-08-12 09:19:59 +08:00
parent 34c4d54669
commit bb3b93a79f
8 changed files with 274 additions and 169 deletions

View File

@@ -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;
}
}