feat: model shadow traffic resources

This commit is contained in:
zgw
2026-07-30 13:40:14 +08:00
parent d691b55e4c
commit 0a5d3c5cea
5 changed files with 372 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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