feat: run deterministic full-chain history simulation
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.ft546.wcs.shadow.history.simulation;
|
||||
|
||||
public enum SimulationTaskStatus {
|
||||
COMPLETED,
|
||||
WAITING,
|
||||
FAILED
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user