feat(javaTest): reroute after repeated traffic hold

This commit is contained in:
zgw
2026-08-06 17:47:27 +08:00
parent 87129e39b4
commit 407184b9d5
7 changed files with 549 additions and 51 deletions

View File

@@ -14,6 +14,8 @@ public final class CorridorReservationManager {
new LinkedHashMap<String, EdgeReservation>(); new LinkedHashMap<String, EdgeReservation>();
private final Map<String, Set<String>> taskEdges = private final Map<String, Set<String>> taskEdges =
new LinkedHashMap<String, Set<String>>(); new LinkedHashMap<String, Set<String>>();
private final Map<String, Map<String, Integer>> taskEdgeUses =
new LinkedHashMap<String, Map<String, Integer>>();
private final Map<String, Rank> ranks = new LinkedHashMap<String, Rank>(); private final Map<String, Rank> ranks = new LinkedHashMap<String, Rank>();
/** /**
@@ -33,11 +35,12 @@ public final class CorridorReservationManager {
throw new IllegalArgumentException("task id and route are required"); throw new IllegalArgumentException("task id and route are required");
} }
Rank rank = new Rank(taskId, effectivePriority, arrivalMillis); Rank rank = new Rank(taskId, effectivePriority, arrivalMillis);
List<EdgeRequest> requests = routeRequests(route);
// 先做全路线预检:任一边已有相反方向持有者即整体拒绝,避免只预约半条路线。 // 先做全路线预检:任一边已有相反方向持有者即整体拒绝,避免只预约半条路线。
for (int i = 1; i < route.size(); i++) { for (EdgeRequest request : requests) {
EdgeRequest request = request(route.get(i - 1), route.get(i));
EdgeReservation existing = edges.get(request.edgeId); EdgeReservation existing = edges.get(request.edgeId);
if (existing != null && existing.direction != request.direction) { if (existing != null && (existing.direction != request.direction
|| existing.direction == 0)) {
return ReservationDecision.denied(bestOwner(existing.holders), return ReservationDecision.denied(bestOwner(existing.holders),
"CORRIDOR_DIRECTION_OWNED"); "CORRIDOR_DIRECTION_OWNED");
} }
@@ -46,13 +49,13 @@ public final class CorridorReservationManager {
ranks.put(taskId, rank); ranks.put(taskId, rank);
Set<String> reserved = taskEdges.computeIfAbsent( Set<String> reserved = taskEdges.computeIfAbsent(
taskId, ignored -> new LinkedHashSet<String>()); taskId, ignored -> new LinkedHashSet<String>());
for (int i = 1; i < route.size(); i++) { for (EdgeRequest request : requests) {
EdgeRequest request = request(route.get(i - 1), route.get(i));
EdgeReservation reservation = edges.computeIfAbsent( EdgeReservation reservation = edges.computeIfAbsent(
request.edgeId, ignored -> new EdgeReservation(request.direction)); request.edgeId, ignored -> new EdgeReservation(request.direction));
reservation.holders.add(taskId); reservation.holders.add(taskId);
reserved.add(request.edgeId); reserved.add(request.edgeId);
} }
taskEdgeUses.put(taskId, edgeUseCounts(route));
return ReservationDecision.granted(); return ReservationDecision.granted();
} }
@@ -76,17 +79,17 @@ public final class CorridorReservationManager {
throw new IllegalArgumentException("task id and new route are required"); throw new IllegalArgumentException("task id and new route are required");
} }
List<EdgeRequest> requests = new ArrayList<EdgeRequest>(); List<EdgeRequest> requests = routeRequests(newRoute);
for (int i = 1; i < newRoute.size(); i++) { for (EdgeRequest request : requests) {
EdgeRequest request = request(newRoute.get(i - 1), newRoute.get(i));
requests.add(request);
EdgeReservation existing = edges.get(request.edgeId); EdgeReservation existing = edges.get(request.edgeId);
if (existing == null || existing.direction == request.direction) continue; if (existing == null) continue;
// 当前任务旧预约可以被翻转;其他任务仍持有旧方向时,新路线不能提交。 // 当前任务旧预约可以被翻转;其他任务仍持有旧方向时,新路线不能提交。
Set<String> otherHolders = new LinkedHashSet<String>(existing.holders); Set<String> otherHolders = new LinkedHashSet<String>(existing.holders);
otherHolders.remove(taskId); otherHolders.remove(taskId);
if (!otherHolders.isEmpty()) { if (!otherHolders.isEmpty()
&& (existing.direction != request.direction
|| existing.direction == 0 || request.direction == 0)) {
return ReservationDecision.denied(bestOwner(otherHolders), return ReservationDecision.denied(bestOwner(otherHolders),
"CORRIDOR_DIRECTION_OWNED"); "CORRIDOR_DIRECTION_OWNED");
} }
@@ -120,6 +123,7 @@ public final class CorridorReservationManager {
} else { } else {
taskEdges.put(taskId, replacementEdges); taskEdges.put(taskId, replacementEdges);
} }
taskEdgeUses.put(taskId, edgeUseCounts(newRoute));
return ReservationDecision.granted(); return ReservationDecision.granted();
} }
@@ -141,9 +145,9 @@ public final class CorridorReservationManager {
List<String> holders = new ArrayList<String>(reservation.holders); List<String> holders = new ArrayList<String>(reservation.holders);
Collections.sort(holders); Collections.sort(holders);
result.add(new CorridorReservationSnapshot( result.add(new CorridorReservationSnapshot(
reservation.direction > 0 ? left : right, reservation.direction >= 0 ? left : right,
reservation.direction > 0 ? right : left, reservation.direction >= 0 ? right : left,
holders)); holders, reservation.direction == 0));
} }
return Collections.unmodifiableList(result); return Collections.unmodifiableList(result);
} }
@@ -157,6 +161,17 @@ public final class CorridorReservationManager {
public void releasePassedEdge(String taskId, String from, String to) { public void releasePassedEdge(String taskId, String from, String to) {
// 车辆已越过该物理边,后车可以立即竞争它;剩余边为空时一并清理任务排序记录。 // 车辆已越过该物理边,后车可以立即竞争它;剩余边为空时一并清理任务排序记录。
String edgeId = request(from, to).edgeId; String edgeId = request(from, to).edgeId;
Map<String, Integer> uses = taskEdgeUses.get(taskId);
Integer remainingUses = uses == null ? null : uses.get(edgeId);
if (remainingUses != null && remainingUses > 1) {
// 路线后面还会再次通过此边时继续保持预约,防止其他车辆插入任务的回程窗口。
uses.put(edgeId, remainingUses - 1);
return;
}
if (uses != null) {
uses.remove(edgeId);
if (uses.isEmpty()) taskEdgeUses.remove(taskId);
}
EdgeReservation reservation = edges.get(edgeId); EdgeReservation reservation = edges.get(edgeId);
if (reservation != null) { if (reservation != null) {
reservation.holders.remove(taskId); reservation.holders.remove(taskId);
@@ -179,6 +194,7 @@ public final class CorridorReservationManager {
public void releaseTask(String taskId) { public void releaseTask(String taskId) {
// HOLD 重派、车辆故障和正常完成都会走这里,确保未来路线不残留方向占用。 // HOLD 重派、车辆故障和正常完成都会走这里,确保未来路线不残留方向占用。
Set<String> reserved = taskEdges.remove(taskId); Set<String> reserved = taskEdges.remove(taskId);
taskEdgeUses.remove(taskId);
if (reserved != null) { if (reserved != null) {
for (String edgeId : new ArrayList<String>(reserved)) { for (String edgeId : new ArrayList<String>(reserved)) {
EdgeReservation reservation = edges.get(edgeId); EdgeReservation reservation = edges.get(edgeId);
@@ -235,6 +251,34 @@ public final class CorridorReservationManager {
canonical ? 1 : -1); canonical ? 1 : -1);
} }
/**
* 把路线折叠为每条物理边的一次方向请求;同一任务往返经过时用方向 0 表示双向独占。
*/
private List<EdgeRequest> routeRequests(List<String> route) {
Map<String, EdgeRequest> requests = new LinkedHashMap<String, EdgeRequest>();
for (int index = 1; index < route.size(); index++) {
EdgeRequest next = request(route.get(index - 1), route.get(index));
EdgeRequest existing = requests.get(next.edgeId);
if (existing == null) {
requests.put(next.edgeId, next);
} else if (existing.direction != next.direction) {
requests.put(next.edgeId, new EdgeRequest(next.edgeId, 0));
}
}
return new ArrayList<EdgeRequest>(requests.values());
}
/** 统计任务未来通过每条边的次数,确保最后一次通过后才逐段释放预约。 */
private Map<String, Integer> edgeUseCounts(List<String> route) {
Map<String, Integer> counts = new LinkedHashMap<String, Integer>();
for (int index = 1; index < route.size(); index++) {
String edgeId = request(route.get(index - 1), route.get(index)).edgeId;
Integer count = counts.get(edgeId);
counts.put(edgeId, count == null ? 1 : count + 1);
}
return counts;
}
public static final class ReservationDecision { public static final class ReservationDecision {
private final boolean granted; private final boolean granted;
private final String ownerTaskId; private final String ownerTaskId;
@@ -304,12 +348,15 @@ public final class CorridorReservationManager {
private final String fromSite; private final String fromSite;
private final String toSite; private final String toSite;
private final List<String> taskIds; private final List<String> taskIds;
private final boolean bidirectionalExclusive;
private CorridorReservationSnapshot(String fromSite, String toSite, private CorridorReservationSnapshot(String fromSite, String toSite,
List<String> taskIds) { List<String> taskIds,
boolean bidirectionalExclusive) {
this.fromSite = fromSite; this.fromSite = fromSite;
this.toSite = toSite; this.toSite = toSite;
this.taskIds = Collections.unmodifiableList(new ArrayList<String>(taskIds)); this.taskIds = Collections.unmodifiableList(new ArrayList<String>(taskIds));
this.bidirectionalExclusive = bidirectionalExclusive;
} }
/** 返回预约方向的起点。 */ /** 返回预约方向的起点。 */
@@ -320,6 +367,9 @@ public final class CorridorReservationManager {
/** 返回共享该方向预约的任务编号,按编号稳定排序。 */ /** 返回共享该方向预约的任务编号,按编号稳定排序。 */
public List<String> getTaskIds() { return taskIds; } public List<String> getTaskIds() { return taskIds; }
/** 返回是否因同一任务后续需要反向通过而对这条边双向独占。 */
public boolean isBidirectionalExclusive() { return bidirectionalExclusive; }
} }
private static final class EdgeReservation { private static final class EdgeReservation {

View File

@@ -5,11 +5,12 @@ import com.ft546.wcs.demo.center.VehicleAssignmentService;
import com.ft546.wcs.demo.model.DispatchTask; import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.demo.model.Site; import com.ft546.wcs.demo.model.Site;
import com.ft546.wcs.demo.model.VehicleSnapshot; import com.ft546.wcs.demo.model.VehicleSnapshot;
import com.ft546.wcs.demo.traffic.GridPathPlanner; import com.ft546.wcs.demo.traffic.RouteCandidate;
import com.ft546.wcs.demo.traffic.RouteCandidatePlanner; import com.ft546.wcs.demo.traffic.RouteCandidatePlanner;
import com.ft546.wcs.demo.traffic.RouteCandidateSet; import com.ft546.wcs.demo.traffic.RouteCandidateSet;
import com.ft546.wcs.demo.traffic.RoutePlanDecision; import com.ft546.wcs.demo.traffic.RoutePlanDecision;
import com.ft546.wcs.demo.traffic.RoutePlanningPolicy; import com.ft546.wcs.demo.traffic.RoutePlanningPolicy;
import com.ft546.wcs.demo.traffic.RouteQuality;
import com.ft546.wcs.demo.traffic.RouteQualityEvaluator; import com.ft546.wcs.demo.traffic.RouteQualityEvaluator;
import com.ft546.wcs.demo.traffic.TrafficRouteSnapshot; import com.ft546.wcs.demo.traffic.TrafficRouteSnapshot;
import com.ft546.wcs.shadow.application.ShadowTrafficCoordinator; import com.ft546.wcs.shadow.application.ShadowTrafficCoordinator;
@@ -36,7 +37,7 @@ import java.util.Set;
public final class HistoricalDispatchSimulator { public final class HistoricalDispatchSimulator {
private static final long STEP_MILLIS = 1_000L; private static final long STEP_MILLIS = 1_000L;
private static final int HOLD_REDISPATCH_THRESHOLD = 10; private static final int TRAFFIC_HOLD_REROUTE_THRESHOLD = 3;
/** /**
* 按中心调度的完整业务链路回放历史任务,而不是复用历史车辆或 {@code path_info}。 * 按中心调度的完整业务链路回放历史任务,而不是复用历史车辆或 {@code path_info}。
@@ -84,7 +85,7 @@ public final class HistoricalDispatchSimulator {
Collections.emptyList(), Collections.emptyList(), 0L, Collections.emptyList(), Collections.emptyList(), 0L,
null, null, 0, 0, 0, null, null, 0, 0, 0,
Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(),
Collections.emptyList())); Collections.emptyList(), 0));
} }
Map<String, SimulatedVehicleRuntime> vehicles = createRuntimes(input); Map<String, SimulatedVehicleRuntime> vehicles = createRuntimes(input);
@@ -97,7 +98,6 @@ public final class HistoricalDispatchSimulator {
SimulatedPlcGateway plc = new SimulatedPlcGateway(faults); SimulatedPlcGateway plc = new SimulatedPlcGateway(faults);
SimulationArchive archive = new SimulationArchive(); SimulationArchive archive = new SimulationArchive();
IdempotentWmsCompletionReceiver wms = new IdempotentWmsCompletionReceiver(); IdempotentWmsCompletionReceiver wms = new IdempotentWmsCompletionReceiver();
GridPathPlanner planner = new GridPathPlanner();
RouteCandidatePlanner routePlanner = new RouteCandidatePlanner(); RouteCandidatePlanner routePlanner = new RouteCandidatePlanner();
RouteQualityEvaluator routeEvaluator = new RouteQualityEvaluator(); RouteQualityEvaluator routeEvaluator = new RouteQualityEvaluator();
VehicleAssignmentService assignment = new VehicleAssignmentService(); VehicleAssignmentService assignment = new VehicleAssignmentService();
@@ -123,9 +123,9 @@ public final class HistoricalDispatchSimulator {
ledger, plc, events, traversedEdges); ledger, plc, events, traversedEdges);
boolean businessProgress = advanceActiveTasks( boolean businessProgress = advanceActiveTasks(
tick, now, input, config, queue, vehicles, tick, now, input, config, queue, vehicles,
active, executions, terminal, planner, ledger, plc, archive, wms, active, executions, terminal, routePlanner, routeEvaluator,
invariants, reservations, clearanceMoves, clearancePlanner, ledger, plc, archive, wms, invariants, reservations,
events, traversedEdges); clearanceMoves, clearancePlanner, events, traversedEdges);
verifyLedger(ledger, invariants); verifyLedger(ledger, invariants);
// 尚未到达的未来任务不应被误判为死锁,因为它只是在等待 create_date 对应的时刻。 // 尚未到达的未来任务不应被误判为死锁,因为它只是在等待 create_date 对应的时刻。
@@ -397,7 +397,8 @@ public final class HistoricalDispatchSimulator {
execution.planningEvidence.add(new RoutePlanningEvidence( execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.SERVICE, RoutePlanningEvidence.PlanningStage.SERVICE,
execution.pathVersion, now, false, serviceDecision)); execution.pathVersion, now, false, serviceDecision));
active.put(vehicleId, new ActiveTask(execution, route)); active.put(vehicleId, new ActiveTask(execution, route,
approachDecision.getSelectedPath().size() - 1));
queue.remove(task.getSimulationTaskId()); queue.remove(task.getSimulationTaskId());
events.add(event(tick, now, "ASSIGN", task.getSimulationTaskId(), vehicleId, events.add(event(tick, now, "ASSIGN", task.getSimulationTaskId(), vehicleId,
decision.getReason(), route.get(0), route.get(route.size() - 1))); decision.getReason(), route.get(0), route.get(route.size() - 1)));
@@ -413,7 +414,8 @@ public final class HistoricalDispatchSimulator {
* @param task 执行该处理所需的 task 输入。 * @param task 执行该处理所需的 task 输入。
* @param vehicles 执行该处理所需的 vehicles 输入。 * @param vehicles 执行该处理所需的 vehicles 输入。
* @param input 执行该处理所需的 input 输入。 * @param input 执行该处理所需的 input 输入。
* @param planner 执行该处理所需的 planner 输入。 * @param routePlanner 执行该处理所需的 route planner 输入。
* @param routeEvaluator 执行该处理所需的 route evaluator 输入。
* @return 存在通往源点的车队路径时返回 {@code true}。 * @return 存在通往源点的车队路径时返回 {@code true}。
*/ */
private boolean fleetCanReach(HistoricalSimulationTask task, private boolean fleetCanReach(HistoricalSimulationTask task,
@@ -458,8 +460,14 @@ public final class HistoricalDispatchSimulator {
for (CorridorReservationManager.CorridorReservationSnapshot reservation for (CorridorReservationManager.CorridorReservationSnapshot reservation
: reservations.snapshot()) { : reservations.snapshot()) {
for (String taskId : reservation.getTaskIds()) { for (String taskId : reservation.getTaskIds()) {
snapshot.reserveDirection(reservation.getFromSite(), if (reservation.isBidirectionalExclusive()) {
reservation.getToSite(), taskId); // 往返任务在该边两个方向都可能再次通行,对其他任务表现为不可兼容的独占边。
snapshot.reserveEdge(reservation.getFromSite(),
reservation.getToSite(), taskId);
} else {
snapshot.reserveDirection(reservation.getFromSite(),
reservation.getToSite(), taskId);
}
} }
} }
return snapshot.build(); return snapshot.build();
@@ -502,7 +510,8 @@ public final class HistoricalDispatchSimulator {
Map<String, ActiveTask> active, Map<String, ActiveTask> active,
Map<String, Execution> executions, Map<String, Execution> executions,
Map<String, SimulationTaskResult> terminal, Map<String, SimulationTaskResult> terminal,
GridPathPlanner planner, RouteCandidatePlanner routePlanner,
RouteQualityEvaluator routeEvaluator,
ResourceLedger ledger, ResourceLedger ledger,
SimulatedPlcGateway plc, SimulatedPlcGateway plc,
SimulationArchive archive, SimulationArchive archive,
@@ -536,8 +545,10 @@ public final class HistoricalDispatchSimulator {
String reverse = to + "->" + from; String reverse = to + "->" + from;
// 同一 tick 禁止重复占边和对向换边,防止两个移动在物理边中相撞。 // 同一 tick 禁止重复占边和对向换边,防止两个移动在物理边中相撞。
if (traversedEdges.contains(reverse) || traversedEdges.contains(direction)) { if (traversedEdges.contains(reverse) || traversedEdges.contains(direction)) {
recordHold(tick, now, current, vehicle, active, queue, progress |= recordTrafficHold(tick, now, current, vehicle, queue,
"OPPOSITE_EDGE_WINDOW", reservations, events); "OPPOSITE_EDGE_WINDOW", edge.value(), input, config,
routePlanner, routeEvaluator, vehicles, active, ledger,
reservations, events);
continue; continue;
} }
List<ResourceKey> resources = Arrays.asList( List<ResourceKey> resources = Arrays.asList(
@@ -549,10 +560,11 @@ public final class HistoricalDispatchSimulator {
boolean clearancePlanned = planClearanceForIdleBlockers( boolean clearancePlanned = planClearanceForIdleBlockers(
tick, now, current, vehicles, active, clearanceMoves, tick, now, current, vehicles, active, clearanceMoves,
clearancePlanner, ledger, input, idleBlockers, events); clearancePlanner, ledger, input, idleBlockers, events);
recordHold(tick, now, current, vehicle, active, queue, progress |= recordTrafficHold(tick, now, current, vehicle, queue,
clearancePlanned ? "IDLE_VEHICLE_CLEARANCE" clearancePlanned ? "IDLE_VEHICLE_CLEARANCE"
: "NO_CLEARANCE_SITE", : "NO_CLEARANCE_SITE",
reservations, events); ResourceKey.node(to).value(), input, config, routePlanner,
routeEvaluator, vehicles, active, ledger, reservations, events);
continue; continue;
} }
// 将车辆当前位置、意图和剩余路线冻结为影子快照,交通策略不能读取或修改实时对象。 // 将车辆当前位置、意图和剩余路线冻结为影子快照,交通策略不能读取或修改实时对象。
@@ -577,8 +589,11 @@ public final class HistoricalDispatchSimulator {
ShadowDecision decision = decisionEvent.getDecisions().get(0); ShadowDecision decision = decisionEvent.getDecisions().get(0);
if (decision.getType() != DecisionType.GRANT) { if (decision.getType() != DecisionType.GRANT) {
// 策略层拒绝时保留当前任务和预约,只累计 HOLD,等待下一轮优先级老化后再裁决。 // 策略层拒绝时保留当前任务和预约,只累计 HOLD,等待下一轮优先级老化后再裁决。
recordHold(tick, now, current, vehicle, active, queue, progress |= recordTrafficHold(tick, now, current, vehicle, queue,
decision.getReasonCode(), reservations, events); decision.getReasonCode(), firstResource(
decision.getConflictingResources(), edge.value()),
input, config, routePlanner, routeEvaluator, vehicles,
active, ledger, reservations, events);
continue; continue;
} }
// 策略许可后仍要通过权威资源台账原子预占起点、边和目标,防止试算与实际状态脱节。 // 策略许可后仍要通过权威资源台账原子预占起点、边和目标,防止试算与实际状态脱节。
@@ -588,10 +603,12 @@ public final class HistoricalDispatchSimulator {
tick, now, current, vehicles, active, clearanceMoves, tick, now, current, vehicles, active, clearanceMoves,
clearancePlanner, ledger, input, clearancePlanner, ledger, input,
protection.getBlockingVehicles(), events); protection.getBlockingVehicles(), events);
recordHold(tick, now, current, vehicle, active, queue, progress |= recordTrafficHold(tick, now, current, vehicle, queue,
clearancePlanned ? "IDLE_VEHICLE_CLEARANCE" clearancePlanned ? "IDLE_VEHICLE_CLEARANCE"
: "AUTHORITATIVE_RESOURCE_REJECTED", : "AUTHORITATIVE_RESOURCE_REJECTED",
reservations, events); firstResource(protection.getConflictingResources(), edge.value()),
input, config, routePlanner, routeEvaluator, vehicles,
active, ledger, reservations, events);
continue; continue;
} }
// 资源已预占后才下发 PLC;只有确认 ACK 才能提交车辆位置变化。 // 资源已预占后才下发 PLC;只有确认 ACK 才能提交车辆位置变化。
@@ -626,7 +643,7 @@ public final class HistoricalDispatchSimulator {
current.execution.task.getSimulationTaskId(), from, to); current.execution.task.getSimulationTaskId(), from, to);
traversedEdges.add(direction); traversedEdges.add(direction);
current.index++; current.index++;
current.holdStreak = 0; current.moved(to);
events.add(event(tick, now, "MOVE", events.add(event(tick, now, "MOVE",
current.execution.task.getSimulationTaskId(), vehicleId, current.execution.task.getSimulationTaskId(), vehicleId,
"ACK", from, to)); "ACK", from, to));
@@ -873,24 +890,170 @@ public final class HistoricalDispatchSimulator {
* @param reservations 执行该处理所需的 reservations 输入。 * @param reservations 执行该处理所需的 reservations 输入。
* @param events 执行该处理所需的 events 输入。 * @param events 执行该处理所需的 events 输入。
*/ */
private void recordHold(long tick, long now, ActiveTask current, private boolean recordTrafficHold(long tick, long now, ActiveTask current,
SimulatedVehicleRuntime vehicle, SimulatedVehicleRuntime vehicle,
Map<String, ActiveTask> active,
SimulationTaskQueue queue, String reason, SimulationTaskQueue queue, String reason,
String blockingResource,
HistoricalSimulationInput input,
SimulationConfig config,
RouteCandidatePlanner routePlanner,
RouteQualityEvaluator routeEvaluator,
Map<String, SimulatedVehicleRuntime> vehicles,
Map<String, ActiveTask> active,
ResourceLedger ledger,
CorridorReservationManager reservations, CorridorReservationManager reservations,
List<SimulationTickEvent> events) { List<SimulationTickEvent> events) {
hold(current.execution, reason); hold(current.execution, reason);
current.holdStreak++; TrafficHoldTracker.Observation observation = current.trafficHolds.record(
current.execution.task.getSimulationTaskId(), vehicle.getVehicleId(),
blockingResource, reason);
events.add(event(tick, now, "HOLD", current.execution.task.getSimulationTaskId(), events.add(event(tick, now, "HOLD", current.execution.task.getSimulationTaskId(),
vehicle.getVehicleId(), reason, vehicle.getCurrentSite(), null)); vehicle.getVehicleId(), reason, vehicle.getCurrentSite(), null));
if (current.holdStreak >= HOLD_REDISPATCH_THRESHOLD) { if (!observation.isRerouteTriggered()) return false;
active.remove(vehicle.getVehicleId());
reservations.releaseTask(current.execution.task.getSimulationTaskId()); events.add(event(tick, now, "REROUTE_TRIGGER",
queue.requeue(current.execution.task); current.execution.task.getSimulationTaskId(), vehicle.getVehicleId(),
events.add(event(tick, now, "REDISPATCH", reason + ";resource=" + blockingResource + ";count="
current.execution.task.getSimulationTaskId(), vehicle.getVehicleId(), + observation.getConsecutiveCount(),
reason, vehicle.getCurrentSite(), null)); vehicle.getCurrentSite(), null));
return attemptReroute(tick, now, current, vehicle, queue, input, config,
routePlanner, routeEvaluator, vehicles, active, ledger,
reservations, events);
}
/**
* 在第三次相同交通 HOLD 时,从车辆当前位置生成并试换严格更优的剩余路线。
*/
private boolean attemptReroute(long tick, long now, ActiveTask current,
SimulatedVehicleRuntime vehicle,
SimulationTaskQueue queue,
HistoricalSimulationInput input,
SimulationConfig config,
RouteCandidatePlanner routePlanner,
RouteQualityEvaluator routeEvaluator,
Map<String, SimulatedVehicleRuntime> vehicles,
Map<String, ActiveTask> active,
ResourceLedger ledger,
CorridorReservationManager reservations,
List<SimulationTickEvent> events) {
String taskId = current.execution.task.getSimulationTaskId();
if (!current.movedSinceReroute) {
events.add(event(tick, now, "REROUTE_REJECTED", taskId,
vehicle.getVehicleId(), "MOVE_REQUIRED_AFTER_LAST_REROUTE",
vehicle.getCurrentSite(), null));
return false;
} }
TrafficRouteSnapshot snapshot = trafficSnapshot(vehicles, active, ledger, reservations);
List<String> oldRemaining = new ArrayList<String>(
current.route.subList(current.index, current.route.size()));
List<RoutePlanningEvidence> newEvidence = new ArrayList<RoutePlanningEvidence>();
List<String> newRemaining;
RoutePlanDecision serviceDecision;
int nextVersion = current.execution.pathVersion + 1;
// 未到业务源点时同时重新评价接近段和完整服务段;进入服务段后只规划当前位置到终点。
if (!current.hasReachedServiceSource()) {
RouteCandidateSet approachCandidates = routePlanner.plan(input.getGridMap(),
vehicle.getCurrentSite(), current.execution.task.getPhysicalSource(),
Collections.<String>emptySet(), RoutePlanningPolicy.defaultPolicy());
RouteCandidateSet serviceCandidates = routePlanner.plan(input.getGridMap(),
current.execution.task.getPhysicalSource(),
current.execution.task.getPhysicalTarget(),
Collections.<String>emptySet(), RoutePlanningPolicy.defaultPolicy());
if (approachCandidates.getCandidates().isEmpty()
|| serviceCandidates.getCandidates().isEmpty()) {
return rejectReroute(tick, now, current, vehicle,
"NO_BOUNDED_CANDIDATE", events);
}
RoutePlanDecision approachDecision = routeEvaluator.select(input.getGridMap(),
approachCandidates, snapshot, taskId, vehicle.getVehicleId());
serviceDecision = routeEvaluator.select(input.getGridMap(), serviceCandidates,
snapshot, taskId, vehicle.getVehicleId());
newRemaining = new ArrayList<String>(approachDecision.getSelectedPath());
for (int index = 1; index < serviceDecision.getSelectedPath().size(); index++) {
newRemaining.add(serviceDecision.getSelectedPath().get(index));
}
newEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.APPROACH, nextVersion,
now, true, approachDecision));
} else {
RouteCandidateSet serviceCandidates = routePlanner.plan(input.getGridMap(),
vehicle.getCurrentSite(), current.execution.task.getPhysicalTarget(),
Collections.<String>emptySet(), RoutePlanningPolicy.defaultPolicy());
if (serviceCandidates.getCandidates().isEmpty()) {
return rejectReroute(tick, now, current, vehicle,
"NO_BOUNDED_CANDIDATE", events);
}
serviceDecision = routeEvaluator.select(input.getGridMap(), serviceCandidates,
snapshot, taskId, vehicle.getVehicleId());
newRemaining = new ArrayList<String>(serviceDecision.getSelectedPath());
}
newEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.SERVICE, nextVersion,
now, true, serviceDecision));
RouteQuality oldQuality = routeEvaluator.evaluate(input.getGridMap(),
new RouteCandidate(oldRemaining,
routePlanner.pathDistance(input.getGridMap(), oldRemaining)),
snapshot, taskId, vehicle.getVehicleId());
RouteQuality newQuality = routeEvaluator.evaluate(input.getGridMap(),
new RouteCandidate(newRemaining,
routePlanner.pathDistance(input.getGridMap(), newRemaining)),
snapshot, taskId, vehicle.getVehicleId());
if (newRemaining.equals(oldRemaining) || newQuality.compareTo(oldQuality) >= 0) {
return rejectReroute(tick, now, current, vehicle,
"NO_STRICTLY_BETTER_ROUTE", events);
}
CorridorReservationManager.ReservationDecision replacement =
reservations.replaceRoute(taskId, newRemaining,
queuePriority(current.execution, now, config),
current.execution.arrivalMillis);
if (!replacement.isGranted()) {
return rejectReroute(tick, now, current, vehicle,
"RESERVATION_REPLACEMENT_FAILED:" + replacement.getReason(), events);
}
// 预约先成功,才提交路线和证据;这条顺序保证失败时旧路线、旧索引和旧证据原样保留。
boolean beforeSource = !current.hasReachedServiceSource();
current.replaceRemainingRoute(newRemaining, beforeSource
? newEvidence.get(0).getDecision().getSelectedPath().size() - 1 : 0);
if (beforeSource) {
current.execution.plannedPath = new ArrayList<String>(
serviceDecision.getSelectedPath());
} else {
List<String> fullServicePath = new ArrayList<String>(current.traversedServicePath);
for (int index = 1; index < serviceDecision.getSelectedPath().size(); index++) {
fullServicePath.add(serviceDecision.getSelectedPath().get(index));
}
current.execution.plannedPath = fullServicePath;
}
current.execution.pathVersion = nextVersion;
current.execution.rerouteCount++;
current.execution.planningEvidence.addAll(newEvidence);
events.add(event(tick, now, "REROUTE", taskId, vehicle.getVehicleId(),
"STRICTLY_BETTER_TRAFFIC_QUALITY", oldRemaining.get(0),
newRemaining.get(newRemaining.size() - 1)));
return true;
}
/** 记录未换路原因并保持现有路线和预约。 */
private boolean rejectReroute(long tick, long now, ActiveTask current,
SimulatedVehicleRuntime vehicle, String reason,
List<SimulationTickEvent> events) {
events.add(event(tick, now, "REROUTE_REJECTED",
current.execution.task.getSimulationTaskId(), vehicle.getVehicleId(),
reason, vehicle.getCurrentSite(), null));
return false;
}
/** 从资源集合稳定选出阻塞标识;策略未返回明细时使用本步物理边。 */
private String firstResource(Set<ResourceKey> resources, String fallback) {
if (resources == null || resources.isEmpty()) return fallback;
List<ResourceKey> ordered = new ArrayList<ResourceKey>(resources);
Collections.sort(ordered);
return ordered.get(0).value();
} }
/** /**
@@ -991,7 +1154,7 @@ public final class HistoricalDispatchSimulator {
execution.holdCount, execution.holdCount,
plc.attemptsFor(execution.task.getSimulationTaskId()), plc.attemptsFor(execution.task.getSimulationTaskId()),
execution.retryCount, execution.holdReasons, execution.retryReasons, execution.retryCount, execution.holdReasons, execution.retryReasons,
execution.planningEvidence); execution.planningEvidence, execution.rerouteCount);
} }
/** /**
@@ -1078,6 +1241,7 @@ public final class HistoricalDispatchSimulator {
private final List<RoutePlanningEvidence> planningEvidence = private final List<RoutePlanningEvidence> planningEvidence =
new ArrayList<RoutePlanningEvidence>(); new ArrayList<RoutePlanningEvidence>();
private int pathVersion; private int pathVersion;
private int rerouteCount;
/** /**
* 创建 {@code Execution} 实例并初始化其内部状态。 * 创建 {@code Execution} 实例并初始化其内部状态。
@@ -1094,9 +1258,13 @@ public final class HistoricalDispatchSimulator {
private static final class ActiveTask { private static final class ActiveTask {
private final Execution execution; private final Execution execution;
private final List<String> route; private List<String> route;
private int index; private int index;
private int holdStreak; private int serviceStartIndex;
private final List<String> traversedServicePath = new ArrayList<String>();
private final TrafficHoldTracker trafficHolds =
new TrafficHoldTracker(TRAFFIC_HOLD_REROUTE_THRESHOLD);
private boolean movedSinceReroute = true;
/** /**
* 创建 {@code ActiveTask} 实例并初始化其内部状态。 * 创建 {@code ActiveTask} 实例并初始化其内部状态。
@@ -1104,10 +1272,41 @@ public final class HistoricalDispatchSimulator {
* <p>该方法属于 {@code ActiveTask},用于保持当前模块的职责边界清晰。 * <p>该方法属于 {@code ActiveTask},用于保持当前模块的职责边界清晰。
* @param execution 执行该处理所需的 execution 输入。 * @param execution 执行该处理所需的 execution 输入。
* @param route 执行该处理所需的 route 输入。 * @param route 执行该处理所需的 route 输入。
* @param serviceStartIndex 服务段源点在完整执行路线中的下标。
*/ */
private ActiveTask(Execution execution, List<String> route) { private ActiveTask(Execution execution, List<String> route, int serviceStartIndex) {
this.execution = execution; this.execution = execution;
this.route = Collections.unmodifiableList(new ArrayList<String>(route)); this.route = Collections.unmodifiableList(new ArrayList<String>(route));
this.serviceStartIndex = serviceStartIndex;
if (serviceStartIndex == 0) traversedServicePath.add(route.get(0));
}
/** 返回车辆是否已经到达业务源点并开始执行服务段。 */
private boolean hasReachedServiceSource() {
return index >= serviceStartIndex || !traversedServicePath.isEmpty();
}
/** 成功换路后从当前位置重新建立剩余路线索引,并启用移动一次的防抖门禁。 */
private void replaceRemainingRoute(List<String> replacement,
int replacementServiceStartIndex) {
route = Collections.unmodifiableList(new ArrayList<String>(replacement));
index = 0;
serviceStartIndex = replacementServiceStartIndex;
movedSinceReroute = false;
}
/** ACK 移动后更新服务段实际前缀、清空旧 HOLD,并解除换路防抖门禁。 */
private void moved(String toSite) {
if (index >= serviceStartIndex && traversedServicePath.isEmpty()) {
traversedServicePath.add(execution.task.getPhysicalSource());
}
if (!traversedServicePath.isEmpty()
&& !traversedServicePath.get(traversedServicePath.size() - 1)
.equals(toSite)) {
traversedServicePath.add(toSite);
}
trafficHolds.clearAfterMovement();
movedSinceReroute = true;
} }
} }

View File

@@ -25,6 +25,7 @@ public final class SimulationTaskResult {
private final Map<String, Integer> holdReasons; private final Map<String, Integer> holdReasons;
private final Map<String, Integer> retryReasons; private final Map<String, Integer> retryReasons;
private final List<RoutePlanningEvidence> planningEvidence; private final List<RoutePlanningEvidence> planningEvidence;
private final int rerouteCount;
/** /**
* 创建 {@code SimulationTaskResult} 实例并初始化其内部状态。 * 创建 {@code SimulationTaskResult} 实例并初始化其内部状态。
@@ -46,6 +47,7 @@ public final class SimulationTaskResult {
* @param holdReasons 执行该处理所需的 hold reasons 输入。 * @param holdReasons 执行该处理所需的 hold reasons 输入。
* @param retryReasons 执行该处理所需的 retry reasons 输入。 * @param retryReasons 执行该处理所需的 retry reasons 输入。
* @param planningEvidence 每个实际采用路径版本的接近段和服务段决策证据。 * @param planningEvidence 每个实际采用路径版本的接近段和服务段决策证据。
* @param rerouteCount 运行中成功替换剩余路线的次数。
*/ */
SimulationTaskResult(String simulationTaskId, HistoricalQueueRecord record, SimulationTaskResult(String simulationTaskId, HistoricalQueueRecord record,
SimulationTaskStatus status, String reason, SimulationTaskStatus status, String reason,
@@ -55,7 +57,8 @@ public final class SimulationTaskResult {
int holdCount, int protocolAttempts, int retryCount, int holdCount, int protocolAttempts, int retryCount,
Map<String, Integer> holdReasons, Map<String, Integer> holdReasons,
Map<String, Integer> retryReasons, Map<String, Integer> retryReasons,
List<RoutePlanningEvidence> planningEvidence) { List<RoutePlanningEvidence> planningEvidence,
int rerouteCount) {
this.simulationTaskId = simulationTaskId; this.simulationTaskId = simulationTaskId;
this.record = record; this.record = record;
this.status = status; this.status = status;
@@ -72,6 +75,7 @@ public final class SimulationTaskResult {
this.holdReasons = immutableMap(holdReasons); this.holdReasons = immutableMap(holdReasons);
this.retryReasons = immutableMap(retryReasons); this.retryReasons = immutableMap(retryReasons);
this.planningEvidence = immutable(planningEvidence); this.planningEvidence = immutable(planningEvidence);
this.rerouteCount = rerouteCount;
} }
/** /**
@@ -182,6 +186,9 @@ public final class SimulationTaskResult {
/** 返回任务每个路径版本的接近段和服务段中心规划证据。 */ /** 返回任务每个路径版本的接近段和服务段中心规划证据。 */
public List<RoutePlanningEvidence> getPlanningEvidence() { return planningEvidence; } public List<RoutePlanningEvidence> getPlanningEvidence() { return planningEvidence; }
/** 返回运行中因连续交通 HOLD 成功切换路线的次数。 */
public int getRerouteCount() { return rerouteCount; }
/** /**
* 返回当前对象保存的“waiting millis”信息。 * 返回当前对象保存的“waiting millis”信息。
* *

View File

@@ -0,0 +1,107 @@
package com.ft546.wcs.shadow.history.simulation;
import java.util.Objects;
/** 只跟踪同一交通资源连续阻塞次数,不接收 PLC 或车辆健康事件。 */
public final class TrafficHoldTracker {
private final int rerouteThreshold;
private HoldKey currentKey;
private int consecutiveCount;
/**
* 创建连续交通 HOLD 跟踪器。
*
* @param rerouteThreshold 第几次完全相同的交通阻塞触发一次重规划
*/
public TrafficHoldTracker(int rerouteThreshold) {
if (rerouteThreshold <= 0) {
throw new IllegalArgumentException("reroute threshold must be positive");
}
this.rerouteThreshold = rerouteThreshold;
}
/**
* 登记一次交通 HOLD,并判断本次是否恰好到达重规划阈值。
*
* <p>任务、车辆、阻塞资源或原因任一变化都代表交通条件已经变化,新序列从 1 开始。阈值只在恰好
* 到达时触发一次;若没有更优路线,同一未变化阻塞不会每个 tick 重复做昂贵规划。
*/
public Observation record(String taskId, String vehicleId,
String blockingResourceId, String holdReason) {
HoldKey next = new HoldKey(required(taskId), required(vehicleId),
required(blockingResourceId), required(holdReason));
if (!next.equals(currentKey)) {
currentKey = next;
consecutiveCount = 1;
} else {
consecutiveCount++;
}
return new Observation(consecutiveCount,
consecutiveCount == rerouteThreshold,
blockingResourceId, holdReason);
}
/** 车辆成功移动后清除旧阻塞序列,下一次 HOLD 必须重新累计三次。 */
public void clearAfterMovement() {
currentKey = null;
consecutiveCount = 0;
}
/** 拒绝空标识,保证事件中每次触发都能追溯到具体任务、车辆和资源。 */
private String required(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("traffic hold key parts are required");
}
return value;
}
/** 一次 HOLD 累计后的可审计结果。 */
public static final class Observation {
private final int consecutiveCount;
private final boolean rerouteTriggered;
private final String blockingResourceId;
private final String holdReason;
private Observation(int consecutiveCount, boolean rerouteTriggered,
String blockingResourceId, String holdReason) {
this.consecutiveCount = consecutiveCount;
this.rerouteTriggered = rerouteTriggered;
this.blockingResourceId = blockingResourceId;
this.holdReason = holdReason;
}
public int getConsecutiveCount() { return consecutiveCount; }
public boolean isRerouteTriggered() { return rerouteTriggered; }
public String getBlockingResourceId() { return blockingResourceId; }
public String getHoldReason() { return holdReason; }
}
/** 完整标识“相同阻塞”的业务键。 */
private static final class HoldKey {
private final String taskId;
private final String vehicleId;
private final String resourceId;
private final String reason;
private HoldKey(String taskId, String vehicleId,
String resourceId, String reason) {
this.taskId = taskId;
this.vehicleId = vehicleId;
this.resourceId = resourceId;
this.reason = reason;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof HoldKey)) return false;
HoldKey that = (HoldKey) other;
return taskId.equals(that.taskId) && vehicleId.equals(that.vehicleId)
&& resourceId.equals(that.resourceId) && reason.equals(that.reason);
}
@Override
public int hashCode() {
return Objects.hash(taskId, vehicleId, resourceId, reason);
}
}
}

View File

@@ -91,6 +91,28 @@ class CorridorReservationManagerTest {
assertEquals("TASK-1", manager.ownerFor("A", "D")); assertEquals("TASK-1", manager.ownerFor("A", "D"));
} }
@Test
void routeThatUsesOneCorridorBothWaysKeepsItExclusiveToThatTask() {
CorridorReservationManager manager = new CorridorReservationManager();
assertTrue(manager.reserve("TURNAROUND",
Arrays.asList("B", "A", "B", "C"), 1, 0L).isGranted());
CorridorReservationManager.ReservationDecision sameDirectionOtherTask =
manager.reserve("OTHER", Arrays.asList("D", "B", "A"), 2, 1L);
assertFalse(sameDirectionOtherTask.isGranted());
assertEquals("TURNAROUND", sameDirectionOtherTask.getOwnerTaskId());
assertTrue(manager.snapshot().stream().anyMatch(reservation ->
reservation.isBidirectionalExclusive()
&& reservation.getTaskIds().contains("TURNAROUND")));
manager.releasePassedEdge("TURNAROUND", "B", "A");
assertFalse(manager.reserve("WAITING", Arrays.asList("D", "B", "A"),
2, 2L).isGranted());
manager.releasePassedEdge("TURNAROUND", "A", "B");
assertTrue(manager.reserve("AFTER-TURN", Arrays.asList("D", "B", "A"),
2, 3L).isGranted());
}
private Set<String> edgesFor(CorridorReservationManager manager, String taskId) { private Set<String> edgesFor(CorridorReservationManager manager, String taskId) {
Set<String> edges = new LinkedHashSet<String>(); Set<String> edges = new LinkedHashSet<String>();
for (CorridorReservationManager.CorridorReservationSnapshot reservation for (CorridorReservationManager.CorridorReservationSnapshot reservation

View File

@@ -106,6 +106,33 @@ class HistoricalDispatchSimulatorTest {
assertEquals(0, result.getFailedCount()); assertEquals(0, result.getFailedCount());
} }
@Test
void reroutesOnThirdIdenticalTrafficHoldAfterConflictAppearsDuringExecution() {
DeterministicFaultPlan faults = DeterministicFaultPlan.builder()
.vehicleEvent(1L, "1",
DeterministicFaultPlan.VehicleEventType.DISCONNECT)
.build();
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
runtimeRerouteInput(), new SimulationConfig(100, 30_000L, 3, 100, 300),
faults);
SimulationTaskResult task = result.getTaskResults().stream()
.filter(value -> "REROUTE".equals(value.getSimulationTaskId()))
.findFirst().orElseThrow(AssertionError::new);
assertEquals(Arrays.asList("A", "C", "D"), task.getPlannedPath());
assertEquals(1, task.getRerouteCount());
assertTrue(result.getEvents().stream().anyMatch(event ->
"REROUTE_TRIGGER".equals(event.getType())));
assertTrue(result.getEvents().stream().anyMatch(event ->
"REROUTE".equals(event.getType())));
assertEquals(2, task.getPlanningEvidence().stream()
.map(RoutePlanningEvidence::getPathVersion)
.distinct().count());
assertEquals(2, result.getCompletedCount());
assertEquals(0, result.getInvariants().totalSafetyViolations());
}
/** /**
* 验证“moves idle blockers node by node and completes both business directions”场景,确保相关业务规则和断言成立。 * 验证“moves idle blockers node by node and completes both business directions”场景,确保相关业务规则和断言成立。
* *
@@ -274,6 +301,44 @@ class HistoricalDispatchSimulatorTest {
traffic, vehicles); traffic, vehicles);
} }
private HistoricalSimulationInput runtimeRerouteInput() {
GridMap grid = new GridMap();
grid.addSite(new Site("A", 1, 0, 0, "TEST"));
grid.addSite(new Site("B", 1, 5, 0, "TEST"));
grid.addSite(new Site("C", 1, 5, 1, "TEST"));
grid.addSite(new Site("D", 1, 10, 0, "TEST"));
grid.addSite(new Site("E", 1, 5, -1, "TEST"));
grid.connect("A", "B");
grid.connect("B", "D");
grid.connect("A", "C");
grid.connect("C", "D");
grid.connect("E", "B");
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", "D"));
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "C"));
edges.add(new TrafficMapCompiler.EdgeDefinition("C", "D"));
edges.add(new TrafficMapCompiler.EdgeDefinition("E", "B"));
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
new TrafficMapCompiler.MapDefinition("REROUTE", 1L, "TEST", true,
"test", "test", nodes, edges, Collections.emptyMap(),
Collections.emptySet(), Collections.emptySet()));
List<HistoricalSimulationTask> tasks = Arrays.asList(
customTask("REROUTE", "A", "D", 1, 0,
Arrays.asList("A", "B", "D")),
customTask("BLOCKER", "E", "B", 2, 0,
Arrays.asList("E", "B")));
List<VehicleSnapshot> vehicles = Arrays.asList(
new VehicleSnapshot("1", 1, "E", 5, 1, 100, true, true, false),
new VehicleSnapshot("2", 1, "A", 5, 1, 100, true, true, false));
return new HistoricalSimulationInput(tasks, Collections.emptyList(), grid,
traffic, vehicles);
}
/** /**
* 验证“blocked three vehicle corridor input”场景,确保相关业务规则和断言成立。 * 验证“blocked three vehicle corridor input”场景,确保相关业务规则和断言成立。
* *

View File

@@ -0,0 +1,48 @@
package com.ft546.wcs.shadow.history.simulation;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TrafficHoldTrackerTest {
@Test
void triggersOnlyOnThirdConsecutiveIdenticalTrafficBlock() {
TrafficHoldTracker tracker = new TrafficHoldTracker(3);
TrafficHoldTracker.Observation first = tracker.record(
"TASK", "AGV", "NODE:B", "NODE_OCCUPIED");
TrafficHoldTracker.Observation second = tracker.record(
"TASK", "AGV", "NODE:B", "NODE_OCCUPIED");
TrafficHoldTracker.Observation third = tracker.record(
"TASK", "AGV", "NODE:B", "NODE_OCCUPIED");
TrafficHoldTracker.Observation fourth = tracker.record(
"TASK", "AGV", "NODE:B", "NODE_OCCUPIED");
assertEquals(1, first.getConsecutiveCount());
assertFalse(first.isRerouteTriggered());
assertEquals(2, second.getConsecutiveCount());
assertFalse(second.isRerouteTriggered());
assertEquals(3, third.getConsecutiveCount());
assertTrue(third.isRerouteTriggered());
assertEquals(4, fourth.getConsecutiveCount());
assertFalse(fourth.isRerouteTriggered());
}
@Test
void changedResourceReasonOrMovementStartsANewSequence() {
TrafficHoldTracker tracker = new TrafficHoldTracker(3);
tracker.record("TASK", "AGV", "NODE:B", "NODE_OCCUPIED");
tracker.record("TASK", "AGV", "NODE:B", "NODE_OCCUPIED");
assertEquals(1, tracker.record(
"TASK", "AGV", "EDGE:B:C", "EDGE_RESERVED").getConsecutiveCount());
assertEquals(1, tracker.record(
"TASK", "AGV", "EDGE:B:C", "OPPOSITE_DIRECTION").getConsecutiveCount());
tracker.clearAfterMovement();
assertEquals(1, tracker.record(
"TASK", "AGV", "EDGE:B:C", "OPPOSITE_DIRECTION").getConsecutiveCount());
}
}