feat(wcs): coordinate lift joint resources
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
package com.ruoyi.wcs.application.device;
|
||||
|
||||
import com.ruoyi.wcs.domain.equipment.LiftDecision;
|
||||
import com.ruoyi.wcs.domain.equipment.LiftDecisionType;
|
||||
import com.ruoyi.wcs.domain.equipment.LiftTransfer;
|
||||
import com.ruoyi.wcs.domain.equipment.LiftTransferState;
|
||||
import com.ruoyi.wcs.domain.execution.CommandOutboxEntry;
|
||||
import com.ruoyi.wcs.domain.execution.DeviceCommand;
|
||||
import com.ruoyi.wcs.domain.execution.DeviceSnapshot;
|
||||
import com.ruoyi.wcs.domain.shared.IdGenerator;
|
||||
import com.ruoyi.wcs.domain.traffic.ReservationDecision;
|
||||
import com.ruoyi.wcs.domain.traffic.ReservationRequest;
|
||||
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
|
||||
import com.ruoyi.wcs.port.out.CommandOutboxRepository;
|
||||
import com.ruoyi.wcs.port.out.DeviceCommandRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class LiftCoordinatorTest {
|
||||
private ResourceLedger ledger;
|
||||
private FakeCommandRepository commands;
|
||||
private FakeOutboxRepository outbox;
|
||||
private LiftCoordinator coordinator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Map<String, Integer> capacities = new LinkedHashMap<String, Integer>();
|
||||
capacities.put("LIFT_PORT:P1", 1);
|
||||
capacities.put("LIFT:L1", 1);
|
||||
capacities.put("LIFT_PORT:P2", 1);
|
||||
ledger = new ResourceLedger(capacities);
|
||||
commands = new FakeCommandRepository();
|
||||
outbox = new FakeOutboxRepository();
|
||||
coordinator = new LiftCoordinator(ledger, commands, outbox,
|
||||
new IdGenerator() {
|
||||
@Override
|
||||
public String nextId() {
|
||||
return "LIFT-CMD-1";
|
||||
}
|
||||
}, 7L, new java.util.function.Supplier<Instant>() {
|
||||
@Override
|
||||
public Instant get() {
|
||||
return Instant.EPOCH;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotEnterLiftWhenTargetExitCannotBeReserved() {
|
||||
ReservationDecision occupied = ledger.reserve(new ReservationRequest(
|
||||
"OTHER-RES", "OTHER-ORDER", "OTHER-STEP", "OTHER",
|
||||
Collections.singletonList("LIFT_PORT:P2")));
|
||||
ledger.occupy("OTHER-RES", occupied.getFencingToken());
|
||||
|
||||
LiftDecision decision = coordinator.prepare(
|
||||
"STEP-1", "V1", "L1", "P1", "P2");
|
||||
|
||||
assertEquals(LiftDecisionType.BLOCKED, decision.getType());
|
||||
assertEquals(0, commands.values.size());
|
||||
assertEquals(0, outbox.values.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void blockedPreparationCanRetryAfterTheTargetPortBecomesAvailable() {
|
||||
ReservationDecision occupied = ledger.reserve(new ReservationRequest(
|
||||
"OTHER-RES", "OTHER-ORDER", "OTHER-STEP", "OTHER",
|
||||
Collections.singletonList("LIFT_PORT:P2")));
|
||||
ledger.occupy("OTHER-RES", occupied.getFencingToken());
|
||||
LiftDecision blocked = coordinator.prepare(
|
||||
"STEP-1", "V1", "L1", "P1", "P2");
|
||||
ledger.release("OTHER-RES", "OTHER-LEFT",
|
||||
occupied.getFencingToken());
|
||||
|
||||
LiftDecision retried = coordinator.prepare(
|
||||
"STEP-1", "V1", "L1", "P1", "P2");
|
||||
|
||||
assertEquals(LiftDecisionType.BLOCKED, blocked.getType());
|
||||
assertEquals(LiftDecisionType.READY, retried.getType());
|
||||
assertEquals(1, commands.values.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasesLiftOnlyAfterVehicleLeavesTargetPort() {
|
||||
LiftDecision prepared = coordinator.prepare(
|
||||
"STEP-1", "V1", "L1", "P1", "P2");
|
||||
LiftTransfer transfer = prepared.getTransfer();
|
||||
transfer.vehicleEnteredLift("VEHICLE-IN-LIFT");
|
||||
transfer.liftStartedMoving("LIFT-MOVING");
|
||||
transfer.arrivedAtTarget("AT-TARGET");
|
||||
transfer.commitTargetExit("TARGET-EXIT");
|
||||
|
||||
LiftDecision completed = coordinator.onVehicleLeftTarget(
|
||||
transfer, matchingFacts());
|
||||
|
||||
assertEquals(LiftDecisionType.COMPLETED, completed.getType());
|
||||
assertEquals(LiftTransferState.COMPLETED, transfer.getState());
|
||||
assertEquals(1, ledger.availableCapacity("LIFT:L1"));
|
||||
assertEquals(1, ledger.availableCapacity("LIFT_PORT:P2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mismatchingDoorOrFloorFactsKeepAllJointResourcesIsolated() {
|
||||
LiftTransfer transfer = coordinator.prepare(
|
||||
"STEP-1", "V1", "L1", "P1", "P2").getTransfer();
|
||||
transfer.vehicleEnteredLift("VEHICLE-IN-LIFT");
|
||||
transfer.liftStartedMoving("LIFT-MOVING");
|
||||
transfer.arrivedAtTarget("AT-TARGET");
|
||||
transfer.commitTargetExit("TARGET-EXIT");
|
||||
Map<String, String> facts = matchingFactMap();
|
||||
facts.put("liftDoorOpen", "false");
|
||||
|
||||
LiftDecision decision = coordinator.onVehicleLeftTarget(
|
||||
transfer, snapshot(facts));
|
||||
|
||||
assertEquals(LiftDecisionType.RECOVERY_REQUIRED, decision.getType());
|
||||
assertEquals(0, ledger.availableCapacity("LIFT:L1"));
|
||||
assertEquals(0, ledger.availableCapacity("LIFT_PORT:P2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void laterMatchingCombinedFactsCanResolveTheIsolationAndRelease() {
|
||||
LiftTransfer transfer = coordinator.prepare(
|
||||
"STEP-1", "V1", "L1", "P1", "P2").getTransfer();
|
||||
transfer.vehicleEnteredLift("VEHICLE-IN-LIFT");
|
||||
transfer.liftStartedMoving("LIFT-MOVING");
|
||||
transfer.arrivedAtTarget("AT-TARGET");
|
||||
transfer.commitTargetExit("TARGET-EXIT");
|
||||
Map<String, String> mismatch = matchingFactMap();
|
||||
mismatch.put("liftFloor", "P1");
|
||||
coordinator.onVehicleLeftTarget(transfer, snapshot(mismatch));
|
||||
|
||||
LiftDecision recovered = coordinator.onVehicleLeftTarget(
|
||||
transfer, matchingFacts());
|
||||
|
||||
assertEquals(LiftDecisionType.COMPLETED, recovered.getType());
|
||||
assertEquals(1, ledger.availableCapacity("LIFT:L1"));
|
||||
}
|
||||
|
||||
private static DeviceSnapshot matchingFacts() {
|
||||
return snapshot(matchingFactMap());
|
||||
}
|
||||
|
||||
private static Map<String, String> matchingFactMap() {
|
||||
Map<String, String> facts = new LinkedHashMap<String, String>();
|
||||
facts.put("liftId", "L1");
|
||||
facts.put("liftFloor", "P2");
|
||||
facts.put("liftDoorOpen", "true");
|
||||
facts.put("targetPortClear", "true");
|
||||
return facts;
|
||||
}
|
||||
|
||||
private static DeviceSnapshot snapshot(Map<String, String> facts) {
|
||||
return new DeviceSnapshot("LIFT-SNAPSHOT-1", "V1",
|
||||
"LIFT:STEP-1", "STEP-1", 10L, Instant.EPOCH.plusSeconds(10),
|
||||
true, "BOOT-1", 7L, 1L, "P2",
|
||||
Collections.singletonList("P2"), true, facts);
|
||||
}
|
||||
|
||||
private static final class FakeCommandRepository
|
||||
implements DeviceCommandRepository {
|
||||
private final Map<String, DeviceCommand> values =
|
||||
new LinkedHashMap<String, DeviceCommand>();
|
||||
|
||||
@Override
|
||||
public DeviceCommand findByCommandKey(String commandKey) {
|
||||
for (DeviceCommand command : values.values()) {
|
||||
if (command.getCommandKey().equals(commandKey)) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceCommand findByCommandId(String commandId) {
|
||||
return values.get(commandId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DeviceCommand command) {
|
||||
values.put(command.getCommandId(), command);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FakeOutboxRepository
|
||||
implements CommandOutboxRepository {
|
||||
private final Map<String, CommandOutboxEntry> values =
|
||||
new LinkedHashMap<String, CommandOutboxEntry>();
|
||||
|
||||
@Override
|
||||
public void saveIfAbsent(CommandOutboxEntry entry) {
|
||||
if (!values.containsKey(entry.getCommandId())) {
|
||||
values.put(entry.getCommandId(), entry);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(CommandOutboxEntry entry) {
|
||||
values.put(entry.getCommandId(), entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandOutboxEntry> claimDue(int limit, Instant now) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user