feat(wcs): prevent traffic deadlocks

This commit is contained in:
zgw
2026-08-10 16:16:05 +08:00
parent 5877d9e60a
commit 8b3713d182
9 changed files with 484 additions and 0 deletions

View File

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

View File

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