feat: run deterministic full-chain history simulation

This commit is contained in:
zgw
2026-07-31 12:00:56 +08:00
parent aa71d6e67b
commit e5b988aec8
14 changed files with 1184 additions and 11 deletions

View File

@@ -0,0 +1,114 @@
package com.ft546.wcs.shadow.history.simulation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class HistoricalDispatchSimulationResult {
private final List<SimulationTaskResult> taskResults;
private final List<SimulationTickEvent> events;
private final SimulationInvariantSummary invariants;
private final int archiveCount;
private final int wmsDeliveryAttempts;
private final int wmsBusinessEffects;
private final long tickCount;
private final int acceleration;
HistoricalDispatchSimulationResult(List<SimulationTaskResult> taskResults,
List<SimulationTickEvent> events,
SimulationInvariantSummary invariants,
int archiveCount, int wmsDeliveryAttempts,
int wmsBusinessEffects, long tickCount,
int acceleration) {
this.taskResults = immutable(taskResults);
this.events = immutable(events);
this.invariants = invariants;
this.archiveCount = archiveCount;
this.wmsDeliveryAttempts = wmsDeliveryAttempts;
this.wmsBusinessEffects = wmsBusinessEffects;
this.tickCount = tickCount;
this.acceleration = acceleration;
}
public List<SimulationTaskResult> getTaskResults() { return taskResults; }
public List<SimulationTickEvent> getEvents() { return events; }
public SimulationInvariantSummary getInvariants() { return invariants; }
public int getArchiveCount() { return archiveCount; }
public int getWmsDeliveryAttempts() { return wmsDeliveryAttempts; }
public int getWmsBusinessEffects() { return wmsBusinessEffects; }
public long getTickCount() { return tickCount; }
public int getAcceleration() { return acceleration; }
public int getCompletedCount() { return count(SimulationTaskStatus.COMPLETED); }
public int getWaitingCount() { return count(SimulationTaskStatus.WAITING); }
public int getFailedCount() { return count(SimulationTaskStatus.FAILED); }
public int getReassignmentCount() {
int count = 0;
for (SimulationTaskResult result : taskResults) {
if (result.getStatus() == SimulationTaskStatus.COMPLETED
&& result.getSimulatedVehicleId() != null
&& !result.getSimulatedVehicleId().equals(result.getRecord().getAgvId())) count++;
}
return count;
}
public int getReplannedCount() {
int count = 0;
for (SimulationTaskResult result : taskResults) {
String historical = result.getRecord().getPathInfo() == null
? "" : result.getRecord().getPathInfo().replaceAll(",+$", "");
String planned = String.join(",", result.getPlannedPath());
if (result.getStatus() == SimulationTaskStatus.COMPLETED
&& !historical.equals(planned)) count++;
}
return count;
}
public int getRetryCount() {
int count = 0;
for (SimulationTaskResult result : taskResults) count += result.getRetryCount();
return count;
}
public Map<String, Integer> completedByVehicle() {
Map<String, Integer> counts = new LinkedHashMap<String, Integer>();
for (SimulationTaskResult result : taskResults) {
if (result.getStatus() != SimulationTaskStatus.COMPLETED) continue;
String vehicle = result.getSimulatedVehicleId();
counts.put(vehicle, counts.containsKey(vehicle) ? counts.get(vehicle) + 1 : 1);
}
return Collections.unmodifiableMap(counts);
}
public String canonicalView() {
List<SimulationTaskResult> ordered = new ArrayList<SimulationTaskResult>(taskResults);
Collections.sort(ordered, Comparator.comparing(SimulationTaskResult::getSimulationTaskId));
StringBuilder value = new StringBuilder();
for (SimulationTaskResult result : ordered) {
value.append(result.getSimulationTaskId()).append('|')
.append(result.getStatus()).append('|')
.append(result.getReason()).append('|')
.append(result.getSimulatedVehicleId()).append('|')
.append(result.getPlannedPath()).append('|')
.append(result.getHoldCount()).append('|')
.append(result.getProtocolAttempts()).append('\n');
}
return value.toString();
}
private int count(SimulationTaskStatus status) {
int count = 0;
for (SimulationTaskResult result : taskResults) {
if (result.getStatus() == status) count++;
}
return count;
}
private static <T> List<T> immutable(List<T> values) {
return Collections.unmodifiableList(new ArrayList<T>(values));
}
}

View File

@@ -0,0 +1,542 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.center.VehicleAssignmentDecision;
import com.ft546.wcs.demo.center.VehicleAssignmentService;
import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.demo.model.Site;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import com.ft546.wcs.demo.traffic.GridPathPlanner;
import com.ft546.wcs.shadow.application.ShadowTrafficCoordinator;
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.replay.DecisionEvent;
import com.ft546.wcs.shadow.resource.ResourceKey;
import com.ft546.wcs.shadow.resource.ResourceLedger;
import com.ft546.wcs.shadow.snapshot.TrafficSnapshot;
import com.ft546.wcs.shadow.snapshot.TrafficVehicleSnapshot;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class HistoricalDispatchSimulator {
private static final long STEP_MILLIS = 1_000L;
private static final int HOLD_REDISPATCH_THRESHOLD = 10;
public HistoricalDispatchSimulationResult simulate(
HistoricalSimulationInput input, SimulationConfig config,
DeterministicFaultPlan faults) {
if (input == null || config == null || faults == null) {
throw new IllegalArgumentException("input, config and fault plan are required");
}
List<java.time.LocalDateTime> arrivalDates = new ArrayList<java.time.LocalDateTime>();
for (HistoricalSimulationTask task : input.getTasks()) arrivalDates.add(task.getCreatedAt());
if (arrivalDates.isEmpty()) {
throw new IllegalArgumentException("at least one schedulable task is required");
}
SimulationClock clock = new SimulationClock(arrivalDates, config.getAcceleration());
SimulationTaskQueue queue = new SimulationTaskQueue(config.getAgingIntervalMillis());
Map<String, Execution> executions = new LinkedHashMap<String, Execution>();
for (HistoricalSimulationTask task : input.getTasks()) {
long arrival = clock.toSimulationMillis(task.getCreatedAt());
queue.add(task, arrival);
executions.put(task.getSimulationTaskId(), new Execution(task, arrival));
}
List<SimulationTickEvent> events = new ArrayList<SimulationTickEvent>();
Map<String, SimulationTaskResult> terminal =
new LinkedHashMap<String, SimulationTaskResult>();
for (HistoricalSimulationFailure failure : input.getFailedInputs()) {
terminal.put(failure.getSimulationTaskId(), new SimulationTaskResult(
failure.getSimulationTaskId(), failure.getRecord(),
SimulationTaskStatus.FAILED, failure.getReason(), null,
Collections.emptyList(), Collections.emptyList(), 0L,
null, null, 0, 0, 0,
Collections.emptyMap(), Collections.emptyMap()));
}
Map<String, SimulatedVehicleRuntime> vehicles = createRuntimes(input);
Map<String, ActiveTask> active = new LinkedHashMap<String, ActiveTask>();
ResourceLedger ledger = new ResourceLedger(input.getTrafficMap().getCapacities());
SimulationInvariantSummary invariants = new SimulationInvariantSummary();
occupyInitialNodes(vehicles, ledger, invariants);
SimulatedPlcGateway plc = new SimulatedPlcGateway(faults);
SimulationArchive archive = new SimulationArchive();
IdempotentWmsCompletionReceiver wms = new IdempotentWmsCompletionReceiver();
GridPathPlanner planner = new GridPathPlanner();
VehicleAssignmentService assignment = new VehicleAssignmentService();
long now = 0L;
long tick = 0L;
int expected = input.getTasks().size() + input.getFailedInputs().size();
SimulationProgressWatchdog watchdog = new SimulationProgressWatchdog(
config.getNoProgressLimit(), terminal.size());
while (terminal.size() < expected && tick < config.getTickLimit()) {
applyVehicleEvents(tick, now, faults, vehicles, active,
queue, executions, events);
assignReadyTasks(tick, now, input, queue, vehicles, active,
executions, terminal, planner, assignment, plc, events);
Set<String> traversedEdges = new LinkedHashSet<String>();
advanceActiveTasks(tick, now, input, config, queue, vehicles,
active, executions, terminal, planner, ledger, plc, archive, wms,
invariants, events, traversedEdges);
verifyLedger(ledger, invariants);
if (watchdog.observe(terminal.size())) {
invariants.deadlock();
snapshotWaiting(queue, active, executions, terminal, plc,
"DEADLOCK_TIMEOUT", now);
break;
}
now += STEP_MILLIS;
tick++;
}
if (terminal.size() < expected) {
snapshotWaiting(queue, active, executions, terminal, plc,
"TICK_LIMIT", now);
}
List<SimulationTaskResult> results = new ArrayList<SimulationTaskResult>(terminal.values());
Collections.sort(results, Comparator.comparing(SimulationTaskResult::getSimulationTaskId));
return new HistoricalDispatchSimulationResult(results, events, invariants,
archive.size(), wms.getDeliveryAttempts(), wms.getBusinessEffects(),
tick, config.getAcceleration());
}
private Map<String, SimulatedVehicleRuntime> createRuntimes(HistoricalSimulationInput input) {
Map<String, SimulatedVehicleRuntime> runtimes =
new LinkedHashMap<String, SimulatedVehicleRuntime>();
for (VehicleSnapshot snapshot : input.getInitialVehicles()) {
runtimes.put(snapshot.getVehicleId(), SimulatedVehicleRuntime.from(snapshot));
}
return runtimes;
}
private void occupyInitialNodes(Map<String, SimulatedVehicleRuntime> vehicles,
ResourceLedger ledger,
SimulationInvariantSummary invariants) {
for (SimulatedVehicleRuntime vehicle : vehicles.values()) {
ResourceLedger.ProtectionResult result = ledger.evaluate(vehicle.getVehicleId(),
Collections.singletonList(ResourceKey.node(vehicle.getCurrentSite())));
if (!result.isGranted()) {
invariants.capacityViolation();
invariants.nodeDoubleOccupancyViolation();
}
}
}
private boolean applyVehicleEvents(long tick, long now, DeterministicFaultPlan faults,
Map<String, SimulatedVehicleRuntime> vehicles,
Map<String, ActiveTask> active,
SimulationTaskQueue queue,
Map<String, Execution> executions,
List<SimulationTickEvent> events) {
boolean changed = false;
for (DeterministicFaultPlan.VehicleEvent event : faults.eventsAt(tick)) {
SimulatedVehicleRuntime vehicle = vehicles.get(event.getVehicleId());
if (vehicle == null) throw new IllegalArgumentException("unknown fault vehicle");
switch (event.getType()) {
case DISCONNECT: vehicle.disconnect(); break;
case CONNECT: vehicle.connect(); break;
case RESTART: vehicle.restart(); break;
case SYNCHRONIZE: vehicle.synchronizeAfterRestart(); break;
case LOW_BATTERY: vehicle.setBattery(29); break;
case RESTORE_BATTERY: vehicle.setBattery(100); break;
case FAULT: vehicle.fail(); break;
case RECOVER: vehicle.recover(); break;
default: throw new IllegalStateException("unsupported vehicle event");
}
events.add(event(tick, now, "VEHICLE_STATE", null, vehicle.getVehicleId(),
event.getType().name(), vehicle.getCurrentSite(), vehicle.getCurrentSite()));
changed = true;
if (!vehicle.isAssignmentEligible()) {
ActiveTask stopped = active.remove(vehicle.getVehicleId());
if (stopped != null) {
queue.requeue(stopped.execution.task);
hold(stopped.execution, event.getType().name());
events.add(event(tick, now, "HOLD", stopped.execution.task.getSimulationTaskId(),
vehicle.getVehicleId(), event.getType().name(),
vehicle.getCurrentSite(), null));
}
}
}
return changed;
}
private boolean assignReadyTasks(long tick, long now, HistoricalSimulationInput input,
SimulationTaskQueue queue,
Map<String, SimulatedVehicleRuntime> vehicles,
Map<String, ActiveTask> active,
Map<String, Execution> executions,
Map<String, SimulationTaskResult> terminal,
GridPathPlanner planner,
VehicleAssignmentService assignment,
SimulatedPlcGateway plc,
List<SimulationTickEvent> events) {
boolean progress = false;
List<HistoricalSimulationTask> ready = new ArrayList<HistoricalSimulationTask>(
queue.readyAt(now));
for (HistoricalSimulationTask task : ready) {
if (terminal.containsKey(task.getSimulationTaskId())) continue;
List<String> servicePath = planner.shortestPath(input.getGridMap(),
task.getPhysicalSource(), task.getPhysicalTarget(), Collections.emptySet());
if (servicePath.isEmpty()) {
queue.remove(task.getSimulationTaskId());
terminal.put(task.getSimulationTaskId(), result(executions.get(
task.getSimulationTaskId()), SimulationTaskStatus.FAILED,
"NO_ROUTE", plc, now));
progress = true;
continue;
}
List<VehicleSnapshot> candidates = new ArrayList<VehicleSnapshot>();
Map<String, List<String>> approaches = new LinkedHashMap<String, List<String>>();
for (SimulatedVehicleRuntime vehicle : vehicles.values()) {
if (!vehicle.isAssignmentEligible() || active.containsKey(vehicle.getVehicleId())) {
continue;
}
List<String> approach = planner.shortestPath(input.getGridMap(),
vehicle.getCurrentSite(), task.getPhysicalSource(), Collections.emptySet());
if (!approach.isEmpty()) {
candidates.add(vehicle.snapshot());
approaches.put(vehicle.getVehicleId(), approach);
}
}
if (candidates.isEmpty()) {
if (!fleetCanReach(task, vehicles, input, planner)) {
queue.remove(task.getSimulationTaskId());
terminal.put(task.getSimulationTaskId(), result(executions.get(
task.getSimulationTaskId()), SimulationTaskStatus.FAILED,
"NO_ROUTE", plc, now));
progress = true;
}
continue;
}
Site source = input.getGridMap().getSite(task.getPhysicalSource());
DispatchTask dispatchTask = new DispatchTask(task.getSimulationTaskId(),
task.getTaskType(), task.getPhysicalSource(), task.getPhysicalTarget(),
source.getFloor(), task.getPriority());
VehicleAssignmentDecision decision = assignment.select(
dispatchTask, candidates, input.getGridMap().getSites());
if (!decision.getVehicle().isPresent()) continue;
String vehicleId = decision.getVehicle().get().getVehicleId();
List<String> route = new ArrayList<String>(approaches.get(vehicleId));
for (int i = 1; i < servicePath.size(); i++) route.add(servicePath.get(i));
Execution execution = executions.get(task.getSimulationTaskId());
if (hasOppositeRouteConflict(route, active.values())) {
hold(execution, "OPPOSITE_ROUTE_CONFLICT");
events.add(event(tick, now, "HOLD", task.getSimulationTaskId(), vehicleId,
"OPPOSITE_ROUTE_CONFLICT", route.get(0), route.get(route.size() - 1)));
continue;
}
if (execution.firstAssignedMillis == null) execution.firstAssignedMillis = now;
execution.lastVehicleId = vehicleId;
execution.plannedPath = new ArrayList<String>(servicePath);
execution.assignedVehicles.add(vehicleId);
active.put(vehicleId, new ActiveTask(execution, route));
queue.remove(task.getSimulationTaskId());
events.add(event(tick, now, "ASSIGN", task.getSimulationTaskId(), vehicleId,
decision.getReason(), route.get(0), route.get(route.size() - 1)));
progress = true;
}
return progress;
}
private boolean hasOppositeRouteConflict(List<String> proposed,
Iterable<ActiveTask> activeTasks) {
Set<String> directions = directedEdges(proposed, 0);
for (ActiveTask activeTask : activeTasks) {
Set<String> activeDirections = directedEdges(activeTask.route, activeTask.index);
for (String direction : directions) {
String[] pair = direction.split("->", -1);
if (activeDirections.contains(pair[1] + "->" + pair[0])) return true;
}
}
return false;
}
private Set<String> directedEdges(List<String> path, int startIndex) {
Set<String> edges = new LinkedHashSet<String>();
for (int i = Math.max(1, startIndex + 1); i < path.size(); i++) {
edges.add(path.get(i - 1) + "->" + path.get(i));
}
return edges;
}
private boolean fleetCanReach(HistoricalSimulationTask task,
Map<String, SimulatedVehicleRuntime> vehicles,
HistoricalSimulationInput input,
GridPathPlanner planner) {
for (SimulatedVehicleRuntime vehicle : vehicles.values()) {
if (!planner.shortestPath(input.getGridMap(), vehicle.getCurrentSite(),
task.getPhysicalSource(), Collections.emptySet()).isEmpty()) return true;
}
return false;
}
private boolean advanceActiveTasks(long tick, long now,
HistoricalSimulationInput input,
SimulationConfig config,
SimulationTaskQueue queue,
Map<String, SimulatedVehicleRuntime> vehicles,
Map<String, ActiveTask> active,
Map<String, Execution> executions,
Map<String, SimulationTaskResult> terminal,
GridPathPlanner planner,
ResourceLedger ledger,
SimulatedPlcGateway plc,
SimulationArchive archive,
IdempotentWmsCompletionReceiver wms,
SimulationInvariantSummary invariants,
List<SimulationTickEvent> events,
Set<String> traversedEdges) {
boolean progress = false;
List<String> vehicleIds = new ArrayList<String>(active.keySet());
Collections.sort(vehicleIds);
for (String vehicleId : vehicleIds) {
ActiveTask current = active.get(vehicleId);
if (current == null) continue;
SimulatedVehicleRuntime vehicle = vehicles.get(vehicleId);
if (!vehicle.isAssignmentEligible()) continue;
if (current.index >= current.route.size() - 1) {
complete(tick, now, current, vehicle, active, terminal, plc,
archive, wms, invariants, events);
progress = true;
continue;
}
String from = current.route.get(current.index);
String to = current.route.get(current.index + 1);
ResourceKey edge = ResourceKey.edge(from, to);
String direction = from + "->" + to;
String reverse = to + "->" + from;
if (traversedEdges.contains(reverse) || traversedEdges.contains(direction)) {
recordHold(tick, now, current, vehicle, active, queue,
"OPPOSITE_EDGE_WINDOW", events);
continue;
}
List<ResourceKey> resources = Arrays.asList(
ResourceKey.node(from), edge, ResourceKey.node(to));
Instant decisionTime = Instant.EPOCH.plusMillis(now + tick);
TrafficVehicleSnapshot frozen = new TrafficVehicleSnapshot(vehicleId,
Long.toString(vehicle.getBootId()), tick + 1, tick + 1,
decisionTime, from, to,
current.route.subList(current.index, current.route.size()),
true, false, false);
TrafficSnapshot snapshot = new TrafficSnapshot(tick + 1, decisionTime,
input.getTrafficMap().getMapCode(), input.getTrafficMap().getVersionId(),
"history-sim-v1", Collections.singletonList(frozen));
int effectivePriority = queuePriority(current.execution, now, config);
ShadowCandidate candidate = ShadowCandidate.waiting(
current.execution.task.getSimulationTaskId(), vehicleId, false,
-effectivePriority, now - current.execution.arrivalMillis,
current.route.size() - current.index, resources, null);
ShadowTrafficCoordinator coordinator = new ShadowTrafficCoordinator(
input.getTrafficMap(), ledger, new RollingDispatchPolicy());
DecisionEvent decisionEvent = coordinator.run(snapshot,
Collections.singletonList(candidate));
ShadowDecision decision = decisionEvent.getDecisions().get(0);
if (decision.getType() != DecisionType.GRANT) {
recordHold(tick, now, current, vehicle, active, queue,
decision.getReasonCode(), events);
continue;
}
ResourceLedger.ProtectionResult protection = ledger.evaluate(vehicleId, resources);
if (!protection.isGranted()) {
invariants.capacityViolation();
recordHold(tick, now, current, vehicle, active, queue,
"AUTHORITATIVE_RESOURCE_REJECTED", events);
continue;
}
SimulatedPlcGateway.PlcResponse sent = plc.send(
current.execution.task.getSimulationTaskId(), vehicle, from, to);
SimulatedPlcGateway.PlcResponse accepted = plc.accept(sent, vehicle);
if (accepted.getReason() != SimulatedPlcGateway.PlcReason.ACK) {
ledger.release(vehicleId, Arrays.asList(edge, ResourceKey.node(to)));
retry(current.execution, accepted.getReason().name());
events.add(event(tick, now, "PLC_RETRY",
current.execution.task.getSimulationTaskId(), vehicleId,
accepted.getReason().name(), from, to));
if (plc.attemptsFor(current.execution.task.getSimulationTaskId())
% config.getRetryLimit() == 0) {
active.remove(vehicleId);
queue.requeue(current.execution.task);
hold(current.execution, "RETRY_EXHAUSTED_REDISPATCH");
events.add(event(tick, now, "HOLD",
current.execution.task.getSimulationTaskId(), vehicleId,
"RETRY_EXHAUSTED_REDISPATCH", from, to));
}
progress = true;
continue;
}
Site nextSite = input.getGridMap().getSite(to);
vehicle.moveTo(to, nextSite.getFloor());
ledger.release(vehicleId, Arrays.asList(ResourceKey.node(from), edge));
traversedEdges.add(direction);
current.index++;
current.holdStreak = 0;
events.add(event(tick, now, "MOVE",
current.execution.task.getSimulationTaskId(), vehicleId,
"ACK", from, to));
progress = true;
if (current.index >= current.route.size() - 1) {
complete(tick, now, current, vehicle, active, terminal, plc,
archive, wms, invariants, events);
}
}
return progress;
}
private int queuePriority(Execution execution, long now, SimulationConfig config) {
long waited = Math.max(0L, now - execution.arrivalMillis);
return (int) Math.max(0L, (long) execution.task.getPriority()
- waited / config.getAgingIntervalMillis());
}
private void recordHold(long tick, long now, ActiveTask current,
SimulatedVehicleRuntime vehicle,
Map<String, ActiveTask> active,
SimulationTaskQueue queue, String reason,
List<SimulationTickEvent> events) {
hold(current.execution, reason);
current.holdStreak++;
events.add(event(tick, now, "HOLD", current.execution.task.getSimulationTaskId(),
vehicle.getVehicleId(), reason, vehicle.getCurrentSite(), null));
if (current.holdStreak >= HOLD_REDISPATCH_THRESHOLD) {
active.remove(vehicle.getVehicleId());
queue.requeue(current.execution.task);
events.add(event(tick, now, "REDISPATCH",
current.execution.task.getSimulationTaskId(), vehicle.getVehicleId(),
reason, vehicle.getCurrentSite(), null));
}
}
private void complete(long tick, long now, ActiveTask activeTask,
SimulatedVehicleRuntime vehicle,
Map<String, ActiveTask> active,
Map<String, SimulationTaskResult> terminal,
SimulatedPlcGateway plc,
SimulationArchive archive,
IdempotentWmsCompletionReceiver wms,
SimulationInvariantSummary invariants,
List<SimulationTickEvent> events) {
String taskId = activeTask.execution.task.getSimulationTaskId();
if (!archive.complete(taskId, vehicle.getVehicleId(), now)) {
invariants.duplicateArchiveAttempt();
}
boolean first = wms.receive(taskId);
boolean duplicate = wms.receive(taskId);
if (!first || duplicate) invariants.duplicateWmsBusinessEffect();
terminal.put(taskId, result(activeTask.execution,
SimulationTaskStatus.COMPLETED, "COMPLETED", plc, now));
active.remove(vehicle.getVehicleId());
events.add(event(tick, now, "COMPLETE", taskId, vehicle.getVehicleId(),
"ARCHIVED_WMS_ACK", vehicle.getCurrentSite(), vehicle.getCurrentSite()));
}
private void snapshotWaiting(SimulationTaskQueue queue,
Map<String, ActiveTask> active,
Map<String, Execution> executions,
Map<String, SimulationTaskResult> terminal,
SimulatedPlcGateway plc, String reason, long now) {
for (HistoricalSimulationTask task : queue.readyAt(Long.MAX_VALUE)) {
if (!terminal.containsKey(task.getSimulationTaskId())) {
terminal.put(task.getSimulationTaskId(), result(executions.get(
task.getSimulationTaskId()), SimulationTaskStatus.WAITING,
reason, plc, now));
}
}
for (ActiveTask task : active.values()) {
String id = task.execution.task.getSimulationTaskId();
if (!terminal.containsKey(id)) {
terminal.put(id, result(task.execution,
SimulationTaskStatus.WAITING, reason, plc, now));
}
}
}
private SimulationTaskResult result(Execution execution, SimulationTaskStatus status,
String reason, SimulatedPlcGateway plc, long now) {
return new SimulationTaskResult(execution.task.getSimulationTaskId(),
execution.task.getRecord(), status, reason, execution.lastVehicleId,
execution.plannedPath, execution.assignedVehicles,
execution.arrivalMillis, execution.firstAssignedMillis,
status == SimulationTaskStatus.COMPLETED ? now : null,
execution.holdCount,
plc.attemptsFor(execution.task.getSimulationTaskId()),
execution.retryCount, execution.holdReasons, execution.retryReasons);
}
private void verifyLedger(ResourceLedger ledger, SimulationInvariantSummary invariants) {
for (Map.Entry<ResourceKey, Set<String>> entry : ledger.snapshotHolders().entrySet()) {
Integer capacity = ledger.getCapacities().get(entry.getKey());
if (capacity != null && entry.getValue().size() > capacity) {
invariants.capacityViolation();
if (entry.getKey().value().startsWith("NODE:")) {
invariants.nodeDoubleOccupancyViolation();
}
}
}
}
private static void hold(Execution execution, String reason) {
execution.holdCount++;
increment(execution.holdReasons, reason);
}
private static void retry(Execution execution, String reason) {
execution.retryCount++;
increment(execution.retryReasons, reason);
}
private static void increment(Map<String, Integer> values, String key) {
values.put(key, values.containsKey(key) ? values.get(key) + 1 : 1);
}
private SimulationTickEvent event(long tick, long now, String type,
String taskId, String vehicleId, String reason,
String from, String to) {
return new SimulationTickEvent(tick, now, type, taskId, vehicleId, reason, from, to);
}
private static final class Execution {
private final HistoricalSimulationTask task;
private final long arrivalMillis;
private Long firstAssignedMillis;
private String lastVehicleId;
private List<String> plannedPath = new ArrayList<String>();
private final List<String> assignedVehicles = new ArrayList<String>();
private int holdCount;
private int retryCount;
private final Map<String, Integer> holdReasons =
new LinkedHashMap<String, Integer>();
private final Map<String, Integer> retryReasons =
new LinkedHashMap<String, Integer>();
private Execution(HistoricalSimulationTask task, long arrivalMillis) {
this.task = task;
this.arrivalMillis = arrivalMillis;
}
}
private static final class ActiveTask {
private final Execution execution;
private final List<String> route;
private int index;
private int holdStreak;
private ActiveTask(Execution execution, List<String> route) {
this.execution = execution;
this.route = Collections.unmodifiableList(new ArrayList<String>(route));
}
}
}

View File

@@ -0,0 +1,29 @@
package com.ft546.wcs.shadow.history.simulation;
public final class SimulationInvariantSummary {
private int capacityViolations;
private int nodeDoubleOccupancyViolations;
private int oppositeEdgeViolations;
private int duplicateArchiveAttempts;
private int duplicateWmsBusinessEffects;
private int deadlocks;
void capacityViolation() { capacityViolations++; }
void nodeDoubleOccupancyViolation() { nodeDoubleOccupancyViolations++; }
void oppositeEdgeViolation() { oppositeEdgeViolations++; }
void duplicateArchiveAttempt() { duplicateArchiveAttempts++; }
void duplicateWmsBusinessEffect() { duplicateWmsBusinessEffects++; }
void deadlock() { deadlocks++; }
public int getCapacityViolations() { return capacityViolations; }
public int getNodeDoubleOccupancyViolations() { return nodeDoubleOccupancyViolations; }
public int getOppositeEdgeViolations() { return oppositeEdgeViolations; }
public int getDuplicateArchiveAttempts() { return duplicateArchiveAttempts; }
public int getDuplicateWmsBusinessEffects() { return duplicateWmsBusinessEffects; }
public int getDeadlocks() { return deadlocks; }
public int totalSafetyViolations() {
return capacityViolations + nodeDoubleOccupancyViolations
+ oppositeEdgeViolations + duplicateWmsBusinessEffects;
}
}

View File

@@ -0,0 +1,28 @@
package com.ft546.wcs.shadow.history.simulation;
public final class SimulationProgressWatchdog {
private final int unchangedLimit;
private int lastTerminalCount;
private int unchangedEvents;
public SimulationProgressWatchdog(int unchangedLimit, int initialTerminalCount) {
if (unchangedLimit <= 0 || initialTerminalCount < 0) {
throw new IllegalArgumentException("valid progress limits are required");
}
this.unchangedLimit = unchangedLimit;
this.lastTerminalCount = initialTerminalCount;
}
public boolean observe(int terminalCount) {
if (terminalCount < lastTerminalCount) {
throw new IllegalArgumentException("terminal count must be monotonic");
}
if (terminalCount > lastTerminalCount) {
lastTerminalCount = terminalCount;
unchangedEvents = 0;
return false;
}
unchangedEvents++;
return unchangedEvents >= unchangedLimit;
}
}

View File

@@ -61,6 +61,18 @@ public final class SimulationTaskQueue {
public boolean isEmpty() { return entries.isEmpty(); }
public int size() { return entries.size(); }
public long arrivalMillis(String simulationTaskId) {
Long value = originalArrivals.get(simulationTaskId);
if (value == null) throw new IllegalArgumentException("task was never admitted");
return value;
}
public int effectivePriorityFor(HistoricalSimulationTask task, long nowMillis) {
Long arrival = originalArrivals.get(task.getSimulationTaskId());
if (arrival == null) throw new IllegalArgumentException("task was never admitted");
return effectivePriority(new Entry(task, arrival), nowMillis);
}
private int effectivePriority(Entry entry, long nowMillis) {
long waited = Math.max(0L, nowMillis - entry.arrivalMillis);
long improvement = waited / agingIntervalMillis;

View File

@@ -0,0 +1,82 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class SimulationTaskResult {
private final String simulationTaskId;
private final HistoricalQueueRecord record;
private final SimulationTaskStatus status;
private final String reason;
private final String simulatedVehicleId;
private final List<String> plannedPath;
private final List<String> assignedVehicles;
private final long arrivalMillis;
private final Long firstAssignedMillis;
private final Long completedMillis;
private final int holdCount;
private final int protocolAttempts;
private final int retryCount;
private final Map<String, Integer> holdReasons;
private final Map<String, Integer> retryReasons;
SimulationTaskResult(String simulationTaskId, HistoricalQueueRecord record,
SimulationTaskStatus status, String reason,
String simulatedVehicleId, List<String> plannedPath,
List<String> assignedVehicles, long arrivalMillis,
Long firstAssignedMillis, Long completedMillis,
int holdCount, int protocolAttempts, int retryCount,
Map<String, Integer> holdReasons,
Map<String, Integer> retryReasons) {
this.simulationTaskId = simulationTaskId;
this.record = record;
this.status = status;
this.reason = reason;
this.simulatedVehicleId = simulatedVehicleId;
this.plannedPath = immutable(plannedPath);
this.assignedVehicles = immutable(assignedVehicles);
this.arrivalMillis = arrivalMillis;
this.firstAssignedMillis = firstAssignedMillis;
this.completedMillis = completedMillis;
this.holdCount = holdCount;
this.protocolAttempts = protocolAttempts;
this.retryCount = retryCount;
this.holdReasons = immutableMap(holdReasons);
this.retryReasons = immutableMap(retryReasons);
}
public String getSimulationTaskId() { return simulationTaskId; }
public HistoricalQueueRecord getRecord() { return record; }
public SimulationTaskStatus getStatus() { return status; }
public String getReason() { return reason; }
public String getSimulatedVehicleId() { return simulatedVehicleId; }
public List<String> getPlannedPath() { return plannedPath; }
public List<String> getAssignedVehicles() { return assignedVehicles; }
public long getArrivalMillis() { return arrivalMillis; }
public Long getFirstAssignedMillis() { return firstAssignedMillis; }
public Long getCompletedMillis() { return completedMillis; }
public int getHoldCount() { return holdCount; }
public int getProtocolAttempts() { return protocolAttempts; }
public int getRetryCount() { return retryCount; }
public Map<String, Integer> getHoldReasons() { return holdReasons; }
public Map<String, Integer> getRetryReasons() { return retryReasons; }
public Long getWaitingMillis() {
return firstAssignedMillis == null ? null : firstAssignedMillis - arrivalMillis;
}
public Long getCompletionMillis() {
return completedMillis == null ? null : completedMillis - arrivalMillis;
}
private static <T> List<T> immutable(List<T> values) {
return Collections.unmodifiableList(new ArrayList<T>(values));
}
private static Map<String, Integer> immutableMap(Map<String, Integer> values) {
return Collections.unmodifiableMap(new LinkedHashMap<String, Integer>(values));
}
}

View File

@@ -0,0 +1,7 @@
package com.ft546.wcs.shadow.history.simulation;
public enum SimulationTaskStatus {
COMPLETED,
WAITING,
FAILED
}

View File

@@ -0,0 +1,34 @@
package com.ft546.wcs.shadow.history.simulation;
public final class SimulationTickEvent {
private final long tick;
private final long simulationMillis;
private final String type;
private final String taskId;
private final String vehicleId;
private final String reason;
private final String fromSite;
private final String toSite;
public SimulationTickEvent(long tick, long simulationMillis, String type,
String taskId, String vehicleId, String reason,
String fromSite, String toSite) {
this.tick = tick;
this.simulationMillis = simulationMillis;
this.type = type;
this.taskId = taskId;
this.vehicleId = vehicleId;
this.reason = reason;
this.fromSite = fromSite;
this.toSite = toSite;
}
public long getTick() { return tick; }
public long getSimulationMillis() { return simulationMillis; }
public String getType() { return type; }
public String getTaskId() { return taskId; }
public String getVehicleId() { return vehicleId; }
public String getReason() { return reason; }
public String getFromSite() { return fromSite; }
public String getToSite() { return toSite; }
}

View File

@@ -0,0 +1,60 @@
package com.ft546.wcs.shadow.history.simulation;
import org.junit.jupiter.api.Test;
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.CONNECT;
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.DISCONNECT;
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.FAULT;
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.LOW_BATTERY;
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.RECOVER;
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.RESTART;
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.RESTORE_BATTERY;
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.SYNCHRONIZE;
import static com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway.PlcReason.ACK_LOST;
import static com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway.PlcReason.ACK_TIMEOUT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HistoricalDispatchFaultMatrixTest {
@Test
void recoversFromDisconnectRestartLowBatteryAndVehicleFault() {
DeterministicFaultPlan faults = DeterministicFaultPlan.builder()
.vehicleEvent(0, "1", DISCONNECT)
.vehicleEvent(0, "2", LOW_BATTERY)
.vehicleEvent(1, "1", CONNECT)
.vehicleEvent(1, "2", RESTORE_BATTERY)
.vehicleEvent(2, "1", RESTART)
.vehicleEvent(2, "2", FAULT)
.vehicleEvent(3, "1", SYNCHRONIZE)
.vehicleEvent(3, "2", RECOVER)
.build();
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
HistoricalDispatchSimulatorTest.controlledInput(),
SimulationConfig.defaults(), faults);
assertEquals(3, result.getCompletedCount());
assertEquals(0, result.getWaitingCount());
assertEquals(0, result.getInvariants().totalSafetyViolations());
assertTrue(result.getEvents().stream()
.filter(event -> "VEHICLE_STATE".equals(event.getType())).count() >= 8);
assertTrue(result.getEvents().stream().anyMatch(event ->
"REDISPATCH".equals(event.getType()) || "HOLD".equals(event.getType())));
}
@Test
void retriesAckTimeoutAndLossBeforeMoving() {
DeterministicFaultPlan faults = DeterministicFaultPlan.builder()
.plcFailure("T1", 1, ACK_TIMEOUT)
.plcFailure("T1", 2, ACK_LOST)
.build();
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
HistoricalDispatchSimulatorTest.controlledInput(),
SimulationConfig.defaults(), faults);
assertEquals(3, result.getCompletedCount());
assertTrue(result.getRetryCount() >= 2);
assertEquals(0, result.getInvariants().totalSafetyViolations());
}
}

View File

@@ -0,0 +1,68 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway.PlcReason.SEND_FAILURE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HistoricalDispatchSimulationRealDataTest {
private static HistoricalDispatchSimulationResult result;
private static HistoricalDispatchSimulationResult secondRun;
@BeforeAll
static void simulateRealHistoryTwice() {
HistoricalQueueDataset dataset = new HistoricalQueueLoader().load(
HistoricalDispatchSimulationRealDataTest.class.getClassLoader(), "shadow/history");
HistoricalSimulationInput input = new HistoricalSimulationInputFactory().create(dataset);
DeterministicFaultPlan.Builder faults = DeterministicFaultPlan.builder();
for (int i = 0; i < input.getTasks().size(); i += 37) {
faults.plcFailure(input.getTasks().get(i).getSimulationTaskId(), 1, SEND_FAILURE);
}
DeterministicFaultPlan plan = faults.build();
HistoricalDispatchSimulator simulator = new HistoricalDispatchSimulator();
result = simulator.simulate(input, SimulationConfig.defaults(), plan);
secondRun = simulator.simulate(input, SimulationConfig.defaults(), plan);
}
static Stream<SimulationTaskResult> realTaskResults() {
return result.getTaskResults().stream();
}
@ParameterizedTest(name = "{index} {0}")
@MethodSource("realTaskResults")
void everyHistoryRowHasOneAuditableOutcome(SimulationTaskResult task) {
assertNotNull(task.getStatus());
assertFalse(task.getSimulationTaskId().isEmpty());
assertNotNull(task.getRecord().getTaskNo());
assertNotNull(task.getRecord().getStartSite());
assertNotNull(task.getRecord().getEndSite());
}
@Test
void completesTheRealWorkloadDeterministicallyWithoutSafetyViolations() {
assertEquals(240, result.getTaskResults().size());
assertEquals(240, result.getCompletedCount()
+ result.getWaitingCount() + result.getFailedCount());
assertTrue(result.getCompletedCount() > 0);
if (result.getWaitingCount() > 0) {
assertTrue(result.getInvariants().getDeadlocks() > 0);
}
assertTrue(result.getReassignmentCount() > 0);
assertTrue(result.getRetryCount() > 0);
assertEquals(result.getCompletedCount(), result.getArchiveCount());
assertEquals(result.getCompletedCount(), result.getWmsBusinessEffects());
assertEquals(result.getCompletedCount() * 2, result.getWmsDeliveryAttempts());
assertEquals(0, result.getInvariants().totalSafetyViolations());
assertEquals(result.canonicalView(), secondRun.canonicalView());
}
}

View File

@@ -0,0 +1,172 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.demo.model.Site;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import com.ft546.wcs.demo.traffic.GridMap;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
import com.ft546.wcs.shadow.map.TrafficMapCompiler;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import static com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway.PlcReason.SEND_FAILURE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HistoricalDispatchSimulatorTest {
@Test
void runsMultipleTasksThroughArbitrationRetryMovementArchiveAndWms() {
HistoricalSimulationInput input = controlledInput();
DeterministicFaultPlan faults = DeterministicFaultPlan.builder()
.plcFailure("T1", 1, SEND_FAILURE).build();
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator()
.simulate(input, SimulationConfig.defaults(), faults);
assertEquals(3, result.getCompletedCount());
assertEquals(0, result.getWaitingCount());
assertEquals(0, result.getFailedCount());
assertTrue(result.getEvents().stream().anyMatch(event -> "HOLD".equals(event.getType())));
assertTrue(result.getEvents().stream()
.anyMatch(event -> "PLC_RETRY".equals(event.getType())));
assertTrue(result.getEvents().stream().anyMatch(event -> "MOVE".equals(event.getType())));
assertEquals(3, result.getArchiveCount());
assertEquals(6, result.getWmsDeliveryAttempts());
assertEquals(3, result.getWmsBusinessEffects());
assertEquals(0, result.getInvariants().totalSafetyViolations());
}
@Test
void holdsAnOppositeRouteAtTheCorridorEntranceBeforeItCanLivelock() {
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
oppositeCorridorInput(), new SimulationConfig(100, 30_000L, 3, 50, 100),
DeterministicFaultPlan.none());
assertEquals(2, result.getCompletedCount());
assertEquals(0, result.getWaitingCount());
assertTrue(result.getEvents().stream().anyMatch(event ->
"HOLD".equals(event.getType())
&& "OPPOSITE_ROUTE_CONFLICT".equals(event.getReason())));
}
@Test
void replansOnTheMapInsteadOfReturningTheHistoricalReferencePath() {
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
alternateRouteInput(), SimulationConfig.defaults(),
DeterministicFaultPlan.none());
assertEquals(Arrays.asList("A", "C"),
result.getTaskResults().get(0).getPlannedPath());
assertEquals(1, result.getReplannedCount());
}
static HistoricalSimulationInput controlledInput() {
GridMap grid = new GridMap();
for (String node : Arrays.asList("A", "B", "C", "D")) {
grid.addSite(new Site(node, 1, node.charAt(0) - 'A', 1, "TEST"));
}
grid.connect("A", "B");
grid.connect("B", "C");
grid.connect("B", "D");
LinkedHashSet<String> nodes = new LinkedHashSet<String>(Arrays.asList("A", "B", "C", "D"));
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges = new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "B"));
edges.add(new TrafficMapCompiler.EdgeDefinition("B", "C"));
edges.add(new TrafficMapCompiler.EdgeDefinition("B", "D"));
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
new TrafficMapCompiler.MapDefinition("TEST", 1L, "TEST", true,
"test", "test", nodes, edges, Collections.emptyMap(),
Collections.emptySet(), Collections.emptySet()));
List<HistoricalSimulationTask> tasks = new ArrayList<HistoricalSimulationTask>();
tasks.add(task("T1", "A", "C", 1, 0));
tasks.add(task("T2", "D", "A", 2, 0));
tasks.add(task("T3", "C", "D", 3, 1));
List<VehicleSnapshot> vehicles = Arrays.asList(
new VehicleSnapshot("1", 1, "A", 5, 1, 100, true, true, false),
new VehicleSnapshot("2", 1, "D", 5, 1, 100, true, true, false));
return new HistoricalSimulationInput(tasks, Collections.emptyList(), grid, traffic, vehicles);
}
private HistoricalSimulationInput oppositeCorridorInput() {
GridMap grid = new GridMap();
for (String node : Arrays.asList("A", "B", "C", "D", "E")) {
grid.addSite(new Site(node, 1, node.charAt(0) - 'A', 1, "TEST"));
}
grid.connect("A", "B");
grid.connect("B", "C");
grid.connect("C", "D");
grid.connect("C", "E");
LinkedHashSet<String> nodes = new LinkedHashSet<String>(
Arrays.asList("A", "B", "C", "D", "E"));
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "B"));
edges.add(new TrafficMapCompiler.EdgeDefinition("B", "C"));
edges.add(new TrafficMapCompiler.EdgeDefinition("C", "D"));
edges.add(new TrafficMapCompiler.EdgeDefinition("C", "E"));
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
new TrafficMapCompiler.MapDefinition("CORRIDOR", 1L, "TEST", true,
"test", "test", nodes, edges, Collections.emptyMap(),
Collections.emptySet(), Collections.emptySet()));
List<HistoricalSimulationTask> tasks = Arrays.asList(
customTask("C1", "A", "E", 1, 0, Arrays.asList("A", "B", "C", "E")),
customTask("C2", "D", "A", 2, 0, Arrays.asList("D", "C", "B", "A")));
List<VehicleSnapshot> vehicles = Arrays.asList(
new VehicleSnapshot("1", 1, "A", 5, 1, 100, true, true, false),
new VehicleSnapshot("2", 1, "D", 5, 1, 100, true, true, false));
return new HistoricalSimulationInput(tasks, Collections.emptyList(), grid, traffic, vehicles);
}
private HistoricalSimulationInput alternateRouteInput() {
GridMap grid = new GridMap();
for (String node : Arrays.asList("A", "B", "C")) {
grid.addSite(new Site(node, 1, node.charAt(0) - 'A', 1, "TEST"));
}
grid.connect("A", "B");
grid.connect("B", "C");
grid.connect("A", "C");
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"));
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "C"));
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
new TrafficMapCompiler.MapDefinition("ALTERNATE", 1L, "TEST", true,
"test", "test", nodes, edges, Collections.emptyMap(),
Collections.emptySet(), Collections.emptySet()));
HistoricalSimulationTask task = customTask(
"R1", "A", "C", 1, 0, Arrays.asList("A", "B", "C"));
return new HistoricalSimulationInput(Collections.singletonList(task),
Collections.emptyList(), grid, traffic,
Collections.singletonList(new VehicleSnapshot(
"1", 1, "A", 5, 1, 100, true, true, false)));
}
private static HistoricalSimulationTask task(String id, String source, String target,
int priority, int sequence) {
List<String> path = "T2".equals(id)
? Arrays.asList("D", "B", "A")
: "T3".equals(id) ? Arrays.asList("C", "B", "D")
: Arrays.asList("A", "B", "C");
return customTask(id, source, target, priority, sequence, path);
}
private static HistoricalSimulationTask customTask(
String id, String source, String target, int priority, int sequence,
List<String> path) {
LocalDateTime created = LocalDateTime.of(2026, 3, 1, 10, 0).plusSeconds(sequence);
HistoricalQueueRecord record = new HistoricalQueueRecord(
id, id, "1", priority, source, target, String.join(",", path),
"2026-03-01 10:00:0" + sequence, "2026-03-01 10:01:00", "1");
return new HistoricalSimulationTask(id, record, DispatchTask.TaskType.INBOUND,
priority, created, created.plusMinutes(1), source, target, path, sequence);
}
}

View File

@@ -0,0 +1,27 @@
package com.ft546.wcs.shadow.history.simulation;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SimulationProgressWatchdogTest {
@Test
void detectsLivelockWhenTerminalCountDoesNotChangeDespiteOtherActivity() {
SimulationProgressWatchdog watchdog = new SimulationProgressWatchdog(3, 0);
assertFalse(watchdog.observe(0));
assertFalse(watchdog.observe(0));
assertTrue(watchdog.observe(0));
}
@Test
void resetsWhenAnotherTaskReachesATerminalState() {
SimulationProgressWatchdog watchdog = new SimulationProgressWatchdog(2, 0);
assertFalse(watchdog.observe(0));
assertFalse(watchdog.observe(1));
assertFalse(watchdog.observe(1));
assertTrue(watchdog.observe(1));
}
}