feat: decide shadow traffic safely

This commit is contained in:
zgw
2026-07-30 13:44:42 +08:00
parent 03964643e6
commit cdbb59ca65
6 changed files with 328 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
package com.ft546.wcs.shadow.decision;
public enum DecisionType {
GRANT, HOLD, YIELD
}

View File

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

View File

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

View File

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

View File

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