feat(wcs): prevent traffic deadlocks
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package com.ruoyi.wcs.domain.traffic;
|
||||
|
||||
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
|
||||
import com.ruoyi.wcs.domain.topology.ResourceDefinition;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyCompiler;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyDefinition;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyEdge;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyNode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DeadlockGuardTest {
|
||||
|
||||
@Test
|
||||
void rejectsGrantThatClosesAWaitCycle() {
|
||||
WaitForGraph graph = new WaitForGraph();
|
||||
graph.addWait("V1", "V2");
|
||||
graph.addWait("V2", "V3");
|
||||
|
||||
assertTrue(graph.wouldCreateCycle("V3", Collections.singleton("V1")));
|
||||
assertFalse(graph.wouldCreateCycle("V4", Collections.singleton("V1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotInventYieldPointWhenTopologyHasNone() {
|
||||
EffectiveTopology topology = topology(false);
|
||||
ResourceLedger ledger = new ResourceLedger(topology);
|
||||
ledger.reserve(request("HOLDER", "V1", "NODE:B"));
|
||||
DeadlockGuard guard = guard(new WaitForGraph());
|
||||
|
||||
TrafficDecision decision = guard.decide(
|
||||
candidate("CANDIDATE", "V2", topology), ledger, topology);
|
||||
|
||||
assertEquals(TrafficDecisionType.HOLD, decision.getType());
|
||||
assertEquals("NO_SAFE_YIELD_POINT", decision.getReasonCode());
|
||||
assertEquals(Collections.singleton("V1"), decision.getBlockingVehicleIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void yieldsOnlyToPublishedSafePointOverAnUnoccupiedRoute() {
|
||||
EffectiveTopology topology = topology(true);
|
||||
ResourceLedger ledger = new ResourceLedger(topology);
|
||||
ledger.reserve(request("HOLDER", "V1", "NODE:B"));
|
||||
|
||||
TrafficDecision decision = guard(new WaitForGraph()).decide(
|
||||
candidate("CANDIDATE", "V2", topology), ledger, topology);
|
||||
|
||||
assertEquals(TrafficDecisionType.YIELD, decision.getType());
|
||||
assertEquals("SAFE_YIELD_POINT", decision.getReasonCode());
|
||||
assertEquals("C", decision.getYieldNodeId());
|
||||
assertEquals(Arrays.asList("A", "C"), decision.getYieldRoute().getNodeIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdsInsteadOfAddingAnEdgeThatWouldCreateCycle() {
|
||||
EffectiveTopology topology = topology(true);
|
||||
ResourceLedger ledger = new ResourceLedger(topology);
|
||||
ledger.reserve(request("HOLDER", "V1", "NODE:B"));
|
||||
WaitForGraph graph = new WaitForGraph();
|
||||
graph.addWait("V1", "V2");
|
||||
|
||||
TrafficDecision decision = guard(graph).decide(
|
||||
candidate("CANDIDATE", "V2", topology), ledger, topology);
|
||||
|
||||
assertEquals(TrafficDecisionType.HOLD, decision.getType());
|
||||
assertEquals("WAIT_CYCLE", decision.getReasonCode());
|
||||
assertFalse(graph.holdersWaitedOnBy("V2").contains("V1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void grantsAndReservesWhenAllResourcesAreAvailable() {
|
||||
EffectiveTopology topology = topology(false);
|
||||
ResourceLedger ledger = new ResourceLedger(topology);
|
||||
TrafficCandidate candidate = new TrafficCandidate(
|
||||
request("CANDIDATE", "V2", "NODE:B"), "A",
|
||||
new AStarRoutePlanner().plan(topology, "A", "B",
|
||||
Collections.<String>emptySet()));
|
||||
|
||||
TrafficDecision decision = guard(new WaitForGraph())
|
||||
.decide(candidate, ledger, topology);
|
||||
|
||||
assertEquals(TrafficDecisionType.GRANT, decision.getType());
|
||||
assertTrue(decision.getReservationDecision().isGranted());
|
||||
assertEquals(1, ledger.usage("NODE:B"));
|
||||
}
|
||||
|
||||
private static DeadlockGuard guard(WaitForGraph graph) {
|
||||
return new DeadlockGuard(graph, new AStarRoutePlanner());
|
||||
}
|
||||
|
||||
private static TrafficCandidate candidate(String reservationId,
|
||||
String vehicleId,
|
||||
EffectiveTopology topology) {
|
||||
return new TrafficCandidate(request(reservationId, vehicleId, "NODE:B"),
|
||||
"A", new AStarRoutePlanner().plan(topology, "A", "B",
|
||||
Collections.<String>emptySet()));
|
||||
}
|
||||
|
||||
private static ReservationRequest request(String reservationId,
|
||||
String vehicleId,
|
||||
String resourceKey) {
|
||||
return new ReservationRequest(reservationId, "O-" + reservationId,
|
||||
"S-" + reservationId, vehicleId,
|
||||
Collections.singletonList(resourceKey));
|
||||
}
|
||||
|
||||
private static EffectiveTopology topology(boolean safeYieldPoint) {
|
||||
TopologyDefinition definition = new TopologyDefinition("FT546", 7L, true,
|
||||
Arrays.asList(
|
||||
new TopologyNode("A", "F1", 0, 0, 1, false, false),
|
||||
new TopologyNode("B", "F1", 1, 0, 1, false, false),
|
||||
new TopologyNode("C", "F1", 0, 1, 1, false, safeYieldPoint)),
|
||||
Arrays.asList(
|
||||
new TopologyEdge("A", "B", 1, 1),
|
||||
new TopologyEdge("A", "C", 1, 1)),
|
||||
Collections.<ResourceDefinition>emptyList(),
|
||||
Collections.<String>emptyList(), Collections.<String>emptyList());
|
||||
return new TopologyCompiler().compile(definition);
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,30 @@ class ResourceLedgerTest {
|
||||
assertTrue(ledger.reserve(request("R2", "V2", "NODE:A")).isGranted());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownResourceWithoutChangingKnownCapacity() {
|
||||
ResourceLedger ledger = ledger("NODE:A", 1);
|
||||
|
||||
ReservationDecision decision = ledger.reserve(
|
||||
request("R1", "V1", "NODE:UNKNOWN"));
|
||||
|
||||
assertFalse(decision.isGranted());
|
||||
assertEquals("UNKNOWN_RESOURCE", decision.getReasonCode());
|
||||
assertEquals(0, ledger.usage("NODE:A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unresolvedRecoveryKeepsOccupiedResourcesProtected() {
|
||||
ResourceLedger ledger = ledger("NODE:A", 1);
|
||||
ReservationDecision decision = ledger.reserve(request("R1", "V1", "NODE:A"));
|
||||
ledger.occupy("R1", decision.getFencingToken());
|
||||
ledger.markRecoveryUnresolved("R1");
|
||||
|
||||
assertThrows(DomainRuleViolation.class, () -> ledger.release(
|
||||
"R1", "PLC-SNAPSHOT-1", decision.getFencingToken()));
|
||||
assertEquals(1, ledger.usage("NODE:A"));
|
||||
}
|
||||
|
||||
private static ResourceLedger ledger(String resourceKey, int capacity) {
|
||||
return new ResourceLedger(Collections.singletonMap(resourceKey, capacity));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user