feat: add fail-open shadow planning boundary
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ShadowObservationGatewayTest {
|
||||
|
||||
@Test
|
||||
void disabledShadowNeverInvokesThePlanningEngine() {
|
||||
ShadowObservationProperties properties = new ShadowObservationProperties();
|
||||
ShadowPlanningEngine engine = mock(ShadowPlanningEngine.class);
|
||||
ShadowObservationGateway gateway = new ShadowObservationGateway(properties, engine);
|
||||
|
||||
InboundShadowObservation observation = gateway.planInbound("TASK-1", "TRAY-1");
|
||||
|
||||
assertThat(observation.observed()).isFalse();
|
||||
verify(engine, never()).planInbound("TASK-1", "TRAY-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void planningFailureIsConvertedToAnEmptyObservation() {
|
||||
ShadowObservationProperties properties = new ShadowObservationProperties();
|
||||
properties.setEnabled(true);
|
||||
ShadowPlanningEngine engine = mock(ShadowPlanningEngine.class);
|
||||
when(engine.planInbound("TASK-2", "TRAY-2"))
|
||||
.thenThrow(new IllegalStateException("algorithm objects are unavailable"));
|
||||
ShadowObservationGateway gateway = new ShadowObservationGateway(properties, engine);
|
||||
|
||||
InboundShadowObservation observation = gateway.planInbound("TASK-2", "TRAY-2");
|
||||
|
||||
assertThat(observation.observed()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingEngineFailsOpenForOutboundObservation() {
|
||||
ShadowObservationProperties properties = new ShadowObservationProperties();
|
||||
properties.setEnabled(true);
|
||||
ShadowObservationGateway gateway = new ShadowObservationGateway(properties, null);
|
||||
|
||||
OutboundShadowObservation observation = gateway.planExactStockOutbound("DETAIL-1", "STOCK-1");
|
||||
|
||||
assertThat(observation.observed()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import com.ruoyi.algorithm.inbound.InboundAssignment;
|
||||
import com.ruoyi.algorithm.inbound.InboundBorrowType;
|
||||
import com.ruoyi.algorithm.inbound.InboundLadderRung;
|
||||
import com.ruoyi.algorithm.inbound.InboundPlan;
|
||||
import com.ruoyi.algorithm.integration.SchedulingFacade;
|
||||
import com.ruoyi.algorithm.repository.VersionToken;
|
||||
import com.ruoyi.algorithm.service.InboundPlanningResult;
|
||||
import com.ruoyi.algorithm.shadow.mapper.ShadowInputMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.support.SimpleTransactionStatus;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ShadowPlanningEngineTest {
|
||||
|
||||
@Test
|
||||
void inboundPlanningUsesRequiresNewAndConfiguredTimeout() {
|
||||
ShadowObservationProperties properties = new ShadowObservationProperties();
|
||||
properties.setWarehouseId("W01");
|
||||
properties.setTimeoutSeconds(7);
|
||||
CapturingTransactionManager transactions = new CapturingTransactionManager();
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
ShadowInboundReference input = new ShadowInboundReference();
|
||||
input.setRequestId("JOB-1");
|
||||
input.setTrayNo("TRAY-1");
|
||||
input.setProjectNo("PROJECT-1");
|
||||
input.setProductId("PRODUCT-1");
|
||||
input.setLotNo("LOT-1");
|
||||
when(mapper.selectInboundInput("TASK-1", "TRAY-1")).thenReturn(input);
|
||||
InboundAssignment assignment = new InboundAssignment(
|
||||
"JOB-1", "SPACE-9", "LANE-2", 4, 2,
|
||||
InboundLadderRung.CONTINUE, InboundBorrowType.NONE, null,
|
||||
1.0, 2.0, 3.0, 50.0, false, false, "shadow proposal");
|
||||
when(facade.planInbound(eq("W01"), any()))
|
||||
.thenReturn(new InboundPlanningResult(InboundPlan.assigned(assignment), VersionToken.numeric(8)));
|
||||
ShadowPlanningEngine engine = new ShadowPlanningEngine(facade, mapper, properties, transactions);
|
||||
|
||||
InboundShadowObservation observation = engine.planInbound("TASK-1", "TRAY-1");
|
||||
|
||||
assertThat(transactions.definition.getPropagationBehavior())
|
||||
.isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
assertThat(transactions.definition.getTimeout()).isEqualTo(7);
|
||||
assertThat(observation.observed()).isTrue();
|
||||
assertThat(observation.proposedSpaceId()).isEqualTo("SPACE-9");
|
||||
}
|
||||
|
||||
private static final class CapturingTransactionManager implements PlatformTransactionManager {
|
||||
private TransactionDefinition definition;
|
||||
|
||||
@Override
|
||||
public TransactionStatus getTransaction(TransactionDefinition definition) {
|
||||
this.definition = definition;
|
||||
return new SimpleTransactionStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit(TransactionStatus status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback(TransactionStatus status) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import com.ruoyi.algorithm.integration.AlgorithmProperties;
|
||||
import com.ruoyi.algorithm.integration.SchedulingMode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class ShadowSafetyGuardTest {
|
||||
|
||||
@Test
|
||||
void enabledShadowAcceptsOnlyShadowModeWithReadModelWrites() {
|
||||
ShadowObservationProperties shadow = enabledShadow();
|
||||
AlgorithmProperties algorithm = new AlgorithmProperties();
|
||||
algorithm.setMode(SchedulingMode.SHADOW);
|
||||
algorithm.setDatabaseWritesEnabled(true);
|
||||
|
||||
assertThatCode(() -> new ShadowSafetyGuard(shadow, algorithm).validate())
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void enabledShadowRejectsActiveAndLegacyModes() {
|
||||
ShadowObservationProperties shadow = enabledShadow();
|
||||
AlgorithmProperties algorithm = new AlgorithmProperties();
|
||||
|
||||
algorithm.setMode(SchedulingMode.ACTIVE);
|
||||
assertThatThrownBy(() -> new ShadowSafetyGuard(shadow, algorithm).validate())
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("SHADOW");
|
||||
|
||||
algorithm.setMode(SchedulingMode.LEGACY);
|
||||
assertThatThrownBy(() -> new ShadowSafetyGuard(shadow, algorithm).validate())
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("SHADOW");
|
||||
}
|
||||
|
||||
@Test
|
||||
void enabledShadowRejectsDisabledReadModelWrites() {
|
||||
ShadowObservationProperties shadow = enabledShadow();
|
||||
AlgorithmProperties algorithm = new AlgorithmProperties();
|
||||
algorithm.setMode(SchedulingMode.SHADOW);
|
||||
algorithm.setDatabaseWritesEnabled(false);
|
||||
|
||||
assertThatThrownBy(() -> new ShadowSafetyGuard(shadow, algorithm).validate())
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("database writes");
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledShadowDoesNotRestrictTheStableLegacyApplication() {
|
||||
ShadowObservationProperties shadow = new ShadowObservationProperties();
|
||||
AlgorithmProperties algorithm = new AlgorithmProperties();
|
||||
algorithm.setMode(SchedulingMode.ACTIVE);
|
||||
algorithm.setDatabaseWritesEnabled(false);
|
||||
|
||||
assertThatCode(() -> new ShadowSafetyGuard(shadow, algorithm).validate())
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
private ShadowObservationProperties enabledShadow() {
|
||||
ShadowObservationProperties properties = new ShadowObservationProperties();
|
||||
properties.setEnabled(true);
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user