feat(javaTest): replace route reservations atomically

This commit is contained in:
zgw
2026-08-06 17:22:08 +08:00
parent 2d834965c0
commit 5719fe4a5f
2 changed files with 166 additions and 0 deletions

View File

@@ -56,6 +56,98 @@ public final class CorridorReservationManager {
return ReservationDecision.granted(); return ReservationDecision.granted();
} }
/**
* 原子替换一个活动任务尚未通过的走廊方向预约。
*
* <p>方法先把当前任务自己的旧预约视为可替换资源,对新路线做完整预检;任一新边与其他任务方向冲突时
* 直接拒绝,旧预约保持原样。只有全部新边通过后才在同一次调用中提交移除和新增,因此重规划不会形成
* “旧路线已释放、新路线尚未取得”的安全空窗。
*
* @param taskId 正在重规划的任务
* @param newRoute 从车辆当前位置开始的新剩余路线
* @param effectivePriority 当前饥饿保护后的有效优先级
* @param arrivalMillis 任务到达队列的仿真时间
* @return 替换是否成功以及失败时优先持有冲突方向的任务
*/
public ReservationDecision replaceRoute(String taskId, List<String> newRoute,
int effectivePriority, long arrivalMillis) {
if (taskId == null || taskId.trim().isEmpty()
|| newRoute == null || newRoute.isEmpty()) {
throw new IllegalArgumentException("task id and new route are required");
}
List<EdgeRequest> requests = new ArrayList<EdgeRequest>();
for (int i = 1; i < newRoute.size(); i++) {
EdgeRequest request = request(newRoute.get(i - 1), newRoute.get(i));
requests.add(request);
EdgeReservation existing = edges.get(request.edgeId);
if (existing == null || existing.direction == request.direction) continue;
// 当前任务旧预约可以被翻转;其他任务仍持有旧方向时,新路线不能提交。
Set<String> otherHolders = new LinkedHashSet<String>(existing.holders);
otherHolders.remove(taskId);
if (!otherHolders.isEmpty()) {
return ReservationDecision.denied(bestOwner(otherHolders),
"CORRIDOR_DIRECTION_OWNED");
}
}
// 预检全部通过后才开始提交。先移除当前任务,绝不触碰同向共享该边的其他任务。
Set<String> oldEdges = taskEdges.get(taskId);
if (oldEdges != null) {
for (String edgeId : new ArrayList<String>(oldEdges)) {
EdgeReservation reservation = edges.get(edgeId);
if (reservation == null) continue;
reservation.holders.remove(taskId);
if (reservation.holders.isEmpty()) edges.remove(edgeId);
}
}
Rank rank = new Rank(taskId, effectivePriority, arrivalMillis);
ranks.put(taskId, rank);
Set<String> replacementEdges = new LinkedHashSet<String>();
for (EdgeRequest request : requests) {
EdgeReservation reservation = edges.get(request.edgeId);
if (reservation == null) {
reservation = new EdgeReservation(request.direction);
edges.put(request.edgeId, reservation);
}
reservation.holders.add(taskId);
replacementEdges.add(request.edgeId);
}
if (replacementEdges.isEmpty()) {
taskEdges.remove(taskId);
} else {
taskEdges.put(taskId, replacementEdges);
}
return ReservationDecision.granted();
}
/**
* 冻结当前全部走廊方向和持有任务,供路线质量评价和审计报告使用。
*
* @return 按物理边规范键排序的不可变预约列表
*/
public List<CorridorReservationSnapshot> snapshot() {
List<String> edgeIds = new ArrayList<String>(edges.keySet());
Collections.sort(edgeIds);
List<CorridorReservationSnapshot> result =
new ArrayList<CorridorReservationSnapshot>();
for (String edgeId : edgeIds) {
EdgeReservation reservation = edges.get(edgeId);
int separator = edgeId.indexOf('|');
String left = edgeId.substring(0, separator);
String right = edgeId.substring(separator + 1);
List<String> holders = new ArrayList<String>(reservation.holders);
Collections.sort(holders);
result.add(new CorridorReservationSnapshot(
reservation.direction > 0 ? left : right,
reservation.direction > 0 ? right : left,
holders));
}
return Collections.unmodifiableList(result);
}
/** /**
* 在车辆通过一条边后立即释放该段走廊,允许后续同向或反向任务重新竞争资源。 * 在车辆通过一条边后立即释放该段走廊,允许后续同向或反向任务重新竞争资源。
* @param taskId 执行该处理所需的 task id 输入。 * @param taskId 执行该处理所需的 task id 输入。
@@ -207,6 +299,29 @@ public final class CorridorReservationManager {
public String getReason() { return reason; } public String getReason() { return reason; }
} }
/** 一条物理走廊当前的有向预约及全部合法同向持有者。 */
public static final class CorridorReservationSnapshot {
private final String fromSite;
private final String toSite;
private final List<String> taskIds;
private CorridorReservationSnapshot(String fromSite, String toSite,
List<String> taskIds) {
this.fromSite = fromSite;
this.toSite = toSite;
this.taskIds = Collections.unmodifiableList(new ArrayList<String>(taskIds));
}
/** 返回预约方向的起点。 */
public String getFromSite() { return fromSite; }
/** 返回预约方向的终点。 */
public String getToSite() { return toSite; }
/** 返回共享该方向预约的任务编号,按编号稳定排序。 */
public List<String> getTaskIds() { return taskIds; }
}
private static final class EdgeReservation { private static final class EdgeReservation {
private final int direction; private final int direction;
private final Set<String> holders = new LinkedHashSet<String>(); private final Set<String> holders = new LinkedHashSet<String>();

View File

@@ -3,6 +3,8 @@ package com.ft546.wcs.shadow.history.simulation;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -50,4 +52,53 @@ class CorridorReservationManagerTest {
.isGranted()); .isGranted());
assertEquals("FIRST", manager.ownerFor("A", "B")); assertEquals("FIRST", manager.ownerFor("A", "B"));
} }
@Test
void rejectedReplacementKeepsEveryOldReservation() {
CorridorReservationManager manager = new CorridorReservationManager();
assertTrue(manager.reserve("TASK-1", Arrays.asList("A", "B", "C"), 1, 10L)
.isGranted());
assertTrue(manager.reserve("TASK-2", Arrays.asList("C", "D"), 2, 20L)
.isGranted());
CorridorReservationManager.ReservationDecision denied = manager.replaceRoute(
"TASK-1", Arrays.asList("A", "D", "C"), 1, 10L);
assertFalse(denied.isGranted());
assertEquals(new LinkedHashSet<String>(Arrays.asList("A->B", "B->C")),
edgesFor(manager, "TASK-1"));
assertEquals("TASK-1", manager.ownerFor("A", "B"));
assertEquals("TASK-1", manager.ownerFor("B", "C"));
}
@Test
void successfulReplacementCommitsNewRouteAndPreservesSharedOldHolder() {
CorridorReservationManager manager = new CorridorReservationManager();
assertTrue(manager.reserve("TASK-1", Arrays.asList("A", "B", "C"), 1, 10L)
.isGranted());
assertTrue(manager.reserve("TASK-2", Arrays.asList("A", "B", "C"), 2, 20L)
.isGranted());
CorridorReservationManager.ReservationDecision replaced = manager.replaceRoute(
"TASK-1", Arrays.asList("A", "D", "C"), 1, 10L);
assertTrue(replaced.isGranted());
assertEquals(new LinkedHashSet<String>(Arrays.asList("A->D", "D->C")),
edgesFor(manager, "TASK-1"));
assertEquals(new LinkedHashSet<String>(Arrays.asList("A->B", "B->C")),
edgesFor(manager, "TASK-2"));
assertEquals("TASK-2", manager.ownerFor("A", "B"));
assertEquals("TASK-1", manager.ownerFor("A", "D"));
}
private Set<String> edgesFor(CorridorReservationManager manager, String taskId) {
Set<String> edges = new LinkedHashSet<String>();
for (CorridorReservationManager.CorridorReservationSnapshot reservation
: manager.snapshot()) {
if (reservation.getTaskIds().contains(taskId)) {
edges.add(reservation.getFromSite() + "->" + reservation.getToSite());
}
}
return edges;
}
} }