From 6169fbcb4663b911a1e6ef2fe65ab8efd08b0b37 Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Fri, 7 Aug 2026 14:12:09 +0800 Subject: [PATCH] feat(javaTest): scope PLC retries by business phase --- .../simulation/DeterministicFaultPlan.java | 74 +++++++++++++---- .../simulation/SimulatedPlcGateway.java | 79 +++++++++++++++---- .../simulation/PhaseAwarePlcGatewayTest.java | 51 ++++++++++++ .../simulation/SimulationAdaptersTest.java | 4 + 4 files changed, 177 insertions(+), 31 deletions(-) create mode 100644 javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/PhaseAwarePlcGatewayTest.java diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/DeterministicFaultPlan.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/DeterministicFaultPlan.java index 77907f7..24ee0db 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/DeterministicFaultPlan.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/DeterministicFaultPlan.java @@ -44,7 +44,8 @@ public final class DeterministicFaultPlan { public VehicleEventType getType() { return type; } } - private final Map> plcFailures; + private final Map> plcTaskFailures; + private final Map> plcCommandFailures; private final Map> vehicleEvents; /** @@ -55,9 +56,11 @@ public final class DeterministicFaultPlan { * @param vehicleEvents 执行该处理所需的 vehicle events 输入。 */ private DeterministicFaultPlan( - Map> plcFailures, + Map> plcTaskFailures, + Map> plcCommandFailures, Map> vehicleEvents) { - this.plcFailures = plcFailures; + this.plcTaskFailures = plcTaskFailures; + this.plcCommandFailures = plcCommandFailures; this.vehicleEvents = vehicleEvents; } @@ -86,10 +89,23 @@ public final class DeterministicFaultPlan { * @return 本次处理产生的结果;调用方据此继续后续流程。 */ SimulatedPlcGateway.PlcReason plcReason(String taskId, int attempt) { - Map byAttempt = plcFailures.get(taskId); + Map byAttempt = plcTaskFailures.get(taskId); return byAttempt == null ? null : byAttempt.get(attempt); } + /** + * 先匹配具体业务阶段命令的第 N 次发送;没有阶段配置时,再按父任务总发送次数兼容旧故障计划。 + */ + SimulatedPlcGateway.PlcReason plcReason(String parentTaskId, String commandId, + int commandAttempt, int taskAttempt) { + Map byCommand = + plcCommandFailures.get(commandId); + SimulatedPlcGateway.PlcReason commandReason = byCommand == null + ? null : byCommand.get(commandAttempt); + return commandReason == null + ? plcReason(parentTaskId, taskAttempt) : commandReason; + } + /** * 执行“events at”所表示的处理流程。 * @@ -103,7 +119,9 @@ public final class DeterministicFaultPlan { } public static final class Builder { - private final Map> plcFailures = + private final Map> plcTaskFailures = + new LinkedHashMap>(); + private final Map> plcCommandFailures = new LinkedHashMap>(); private final Map> vehicleEvents = new LinkedHashMap>(); @@ -119,18 +137,31 @@ public final class DeterministicFaultPlan { */ public Builder plcFailure(String taskId, int attempt, SimulatedPlcGateway.PlcReason reason) { - if (taskId == null || attempt <= 0 || reason == null + addPlcFailure(plcTaskFailures, taskId, attempt, reason); + return this; + } + + /** 为某一业务阶段命令配置独立失败,不消耗其他阶段的重试序号。 */ + public Builder plcCommandFailure(String commandId, int attempt, + SimulatedPlcGateway.PlcReason reason) { + addPlcFailure(plcCommandFailures, commandId, attempt, reason); + return this; + } + + private void addPlcFailure( + Map> failures, + String key, int attempt, SimulatedPlcGateway.PlcReason reason) { + if (key == null || key.trim().isEmpty() || attempt <= 0 || reason == null || reason == SimulatedPlcGateway.PlcReason.ACK || reason == SimulatedPlcGateway.PlcReason.STALE_BOOT_ID) { throw new IllegalArgumentException("valid PLC failure is required"); } - Map byAttempt = plcFailures.get(taskId); + Map byAttempt = failures.get(key); if (byAttempt == null) { byAttempt = new LinkedHashMap(); - plcFailures.put(taskId, byAttempt); + failures.put(key, byAttempt); } byAttempt.put(attempt, reason); - return this; } /** @@ -162,21 +193,30 @@ public final class DeterministicFaultPlan { * @return 本次处理产生的结果;调用方据此继续后续流程。 */ public DeterministicFaultPlan build() { - Map> plc = - new LinkedHashMap>(); - for (Map.Entry> entry - : plcFailures.entrySet()) { - plc.put(entry.getKey(), Collections.unmodifiableMap( - new LinkedHashMap(entry.getValue()))); - } + Map> taskPlc = + immutableFailures(plcTaskFailures); + Map> commandPlc = + immutableFailures(plcCommandFailures); Map> vehicles = new LinkedHashMap>(); for (Map.Entry> entry : vehicleEvents.entrySet()) { vehicles.put(entry.getKey(), Collections.unmodifiableList( new ArrayList(entry.getValue()))); } - return new DeterministicFaultPlan(Collections.unmodifiableMap(plc), + return new DeterministicFaultPlan(taskPlc, commandPlc, Collections.unmodifiableMap(vehicles)); } + + private Map> immutableFailures( + Map> source) { + Map> copy = + new LinkedHashMap>(); + for (Map.Entry> entry + : source.entrySet()) { + copy.put(entry.getKey(), Collections.unmodifiableMap( + new LinkedHashMap(entry.getValue()))); + } + return Collections.unmodifiableMap(copy); + } } } diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulatedPlcGateway.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulatedPlcGateway.java index ccd1eba..7e20a1f 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulatedPlcGateway.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulatedPlcGateway.java @@ -10,7 +10,8 @@ public final class SimulatedPlcGateway { } public static final class PlcResponse { - private final String taskId; + private final String parentTaskId; + private final String commandId; private final String vehicleId; private final String from; private final String to; @@ -30,9 +31,11 @@ public final class SimulatedPlcGateway { * @param bootId 执行该处理所需的 boot id 输入。 * @param reason 执行该处理所需的 reason 输入。 */ - private PlcResponse(String taskId, String vehicleId, String from, String to, + private PlcResponse(String parentTaskId, String commandId, + String vehicleId, String from, String to, int attempt, long bootId, PlcReason reason) { - this.taskId = taskId; + this.parentTaskId = parentTaskId; + this.commandId = commandId; this.vehicleId = vehicleId; this.from = from; this.to = to; @@ -47,7 +50,13 @@ public final class SimulatedPlcGateway { *

该方法属于 {@code PlcResponse},用于保持当前模块的职责边界清晰。 * @return 本次处理产生的结果;调用方据此继续后续流程。 */ - public String getTaskId() { return taskId; } + public String getTaskId() { return parentTaskId; } + + /** 返回归档和 WMS 幂等使用的业务父任务 ID。 */ + public String getParentTaskId() { return parentTaskId; } + + /** 返回包含业务阶段和路线版本的 PLC 命令 ID。 */ + public String getCommandId() { return commandId; } /** * 返回当前对象保存的“vehicle id”信息。 * @@ -93,7 +102,8 @@ public final class SimulatedPlcGateway { } private final DeterministicFaultPlan faults; - private final Map attempts = new LinkedHashMap(); + private final Map taskAttempts = new LinkedHashMap(); + private final Map commandAttempts = new LinkedHashMap(); /** * 创建 PLC 协议模拟器,并绑定确定性故障计划以便重复测试发送、超时和丢 ACK 场景。 @@ -116,13 +126,27 @@ public final class SimulatedPlcGateway { */ public PlcResponse send(String taskId, SimulatedVehicleRuntime vehicle, String from, String to) { - // 按任务累计协议尝试次数,使故障计划能精确指定第 N 次发送失败或超时。 - int attempt = attempts.containsKey(taskId) ? attempts.get(taskId) + 1 : 1; - attempts.put(taskId, attempt); - PlcReason reason = faults.plcReason(taskId, attempt); + return send(taskId, taskId, vehicle, from, to); + } + + /** + * 向 PLC 发送某个父任务的阶段命令;命令尝试次数用于阶段重试,任务尝试次数只用于汇总和旧故障配置。 + */ + public PlcResponse send(String parentTaskId, String commandId, + SimulatedVehicleRuntime vehicle, String from, String to) { + if (blank(parentTaskId) || blank(commandId) || vehicle == null + || blank(from) || blank(to)) { + throw new IllegalArgumentException( + "parent task, command, vehicle and movement endpoints are required"); + } + // 每个阶段命令从 1 独立计数,因此载货阶段的首发失败不会继承空车阶段的次数。 + int commandAttempt = increment(commandAttempts, commandId); + int taskAttempt = increment(taskAttempts, parentTaskId); + PlcReason reason = faults.plcReason( + parentTaskId, commandId, commandAttempt, taskAttempt); if (reason == null) reason = PlcReason.ACK; - return new PlcResponse(taskId, vehicle.getVehicleId(), from, to, - attempt, vehicle.getBootId(), reason); + return new PlcResponse(parentTaskId, commandId, vehicle.getVehicleId(), from, to, + commandAttempt, vehicle.getBootId(), reason); } /** @@ -135,7 +159,8 @@ public final class SimulatedPlcGateway { if (response.getReason() != PlcReason.ACK) return response; // 车辆重启后 bootId 会变化,旧 ACK 即使迟到也不能再提交路线索引和车位。 if (response.getBootId() != vehicle.getBootId()) { - return new PlcResponse(response.getTaskId(), response.getVehicleId(), + return new PlcResponse(response.getParentTaskId(), response.getCommandId(), + response.getVehicleId(), response.getFrom(), response.getTo(), response.getAttempt(), response.getBootId(), PlcReason.STALE_BOOT_ID); } @@ -148,7 +173,18 @@ public final class SimulatedPlcGateway { * @return 本次处理产生的结果;调用方据此继续后续流程。 */ public int attemptsFor(String taskId) { - Integer value = attempts.get(taskId); + return attemptsForTask(taskId); + } + + /** 返回父任务跨所有阶段累计发送 PLC 指令的次数。 */ + public int attemptsForTask(String parentTaskId) { + Integer value = taskAttempts.get(parentTaskId); + return value == null ? 0 : value; + } + + /** 返回单个阶段命令自己的发送次数,供当前阶段的重试上限判断。 */ + public int attemptsForCommand(String commandId) { + Integer value = commandAttempts.get(commandId); return value == null ? 0 : value; } @@ -159,6 +195,21 @@ public final class SimulatedPlcGateway { * @return 本次处理产生的结果;调用方据此继续后续流程。 */ public Map getAttempts() { - return Collections.unmodifiableMap(attempts); + return Collections.unmodifiableMap(taskAttempts); + } + + /** 返回只读的阶段命令尝试次数,供报告按阶段汇总。 */ + public Map getCommandAttempts() { + return Collections.unmodifiableMap(commandAttempts); + } + + private int increment(Map values, String key) { + int next = values.containsKey(key) ? values.get(key) + 1 : 1; + values.put(key, next); + return next; + } + + private boolean blank(String value) { + return value == null || value.trim().isEmpty(); } } diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/PhaseAwarePlcGatewayTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/PhaseAwarePlcGatewayTest.java new file mode 100644 index 0000000..eef5e7b --- /dev/null +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/PhaseAwarePlcGatewayTest.java @@ -0,0 +1,51 @@ +package com.ft546.wcs.shadow.history.simulation; + +import org.junit.jupiter.api.Test; + +import static com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway.PlcReason.ACK; +import static com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway.PlcReason.ACK_TIMEOUT; +import static com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway.PlcReason.SEND_FAILURE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PhaseAwarePlcGatewayTest { + /** 载货阶段自己的首发失败,不得继承空车阶段已经使用过的尝试序号。 */ + @Test + void failureOnLoadedPhaseDoesNotConsumeEmptyPhaseAttemptNumbers() { + String emptyCommand = "B1:EMPTY_TO_PICKUP:v1:e0"; + String loadedCommand = "B1:LOADED_TO_PLACE:v1:e0"; + DeterministicFaultPlan plan = DeterministicFaultPlan.builder() + .plcCommandFailure(loadedCommand, 1, SEND_FAILURE) + .build(); + SimulatedPlcGateway gateway = new SimulatedPlcGateway(plan); + SimulatedVehicleRuntime vehicle = new SimulatedVehicleRuntime("1", "A", 1); + + assertEquals(ACK, gateway.send("B1", emptyCommand, + vehicle, "A", "B").getReason()); + SimulatedPlcGateway.PlcResponse failed = gateway.send( + "B1", loadedCommand, vehicle, "B", "C"); + + assertEquals(SEND_FAILURE, failed.getReason()); + assertEquals("B1", failed.getParentTaskId()); + assertEquals(loadedCommand, failed.getCommandId()); + assertEquals(1, failed.getAttempt()); + assertEquals(2, gateway.attemptsForTask("B1")); + assertEquals(1, gateway.attemptsForCommand(emptyCommand)); + assertEquals(1, gateway.attemptsForCommand(loadedCommand)); + } + + /** 旧的任务级故障配置仍按父任务总发送次数生效,可继续驱动原始回放测试。 */ + @Test + void taskWideFailureRemainsACompatibleFallbackAcrossCommands() { + DeterministicFaultPlan plan = DeterministicFaultPlan.builder() + .plcFailure("B1", 2, ACK_TIMEOUT) + .build(); + SimulatedPlcGateway gateway = new SimulatedPlcGateway(plan); + SimulatedVehicleRuntime vehicle = new SimulatedVehicleRuntime("1", "A", 1); + + assertEquals(ACK, gateway.send("B1", "B1:EMPTY_TO_PICKUP:v1:e0", + vehicle, "A", "B").getReason()); + assertEquals(ACK_TIMEOUT, gateway.send("B1", "B1:LOADED_TO_PLACE:v1:e0", + vehicle, "B", "C").getReason()); + assertEquals(2, gateway.attemptsForTask("B1")); + } +} diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/SimulationAdaptersTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/SimulationAdaptersTest.java index 4a3dc3f..63ebb39 100644 --- a/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/SimulationAdaptersTest.java +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/SimulationAdaptersTest.java @@ -25,6 +25,10 @@ class SimulationAdaptersTest { assertEquals(SEND_FAILURE, plc.send("T1", vehicle, "A", "B").getReason()); SimulatedPlcGateway.PlcResponse ack = plc.send("T1", vehicle, "A", "B"); assertEquals(ACK, ack.getReason()); + assertEquals("T1", ack.getParentTaskId()); + assertEquals("T1", ack.getCommandId()); + assertEquals(2, plc.attemptsForTask("T1")); + assertEquals(2, plc.attemptsForCommand("T1")); assertEquals(ACK, plc.accept(ack, vehicle).getReason()); vehicle.restart();