feat: decide shadow traffic safely
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package com.ft546.wcs.shadow.decision;
|
||||
|
||||
public enum DecisionType {
|
||||
GRANT, HOLD, YIELD
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ft546.wcs.shadow.decision;
|
||||
|
||||
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
|
||||
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||
import com.ft546.wcs.shadow.resource.ResourceLedger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class RollingDispatchPolicy {
|
||||
public List<ShadowDecision> decide(List<ShadowCandidate> input, ResourceLedger authoritative,
|
||||
EffectiveTrafficMap map) {
|
||||
List<ShadowCandidate> candidates = new ArrayList<ShadowCandidate>(input);
|
||||
Collections.sort(candidates, ORDER);
|
||||
ResourceLedger simulation = authoritative.copy();
|
||||
WaitForGraph waits = new WaitForGraph();
|
||||
List<ShadowDecision> decisions = new ArrayList<ShadowDecision>();
|
||||
for (ShadowCandidate candidate : candidates) {
|
||||
if (candidate.isCommitted()) {
|
||||
decisions.add(decision(candidate, DecisionType.GRANT, "COMMITTED_CONTINUATION",
|
||||
Collections.<ResourceKey>emptySet(), Collections.<String>emptySet(), null));
|
||||
continue;
|
||||
}
|
||||
ResourceLedger.ProtectionResult result =
|
||||
simulation.evaluate(candidate.getVehicleId(), candidate.getResources());
|
||||
if (result.isGranted()) {
|
||||
decisions.add(decision(candidate, DecisionType.GRANT, "RESOURCES_AVAILABLE",
|
||||
Collections.<ResourceKey>emptySet(), Collections.<String>emptySet(), null));
|
||||
continue;
|
||||
}
|
||||
boolean cycle = waits.wouldCreateCycle(candidate.getVehicleId(), result.getBlockingVehicles());
|
||||
waits.addWait(candidate.getVehicleId(), result.getBlockingVehicles());
|
||||
if (cycle) {
|
||||
decisions.add(decision(candidate, DecisionType.HOLD, "WAIT_CYCLE",
|
||||
result.getConflictingResources(), result.getBlockingVehicles(), null));
|
||||
} else if (candidate.getSafeYieldSite() != null
|
||||
&& map.getSafeYieldSites().contains(candidate.getSafeYieldSite())) {
|
||||
decisions.add(decision(candidate, DecisionType.YIELD, "SAFE_YIELD",
|
||||
result.getConflictingResources(), result.getBlockingVehicles(),
|
||||
candidate.getSafeYieldSite()));
|
||||
} else {
|
||||
decisions.add(decision(candidate, DecisionType.HOLD, "NO_SAFE_YIELD_SITE",
|
||||
result.getConflictingResources(), result.getBlockingVehicles(), null));
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableList(decisions);
|
||||
}
|
||||
|
||||
private ShadowDecision decision(ShadowCandidate candidate, DecisionType type, String reason,
|
||||
Set<ResourceKey> resources, Set<String> blockers, String yieldSite) {
|
||||
return new ShadowDecision(candidate.getTaskId(), candidate.getVehicleId(), type, reason,
|
||||
resources, blockers, yieldSite);
|
||||
}
|
||||
|
||||
private static final Comparator<ShadowCandidate> ORDER =
|
||||
Comparator.comparing(ShadowCandidate::isCommitted).reversed()
|
||||
.thenComparing(ShadowCandidate::isEmergency, Comparator.reverseOrder())
|
||||
.thenComparing(ShadowCandidate::getBusinessPriority, Comparator.reverseOrder())
|
||||
.thenComparing(ShadowCandidate::getWaitAge, Comparator.reverseOrder())
|
||||
.thenComparingInt(ShadowCandidate::getClearanceCost)
|
||||
.thenComparing(ShadowCandidate::getTaskId)
|
||||
.thenComparing(ShadowCandidate::getVehicleId);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ft546.wcs.shadow.decision;
|
||||
|
||||
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class ShadowCandidate {
|
||||
private final String taskId;
|
||||
private final String vehicleId;
|
||||
private final boolean committed;
|
||||
private final boolean emergency;
|
||||
private final int businessPriority;
|
||||
private final long waitAge;
|
||||
private final int clearanceCost;
|
||||
private final List<ResourceKey> resources;
|
||||
private final String safeYieldSite;
|
||||
|
||||
private ShadowCandidate(String taskId, String vehicleId, boolean committed, boolean emergency,
|
||||
int businessPriority, long waitAge, int clearanceCost,
|
||||
Collection<ResourceKey> resources, String safeYieldSite) {
|
||||
this.taskId = taskId;
|
||||
this.vehicleId = vehicleId;
|
||||
this.committed = committed;
|
||||
this.emergency = emergency;
|
||||
this.businessPriority = businessPriority;
|
||||
this.waitAge = waitAge;
|
||||
this.clearanceCost = clearanceCost;
|
||||
this.resources = Collections.unmodifiableList(new ArrayList<ResourceKey>(resources));
|
||||
this.safeYieldSite = safeYieldSite;
|
||||
}
|
||||
|
||||
public static ShadowCandidate waiting(String taskId, String vehicleId, boolean emergency,
|
||||
int businessPriority, long waitAge, int clearanceCost,
|
||||
Collection<ResourceKey> resources, String safeYieldSite) {
|
||||
return new ShadowCandidate(taskId, vehicleId, false, emergency, businessPriority,
|
||||
waitAge, clearanceCost, resources, safeYieldSite);
|
||||
}
|
||||
|
||||
public static ShadowCandidate committed(String taskId, String vehicleId,
|
||||
Collection<ResourceKey> resources) {
|
||||
return new ShadowCandidate(taskId, vehicleId, true, false, 0, 0, 0, resources, null);
|
||||
}
|
||||
|
||||
public String getTaskId() { return taskId; }
|
||||
public String getVehicleId() { return vehicleId; }
|
||||
public boolean isCommitted() { return committed; }
|
||||
public boolean isEmergency() { return emergency; }
|
||||
public int getBusinessPriority() { return businessPriority; }
|
||||
public long getWaitAge() { return waitAge; }
|
||||
public int getClearanceCost() { return clearanceCost; }
|
||||
public List<ResourceKey> getResources() { return resources; }
|
||||
public String getSafeYieldSite() { return safeYieldSite; }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ft546.wcs.shadow.decision;
|
||||
|
||||
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ShadowDecision {
|
||||
private final String taskId;
|
||||
private final String vehicleId;
|
||||
private final DecisionType type;
|
||||
private final String reasonCode;
|
||||
private final Set<ResourceKey> conflictingResources;
|
||||
private final Set<String> blockingVehicles;
|
||||
private final String yieldSite;
|
||||
|
||||
public ShadowDecision(String taskId, String vehicleId, DecisionType type, String reasonCode,
|
||||
Set<ResourceKey> conflictingResources, Set<String> blockingVehicles,
|
||||
String yieldSite) {
|
||||
this.taskId = taskId;
|
||||
this.vehicleId = vehicleId;
|
||||
this.type = type;
|
||||
this.reasonCode = reasonCode;
|
||||
this.conflictingResources = Collections.unmodifiableSet(
|
||||
new LinkedHashSet<ResourceKey>(conflictingResources));
|
||||
this.blockingVehicles = Collections.unmodifiableSet(
|
||||
new LinkedHashSet<String>(blockingVehicles));
|
||||
this.yieldSite = yieldSite;
|
||||
}
|
||||
|
||||
public String getTaskId() { return taskId; }
|
||||
public String getVehicleId() { return vehicleId; }
|
||||
public DecisionType getType() { return type; }
|
||||
public String getReasonCode() { return reasonCode; }
|
||||
public Set<ResourceKey> getConflictingResources() { return conflictingResources; }
|
||||
public Set<String> getBlockingVehicles() { return blockingVehicles; }
|
||||
public String getYieldSite() { return yieldSite; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ft546.wcs.shadow.decision;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class WaitForGraph {
|
||||
private final Map<String, Set<String>> edges = new LinkedHashMap<String, Set<String>>();
|
||||
|
||||
public boolean wouldCreateCycle(String waiter, Collection<String> holders) {
|
||||
WaitForGraph copy = copy();
|
||||
copy.put(waiter, holders);
|
||||
return !copy.isAcyclic();
|
||||
}
|
||||
|
||||
public boolean addWait(String waiter, Collection<String> holders) {
|
||||
put(waiter, holders);
|
||||
return !isAcyclic();
|
||||
}
|
||||
|
||||
public boolean isAcyclic() {
|
||||
Set<String> visiting = new LinkedHashSet<String>();
|
||||
Set<String> visited = new LinkedHashSet<String>();
|
||||
for (String node : edges.keySet()) {
|
||||
if (hasCycle(node, visiting, visited)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasCycle(String node, Set<String> visiting, Set<String> visited) {
|
||||
if (visiting.contains(node)) return true;
|
||||
if (visited.contains(node)) return false;
|
||||
visiting.add(node);
|
||||
for (String target : edges.containsKey(node) ? edges.get(node) : Collections.<String>emptySet()) {
|
||||
if (hasCycle(target, visiting, visited)) return true;
|
||||
}
|
||||
visiting.remove(node);
|
||||
visited.add(node);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void put(String waiter, Collection<String> holders) {
|
||||
if (!edges.containsKey(waiter)) edges.put(waiter, new LinkedHashSet<String>());
|
||||
edges.get(waiter).addAll(holders);
|
||||
}
|
||||
|
||||
private WaitForGraph copy() {
|
||||
WaitForGraph copy = new WaitForGraph();
|
||||
for (Map.Entry<String, Set<String>> entry : edges.entrySet()) {
|
||||
copy.edges.put(entry.getKey(), new LinkedHashSet<String>(entry.getValue()));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.ft546.wcs.shadow;
|
||||
|
||||
import com.ft546.wcs.shadow.decision.DecisionType;
|
||||
import com.ft546.wcs.shadow.decision.RollingDispatchPolicy;
|
||||
import com.ft546.wcs.shadow.decision.ShadowCandidate;
|
||||
import com.ft546.wcs.shadow.decision.ShadowDecision;
|
||||
import com.ft546.wcs.shadow.decision.WaitForGraph;
|
||||
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
|
||||
import com.ft546.wcs.shadow.map.TrafficMapCompiler;
|
||||
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||
import com.ft546.wcs.shadow.resource.ResourceLedger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
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 RollingDispatchPolicyTest {
|
||||
|
||||
@Test
|
||||
void sortsDeterministicallyAndAllowsNonConflictingParallelGrants() {
|
||||
EffectiveTrafficMap map = map();
|
||||
List<ShadowCandidate> candidates = Arrays.asList(
|
||||
candidate("T2", "V2", 5, 10, ResourceKey.node("C"), null),
|
||||
candidate("T1", "V1", 5, 20, ResourceKey.node("A"), null));
|
||||
|
||||
List<ShadowDecision> decisions =
|
||||
new RollingDispatchPolicy().decide(candidates, new ResourceLedger(map.getCapacities()), map);
|
||||
|
||||
assertEquals("V1", decisions.get(0).getVehicleId());
|
||||
assertEquals("V2", decisions.get(1).getVehicleId());
|
||||
assertEquals(DecisionType.GRANT, decisions.get(0).getType());
|
||||
assertEquals(DecisionType.GRANT, decisions.get(1).getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdsConflictsAndUsesOnlyPublishedYieldSite() {
|
||||
EffectiveTrafficMap map = map();
|
||||
ResourceLedger ledger = new ResourceLedger(map.getCapacities());
|
||||
ledger.evaluate("BLOCKER", Collections.singleton(ResourceKey.node("B")));
|
||||
ShadowCandidate yield = candidate("T1", "V1", 5, 10, ResourceKey.node("B"), "C");
|
||||
ShadowCandidate unsafe = candidate("T2", "V2", 4, 10, ResourceKey.node("B"), "X");
|
||||
|
||||
List<ShadowDecision> decisions =
|
||||
new RollingDispatchPolicy().decide(Arrays.asList(yield, unsafe), ledger, map);
|
||||
|
||||
assertEquals(DecisionType.YIELD, decisions.get(0).getType());
|
||||
assertEquals("C", decisions.get(0).getYieldSite());
|
||||
assertEquals(DecisionType.HOLD, decisions.get(1).getType());
|
||||
assertEquals("NO_SAFE_YIELD_SITE", decisions.get(1).getReasonCode());
|
||||
assertEquals(Collections.singleton("BLOCKER"), decisions.get(1).getBlockingVehicles());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWaitCycleAndNeverRevokesCommittedSegment() {
|
||||
EffectiveTrafficMap map = map();
|
||||
ResourceLedger ledger = new ResourceLedger(map.getCapacities());
|
||||
ledger.evaluate("V2", Collections.singleton(ResourceKey.node("A")));
|
||||
ledger.evaluate("V1", Collections.singleton(ResourceKey.node("B")));
|
||||
ShadowCandidate first = candidate("T1", "V1", 5, 20, ResourceKey.node("A"), null);
|
||||
ShadowCandidate second = candidate("T2", "V2", 5, 10, ResourceKey.node("B"), null);
|
||||
ShadowCandidate committed = ShadowCandidate.committed(
|
||||
"T0", "V0", Collections.singleton(ResourceKey.node("A")));
|
||||
|
||||
List<ShadowDecision> decisions =
|
||||
new RollingDispatchPolicy().decide(Arrays.asList(first, second, committed), ledger, map);
|
||||
|
||||
assertEquals(DecisionType.GRANT, decisions.get(0).getType());
|
||||
assertEquals("COMMITTED_CONTINUATION", decisions.get(0).getReasonCode());
|
||||
assertEquals("WAIT_CYCLE", decisions.get(2).getReasonCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsCycleBeforeAddingWaitEdges() {
|
||||
WaitForGraph graph = new WaitForGraph();
|
||||
assertFalse(graph.addWait("V1", Collections.singleton("V2")));
|
||||
assertTrue(graph.wouldCreateCycle("V2", Collections.singleton("V1")));
|
||||
assertTrue(graph.addWait("V2", Collections.singleton("V1")));
|
||||
assertFalse(graph.isAcyclic());
|
||||
}
|
||||
|
||||
private ShadowCandidate candidate(String task, String vehicle, int priority, long age,
|
||||
ResourceKey resource, String yieldSite) {
|
||||
return ShadowCandidate.waiting(task, vehicle, false, priority, age, 1,
|
||||
Collections.singleton(resource), yieldSite);
|
||||
}
|
||||
|
||||
private EffectiveTrafficMap map() {
|
||||
LinkedHashSet<String> nodes = new LinkedHashSet<String>(Arrays.asList("A", "B", "C"));
|
||||
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
|
||||
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "B"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("B", "C"));
|
||||
return new TrafficMapCompiler().compile(new TrafficMapCompiler.MapDefinition(
|
||||
"FT546", 2L, "ZONE-1", true, "sha", "sha", nodes, edges,
|
||||
new LinkedHashMap<ResourceKey, Integer>(), Collections.singleton("A"),
|
||||
Collections.singleton("C")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user