feat: model shadow traffic resources
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package com.ft546.wcs.shadow.map;
|
||||
|
||||
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class EffectiveTrafficMap {
|
||||
private final String mapCode;
|
||||
private final long versionId;
|
||||
private final String zoneCode;
|
||||
private final String checksum;
|
||||
private final Set<String> nodes;
|
||||
private final Set<ResourceKey> edges;
|
||||
private final Map<ResourceKey, Integer> capacities;
|
||||
private final Set<String> safeWaitSites;
|
||||
private final Set<String> safeYieldSites;
|
||||
|
||||
EffectiveTrafficMap(String mapCode, long versionId, String zoneCode, String checksum,
|
||||
Set<String> nodes, Set<ResourceKey> edges, Map<ResourceKey, Integer> capacities,
|
||||
Set<String> safeWaitSites, Set<String> safeYieldSites) {
|
||||
this.mapCode = mapCode;
|
||||
this.versionId = versionId;
|
||||
this.zoneCode = zoneCode;
|
||||
this.checksum = checksum;
|
||||
this.nodes = Collections.unmodifiableSet(new LinkedHashSet<String>(nodes));
|
||||
this.edges = Collections.unmodifiableSet(new LinkedHashSet<ResourceKey>(edges));
|
||||
this.capacities = Collections.unmodifiableMap(new LinkedHashMap<ResourceKey, Integer>(capacities));
|
||||
this.safeWaitSites = Collections.unmodifiableSet(new LinkedHashSet<String>(safeWaitSites));
|
||||
this.safeYieldSites = Collections.unmodifiableSet(new LinkedHashSet<String>(safeYieldSites));
|
||||
}
|
||||
|
||||
public String getMapCode() { return mapCode; }
|
||||
public long getVersionId() { return versionId; }
|
||||
public String getZoneCode() { return zoneCode; }
|
||||
public String getChecksum() { return checksum; }
|
||||
public Set<String> getNodes() { return nodes; }
|
||||
public Set<ResourceKey> getEdges() { return edges; }
|
||||
public Map<ResourceKey, Integer> getCapacities() { return capacities; }
|
||||
public Set<String> getSafeWaitSites() { return safeWaitSites; }
|
||||
public Set<String> getSafeYieldSites() { return safeYieldSites; }
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.ft546.wcs.shadow.map;
|
||||
|
||||
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class TrafficMapCompiler {
|
||||
public EffectiveTrafficMap compile(MapDefinition definition) {
|
||||
if (definition == null) {
|
||||
throw new IllegalArgumentException("map definition is required");
|
||||
}
|
||||
if (!definition.confirmed) {
|
||||
throw new IllegalArgumentException("traffic zone is not confirmed");
|
||||
}
|
||||
if (!definition.baseChecksum.equals(definition.configuredChecksum)) {
|
||||
throw new IllegalArgumentException("base map checksum mismatch");
|
||||
}
|
||||
LinkedHashSet<ResourceKey> edges = new LinkedHashSet<ResourceKey>();
|
||||
LinkedHashMap<ResourceKey, Integer> capacities = new LinkedHashMap<ResourceKey, Integer>();
|
||||
for (String node : definition.nodes) {
|
||||
capacities.put(ResourceKey.node(node), 1);
|
||||
}
|
||||
for (EdgeDefinition edge : definition.edges) {
|
||||
if (!definition.nodes.contains(edge.first) || !definition.nodes.contains(edge.second)) {
|
||||
throw new IllegalArgumentException("edge references missing node");
|
||||
}
|
||||
ResourceKey key = ResourceKey.edge(edge.first, edge.second);
|
||||
edges.add(key);
|
||||
capacities.put(key, 1);
|
||||
}
|
||||
for (Map.Entry<ResourceKey, Integer> entry : definition.explicitCapacities.entrySet()) {
|
||||
if (entry.getValue() == null || entry.getValue() <= 0) {
|
||||
throw new IllegalArgumentException("resource capacity must be positive");
|
||||
}
|
||||
capacities.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return new EffectiveTrafficMap(definition.mapCode, definition.versionId, definition.zoneCode,
|
||||
definition.baseChecksum, definition.nodes, edges, capacities,
|
||||
definition.safeWaitSites, definition.safeYieldSites);
|
||||
}
|
||||
|
||||
public static final class EdgeDefinition {
|
||||
private final String first;
|
||||
private final String second;
|
||||
|
||||
public EdgeDefinition(String first, String second) {
|
||||
this.first = first;
|
||||
this.second = second;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MapDefinition {
|
||||
private final String mapCode;
|
||||
private final long versionId;
|
||||
private final String zoneCode;
|
||||
private final boolean confirmed;
|
||||
private final String baseChecksum;
|
||||
private final String configuredChecksum;
|
||||
private final Set<String> nodes;
|
||||
private final Set<EdgeDefinition> edges;
|
||||
private final Map<ResourceKey, Integer> explicitCapacities;
|
||||
private final Set<String> safeWaitSites;
|
||||
private final Set<String> safeYieldSites;
|
||||
|
||||
public MapDefinition(String mapCode, long versionId, String zoneCode, boolean confirmed,
|
||||
String baseChecksum, String configuredChecksum, Set<String> nodes,
|
||||
Set<EdgeDefinition> edges, Map<ResourceKey, Integer> explicitCapacities,
|
||||
Set<String> safeWaitSites, Set<String> safeYieldSites) {
|
||||
if (mapCode == null || zoneCode == null || baseChecksum == null || configuredChecksum == null) {
|
||||
throw new IllegalArgumentException("map identity is required");
|
||||
}
|
||||
this.mapCode = mapCode;
|
||||
this.versionId = versionId;
|
||||
this.zoneCode = zoneCode;
|
||||
this.confirmed = confirmed;
|
||||
this.baseChecksum = baseChecksum;
|
||||
this.configuredChecksum = configuredChecksum;
|
||||
this.nodes = immutable(nodes);
|
||||
this.edges = immutable(edges);
|
||||
this.explicitCapacities = Collections.unmodifiableMap(
|
||||
new LinkedHashMap<ResourceKey, Integer>(explicitCapacities));
|
||||
this.safeWaitSites = immutable(safeWaitSites);
|
||||
this.safeYieldSites = immutable(safeYieldSites);
|
||||
}
|
||||
|
||||
private static <T> Set<T> immutable(Set<T> source) {
|
||||
return Collections.unmodifiableSet(new LinkedHashSet<T>(source));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ft546.wcs.shadow.resource;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ResourceKey implements Comparable<ResourceKey> {
|
||||
private final String value;
|
||||
|
||||
private ResourceKey(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static ResourceKey node(String siteId) {
|
||||
return new ResourceKey("NODE:" + required(siteId));
|
||||
}
|
||||
|
||||
public static ResourceKey edge(String first, String second) {
|
||||
String a = required(first);
|
||||
String b = required(second);
|
||||
return new ResourceKey(a.compareTo(b) <= 0 ? "EDGE:" + a + ":" + b : "EDGE:" + b + ":" + a);
|
||||
}
|
||||
|
||||
public static ResourceKey conflict(String code) {
|
||||
return new ResourceKey("CONFLICT:" + required(code));
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String required(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("resource component is required");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ResourceKey other) {
|
||||
return value.compareTo(other.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof ResourceKey && value.equals(((ResourceKey) other).value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.ft546.wcs.shadow.resource;
|
||||
|
||||
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 ResourceLedger {
|
||||
private final Map<ResourceKey, Integer> capacities;
|
||||
private final Map<ResourceKey, LinkedHashSet<String>> holders =
|
||||
new LinkedHashMap<ResourceKey, LinkedHashSet<String>>();
|
||||
|
||||
public ResourceLedger(Map<ResourceKey, Integer> capacities) {
|
||||
this.capacities = Collections.unmodifiableMap(new LinkedHashMap<ResourceKey, Integer>(capacities));
|
||||
}
|
||||
|
||||
public ProtectionResult evaluate(String vehicleId, Collection<ResourceKey> requested) {
|
||||
LinkedHashSet<ResourceKey> conflicts = new LinkedHashSet<ResourceKey>();
|
||||
LinkedHashSet<String> blockers = new LinkedHashSet<String>();
|
||||
for (ResourceKey key : requested) {
|
||||
int capacity = capacity(key);
|
||||
Set<String> current = holders.containsKey(key) ? holders.get(key) : Collections.<String>emptySet();
|
||||
int occupiedByOthers = current.contains(vehicleId) ? current.size() - 1 : current.size();
|
||||
if (occupiedByOthers >= capacity) {
|
||||
conflicts.add(key);
|
||||
for (String holder : current) {
|
||||
if (!holder.equals(vehicleId)) {
|
||||
blockers.add(holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!conflicts.isEmpty()) {
|
||||
return new ProtectionResult(false, conflicts, blockers);
|
||||
}
|
||||
for (ResourceKey key : requested) {
|
||||
if (!holders.containsKey(key)) {
|
||||
holders.put(key, new LinkedHashSet<String>());
|
||||
}
|
||||
holders.get(key).add(vehicleId);
|
||||
}
|
||||
return new ProtectionResult(true, Collections.<ResourceKey>emptySet(), Collections.<String>emptySet());
|
||||
}
|
||||
|
||||
public void release(String vehicleId, Collection<ResourceKey> resources) {
|
||||
for (ResourceKey key : resources) {
|
||||
Set<String> current = holders.get(key);
|
||||
if (current != null) {
|
||||
current.remove(vehicleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> holdersFor(ResourceKey key) {
|
||||
Set<String> current = holders.get(key);
|
||||
return current == null ? Collections.<String>emptySet()
|
||||
: Collections.unmodifiableSet(new LinkedHashSet<String>(current));
|
||||
}
|
||||
|
||||
public int protectedCount(ResourceKey key) {
|
||||
return holdersFor(key).size();
|
||||
}
|
||||
|
||||
public ResourceLedger copy() {
|
||||
ResourceLedger copy = new ResourceLedger(capacities);
|
||||
for (Map.Entry<ResourceKey, LinkedHashSet<String>> entry : holders.entrySet()) {
|
||||
copy.holders.put(entry.getKey(), new LinkedHashSet<String>(entry.getValue()));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private int capacity(ResourceKey key) {
|
||||
Integer capacity = capacities.get(key);
|
||||
if (capacity == null) {
|
||||
throw new IllegalArgumentException("unknown resource " + key);
|
||||
}
|
||||
return capacity;
|
||||
}
|
||||
|
||||
public static final class ProtectionResult {
|
||||
private final boolean granted;
|
||||
private final Set<ResourceKey> conflictingResources;
|
||||
private final Set<String> blockingVehicles;
|
||||
|
||||
private ProtectionResult(boolean granted, Set<ResourceKey> conflictingResources,
|
||||
Set<String> blockingVehicles) {
|
||||
this.granted = granted;
|
||||
this.conflictingResources = Collections.unmodifiableSet(
|
||||
new LinkedHashSet<ResourceKey>(conflictingResources));
|
||||
this.blockingVehicles = Collections.unmodifiableSet(
|
||||
new LinkedHashSet<String>(blockingVehicles));
|
||||
}
|
||||
|
||||
public boolean isGranted() { return granted; }
|
||||
public Set<ResourceKey> getConflictingResources() { return conflictingResources; }
|
||||
public Set<String> getBlockingVehicles() { return blockingVehicles; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.ft546.wcs.shadow;
|
||||
|
||||
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.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MapAndResourceLedgerTest {
|
||||
|
||||
@Test
|
||||
void canonicalizesUndirectedEdgesAndDoesNotLockWholeZone() {
|
||||
assertEquals("EDGE:A:B", ResourceKey.edge("B", "A").value());
|
||||
|
||||
EffectiveTrafficMap map = new TrafficMapCompiler().compile(definition("sha-546", "sha-546", true));
|
||||
ResourceLedger ledger = new ResourceLedger(map.getCapacities());
|
||||
|
||||
assertTrue(ledger.evaluate("V1", Collections.singleton(ResourceKey.node("A"))).isGranted());
|
||||
assertTrue(ledger.evaluate("V2", Collections.singleton(ResourceKey.node("C"))).isGranted());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnconfirmedOrMismatchedMap() {
|
||||
TrafficMapCompiler compiler = new TrafficMapCompiler();
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> compiler.compile(definition("sha-546", "wrong", true)));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> compiler.compile(definition("sha-546", "sha-546", false)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void enforcesNodeEdgeAndConfiguredCapacity() {
|
||||
EffectiveTrafficMap map = new TrafficMapCompiler().compile(definition("sha-546", "sha-546", true));
|
||||
ResourceLedger ledger = new ResourceLedger(map.getCapacities());
|
||||
|
||||
assertTrue(ledger.evaluate("V1", Arrays.asList(ResourceKey.node("B"), ResourceKey.edge("A", "B"))).isGranted());
|
||||
ResourceLedger.ProtectionResult nodeConflict =
|
||||
ledger.evaluate("V2", Collections.singleton(ResourceKey.node("B")));
|
||||
ResourceLedger.ProtectionResult oppositeEdge =
|
||||
ledger.evaluate("V3", Collections.singleton(ResourceKey.edge("B", "A")));
|
||||
|
||||
assertFalse(nodeConflict.isGranted());
|
||||
assertEquals(Collections.singleton("V1"), nodeConflict.getBlockingVehicles());
|
||||
assertFalse(oppositeEdge.isGranted());
|
||||
|
||||
assertTrue(ledger.evaluate("V4", Collections.singleton(ResourceKey.conflict("WIDE"))).isGranted());
|
||||
assertTrue(ledger.evaluate("V5", Collections.singleton(ResourceKey.conflict("WIDE"))).isGranted());
|
||||
assertFalse(ledger.evaluate("V6", Collections.singleton(ResourceKey.conflict("WIDE"))).isGranted());
|
||||
}
|
||||
|
||||
private TrafficMapCompiler.MapDefinition definition(
|
||||
String baseChecksum, String configuredChecksum, boolean confirmed) {
|
||||
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"));
|
||||
Map<ResourceKey, Integer> capacities = new LinkedHashMap<ResourceKey, Integer>();
|
||||
capacities.put(ResourceKey.conflict("WIDE"), 2);
|
||||
return new TrafficMapCompiler.MapDefinition(
|
||||
"FT546", 2L, "ZONE-1", confirmed, baseChecksum, configuredChecksum,
|
||||
nodes, edges, capacities,
|
||||
Collections.singleton("A"), Collections.singleton("C"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user