fix(shadow): protect observed vehicle traffic
This commit is contained in:
@@ -102,6 +102,16 @@ public final class DispatchCoordinator {
|
||||
|
||||
VehicleSnapshot vehicle = removeVehicle(
|
||||
available, assignment.getVehicleId());
|
||||
Set<String> observedBlockers = snapshot.observedBlockers(
|
||||
assignment.getRoute().getResourceKeys(), assignment.getVehicleId());
|
||||
if (!observedBlockers.isEmpty()) {
|
||||
held.add(new HeldDispatch(task.getOrderId(), task.getStepId(),
|
||||
assignment.getVehicleId(), TrafficDecisionType.HOLD,
|
||||
"OBSERVED_TRAFFIC_CONFLICT", observedBlockers, null, null));
|
||||
events.add(event(task, assignment.getVehicleId(), TrafficDecisionType.HOLD,
|
||||
"OBSERVED_TRAFFIC_CONFLICT", snapshot));
|
||||
continue;
|
||||
}
|
||||
String reservationId = requireGeneratedId(idGenerator.nextId());
|
||||
ReservationRequest request = new ReservationRequest(reservationId,
|
||||
task.getOrderId(), task.getStepId(), assignment.getVehicleId(),
|
||||
@@ -109,6 +119,13 @@ public final class DispatchCoordinator {
|
||||
TrafficDecision traffic = cycleGuard.decide(new TrafficCandidate(
|
||||
request, vehicle.getLocationNodeId(), assignment.getRoute()),
|
||||
ledger, snapshot.getTopology());
|
||||
if (traffic.getType() == TrafficDecisionType.YIELD) {
|
||||
Set<String> yieldBlockers = snapshot.observedBlockers(
|
||||
traffic.getYieldRoute().getResourceKeys(), assignment.getVehicleId());
|
||||
if (!yieldBlockers.isEmpty()) {
|
||||
traffic = TrafficDecision.hold("OBSERVED_TRAFFIC_CONFLICT", yieldBlockers);
|
||||
}
|
||||
}
|
||||
if (traffic.getType() == TrafficDecisionType.GRANT) {
|
||||
ReservationDecision reservation = traffic.getReservationDecision();
|
||||
granted.add(new GrantedDispatch(task.getOrderId(), task.getStepId(),
|
||||
|
||||
@@ -10,6 +10,10 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
public final class DispatchSnapshot {
|
||||
private final List<DispatchTask> readySteps;
|
||||
@@ -18,6 +22,7 @@ public final class DispatchSnapshot {
|
||||
private final ResourceLedger resourceLedger;
|
||||
private final WaitForGraph waitForGraph;
|
||||
private final long snapshotVersion;
|
||||
private final Map<String, Set<String>> observedResourceOwners;
|
||||
|
||||
public DispatchSnapshot(List<DispatchTask> readySteps,
|
||||
List<VehicleSnapshot> vehicleSnapshots,
|
||||
@@ -34,6 +39,17 @@ public final class DispatchSnapshot {
|
||||
ResourceLedger resourceLedger,
|
||||
WaitForGraph waitForGraph,
|
||||
long snapshotVersion) {
|
||||
this(readySteps, vehicleSnapshots, topology, resourceLedger, waitForGraph,
|
||||
snapshotVersion, Collections.<String, Set<String>>emptyMap());
|
||||
}
|
||||
|
||||
public DispatchSnapshot(List<DispatchTask> readySteps,
|
||||
List<VehicleSnapshot> vehicleSnapshots,
|
||||
EffectiveTopology topology,
|
||||
ResourceLedger resourceLedger,
|
||||
WaitForGraph waitForGraph,
|
||||
long snapshotVersion,
|
||||
Map<String, Set<String>> observedResourceOwners) {
|
||||
this.readySteps = immutable(readySteps, "readySteps");
|
||||
this.vehicleSnapshots = immutable(vehicleSnapshots, "vehicleSnapshots");
|
||||
this.topology = Objects.requireNonNull(topology, "topology");
|
||||
@@ -44,6 +60,22 @@ public final class DispatchSnapshot {
|
||||
throw new IllegalArgumentException("snapshotVersion must not be negative");
|
||||
}
|
||||
this.snapshotVersion = snapshotVersion;
|
||||
Map<String, Set<String>> owners = new LinkedHashMap<String, Set<String>>();
|
||||
for (Map.Entry<String, Set<String>> entry : Objects.requireNonNull(
|
||||
observedResourceOwners, "observedResourceOwners").entrySet()) {
|
||||
if (topology.resource(entry.getKey()) == null) {
|
||||
throw new IllegalArgumentException("unknown observed resource: " + entry.getKey());
|
||||
}
|
||||
Set<String> vehicles = new LinkedHashSet<String>();
|
||||
for (String owner : Objects.requireNonNull(entry.getValue(), "resource owners")) {
|
||||
if (owner == null || owner.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("observed vehicle id must not be blank");
|
||||
}
|
||||
vehicles.add(owner.trim());
|
||||
}
|
||||
owners.put(entry.getKey(), Collections.unmodifiableSet(vehicles));
|
||||
}
|
||||
this.observedResourceOwners = Collections.unmodifiableMap(owners);
|
||||
}
|
||||
|
||||
public List<DispatchTask> getReadySteps() {
|
||||
@@ -70,6 +102,17 @@ public final class DispatchSnapshot {
|
||||
return snapshotVersion;
|
||||
}
|
||||
|
||||
/** Observation-only constraints, deliberately excluded from the persistent ledger. */
|
||||
public Set<String> observedBlockers(List<String> resourceKeys, String vehicleId) {
|
||||
Set<String> blockers = new LinkedHashSet<String>();
|
||||
for (String key : resourceKeys) {
|
||||
Set<String> owners = observedResourceOwners.get(key);
|
||||
if (owners != null) blockers.addAll(owners);
|
||||
}
|
||||
blockers.remove(vehicleId);
|
||||
return Collections.unmodifiableSet(blockers);
|
||||
}
|
||||
|
||||
private static <T> List<T> immutable(List<T> source, String field) {
|
||||
if (source == null) {
|
||||
throw new IllegalArgumentException(field + " must not be null");
|
||||
|
||||
@@ -27,11 +27,14 @@ import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class DispatchCoordinatorTest {
|
||||
|
||||
@@ -100,6 +103,63 @@ class DispatchCoordinatorTest {
|
||||
result.getGranted().get(0).getCommandParameters());
|
||||
}
|
||||
|
||||
@Test
|
||||
void oldSnapshotConstructorsRemainUnconstrainedAndObservationCopiesCannotBeMutated() {
|
||||
EffectiveTopology topology = topology();
|
||||
Set<String> owners = new LinkedHashSet<String>(Collections.singleton("V2"));
|
||||
Map<String, Set<String>> observations = new LinkedHashMap<String, Set<String>>();
|
||||
observations.put("NODE:X", owners);
|
||||
DispatchSnapshot guarded = new DispatchSnapshot(Collections.singletonList(task("O1", "S1", "A", 1)),
|
||||
Collections.singletonList(vehicle("V1", "A")), topology, new ResourceLedger(topology),
|
||||
new WaitForGraph(), 11L, observations);
|
||||
owners.clear();
|
||||
observations.clear();
|
||||
assertThrows(UnsupportedOperationException.class, () -> guarded.observedBlockers(
|
||||
Collections.singletonList("NODE:X"), "V1").clear());
|
||||
RecordingTrafficRepository repository = new RecordingTrafficRepository(true);
|
||||
assertEquals("OBSERVED_TRAFFIC_CONFLICT", coordinator(repository).dispatch(guarded).getHeld().get(0).getReasonCode());
|
||||
assertTrue(repository.lastCommit.getResultingLedger().snapshotState().getReservations().isEmpty());
|
||||
assertEquals(1, coordinator(repository).dispatch(new DispatchSnapshot(guarded.getReadySteps(),
|
||||
guarded.getVehicleSnapshots(), topology, guarded.workingLedger(), 11L)).getGranted().size());
|
||||
assertEquals(1, coordinator(repository).dispatch(new DispatchSnapshot(guarded.getReadySteps(),
|
||||
guarded.getVehicleSnapshots(), topology, guarded.workingLedger(), new WaitForGraph(), 11L)).getGranted().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidObservationKeysAreRejectedRatherThanSilentlyIgnored() {
|
||||
EffectiveTopology topology = topology();
|
||||
assertThrows(IllegalArgumentException.class, () -> new DispatchSnapshot(Collections.emptyList(),
|
||||
Collections.emptyList(), topology, new ResourceLedger(topology), new WaitForGraph(), 0L,
|
||||
Collections.singletonMap("NODE:missing", Collections.singleton("V2"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void yieldMustNotBypassObservedTrafficAndPersistedReservationsStayIntact() {
|
||||
EffectiveTopology topology = new TopologyCompiler().compile(new TopologyDefinition("FT546", 7L, true,
|
||||
Arrays.asList(new TopologyNode("A", "F1", 0, 0, 1, false, false),
|
||||
new TopologyNode("X", "F1", 1, 0, 1, false, false),
|
||||
new TopologyNode("H", "F1", 0, 1, 1, true, true),
|
||||
new TopologyNode("Y", "F1", 1, 1, 1, false, false)),
|
||||
Arrays.asList(new TopologyEdge("A", "X", 1, 1), new TopologyEdge("A", "H", 1, 1),
|
||||
new TopologyEdge("H", "Y", 1, 1), new TopologyEdge("Y", "X", 1, 1)),
|
||||
Collections.emptyList(), Collections.emptyList(), Collections.emptyList()));
|
||||
ResourceLedger ledger = new ResourceLedger(topology);
|
||||
ledger.reserve(new com.ruoyi.wcs.domain.traffic.ReservationRequest("existing", "O2", "S2", "V2",
|
||||
Collections.singletonList("NODE:X")));
|
||||
DispatchSnapshot unguarded = new DispatchSnapshot(Collections.singletonList(task("O1", "S1", "A", 1)),
|
||||
Collections.singletonList(vehicle("V1", "A")), topology, ledger, 11L);
|
||||
RecordingTrafficRepository repository = new RecordingTrafficRepository(true);
|
||||
assertEquals("H", coordinator(repository).dispatch(unguarded).getYielded().get(0).getYieldNodeId());
|
||||
DispatchCycleResult result = coordinator(repository).dispatch(new DispatchSnapshot(unguarded.getReadySteps(),
|
||||
unguarded.getVehicleSnapshots(), topology, ledger, new WaitForGraph(), 11L,
|
||||
Collections.singletonMap("NODE:H", Collections.singleton("V3"))));
|
||||
assertTrue(result.getGranted().isEmpty());
|
||||
assertTrue(result.getYielded().isEmpty());
|
||||
assertEquals(Collections.singleton("V3"), result.getHeld().get(0).getBlockingVehicleIds());
|
||||
assertEquals(1, repository.lastCommit.getResultingLedger().usage("NODE:X"));
|
||||
assertEquals(0, repository.lastCommit.getResultingLedger().usage("NODE:H"));
|
||||
}
|
||||
|
||||
private static DispatchCoordinator coordinator(TrafficStateRepository repository) {
|
||||
AStarRoutePlanner routePlanner = new AStarRoutePlanner();
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
|
||||
Reference in New Issue
Block a user