feat(javaTest): scope PLC retries by business phase
This commit is contained in:
@@ -44,7 +44,8 @@ public final class DeterministicFaultPlan {
|
||||
public VehicleEventType getType() { return type; }
|
||||
}
|
||||
|
||||
private final Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plcFailures;
|
||||
private final Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plcTaskFailures;
|
||||
private final Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plcCommandFailures;
|
||||
private final Map<Long, List<VehicleEvent>> vehicleEvents;
|
||||
|
||||
/**
|
||||
@@ -55,9 +56,11 @@ public final class DeterministicFaultPlan {
|
||||
* @param vehicleEvents 执行该处理所需的 vehicle events 输入。
|
||||
*/
|
||||
private DeterministicFaultPlan(
|
||||
Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plcFailures,
|
||||
Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plcTaskFailures,
|
||||
Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plcCommandFailures,
|
||||
Map<Long, List<VehicleEvent>> 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<Integer, SimulatedPlcGateway.PlcReason> byAttempt = plcFailures.get(taskId);
|
||||
Map<Integer, SimulatedPlcGateway.PlcReason> byAttempt = plcTaskFailures.get(taskId);
|
||||
return byAttempt == null ? null : byAttempt.get(attempt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 先匹配具体业务阶段命令的第 N 次发送;没有阶段配置时,再按父任务总发送次数兼容旧故障计划。
|
||||
*/
|
||||
SimulatedPlcGateway.PlcReason plcReason(String parentTaskId, String commandId,
|
||||
int commandAttempt, int taskAttempt) {
|
||||
Map<Integer, SimulatedPlcGateway.PlcReason> 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<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plcFailures =
|
||||
private final Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plcTaskFailures =
|
||||
new LinkedHashMap<String, Map<Integer, SimulatedPlcGateway.PlcReason>>();
|
||||
private final Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plcCommandFailures =
|
||||
new LinkedHashMap<String, Map<Integer, SimulatedPlcGateway.PlcReason>>();
|
||||
private final Map<Long, List<VehicleEvent>> vehicleEvents =
|
||||
new LinkedHashMap<Long, List<VehicleEvent>>();
|
||||
@@ -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<String, Map<Integer, SimulatedPlcGateway.PlcReason>> 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<Integer, SimulatedPlcGateway.PlcReason> byAttempt = plcFailures.get(taskId);
|
||||
Map<Integer, SimulatedPlcGateway.PlcReason> byAttempt = failures.get(key);
|
||||
if (byAttempt == null) {
|
||||
byAttempt = new LinkedHashMap<Integer, SimulatedPlcGateway.PlcReason>();
|
||||
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<String, Map<Integer, SimulatedPlcGateway.PlcReason>> plc =
|
||||
new LinkedHashMap<String, Map<Integer, SimulatedPlcGateway.PlcReason>>();
|
||||
for (Map.Entry<String, Map<Integer, SimulatedPlcGateway.PlcReason>> entry
|
||||
: plcFailures.entrySet()) {
|
||||
plc.put(entry.getKey(), Collections.unmodifiableMap(
|
||||
new LinkedHashMap<Integer, SimulatedPlcGateway.PlcReason>(entry.getValue())));
|
||||
}
|
||||
Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> taskPlc =
|
||||
immutableFailures(plcTaskFailures);
|
||||
Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> commandPlc =
|
||||
immutableFailures(plcCommandFailures);
|
||||
Map<Long, List<VehicleEvent>> vehicles =
|
||||
new LinkedHashMap<Long, List<VehicleEvent>>();
|
||||
for (Map.Entry<Long, List<VehicleEvent>> entry : vehicleEvents.entrySet()) {
|
||||
vehicles.put(entry.getKey(), Collections.unmodifiableList(
|
||||
new ArrayList<VehicleEvent>(entry.getValue())));
|
||||
}
|
||||
return new DeterministicFaultPlan(Collections.unmodifiableMap(plc),
|
||||
return new DeterministicFaultPlan(taskPlc, commandPlc,
|
||||
Collections.unmodifiableMap(vehicles));
|
||||
}
|
||||
|
||||
private Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> immutableFailures(
|
||||
Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> source) {
|
||||
Map<String, Map<Integer, SimulatedPlcGateway.PlcReason>> copy =
|
||||
new LinkedHashMap<String, Map<Integer, SimulatedPlcGateway.PlcReason>>();
|
||||
for (Map.Entry<String, Map<Integer, SimulatedPlcGateway.PlcReason>> entry
|
||||
: source.entrySet()) {
|
||||
copy.put(entry.getKey(), Collections.unmodifiableMap(
|
||||
new LinkedHashMap<Integer, SimulatedPlcGateway.PlcReason>(entry.getValue())));
|
||||
}
|
||||
return Collections.unmodifiableMap(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
* <p>该方法属于 {@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<String, Integer> attempts = new LinkedHashMap<String, Integer>();
|
||||
private final Map<String, Integer> taskAttempts = new LinkedHashMap<String, Integer>();
|
||||
private final Map<String, Integer> commandAttempts = new LinkedHashMap<String, Integer>();
|
||||
|
||||
/**
|
||||
* 创建 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<String, Integer> getAttempts() {
|
||||
return Collections.unmodifiableMap(attempts);
|
||||
return Collections.unmodifiableMap(taskAttempts);
|
||||
}
|
||||
|
||||
/** 返回只读的阶段命令尝试次数,供报告按阶段汇总。 */
|
||||
public Map<String, Integer> getCommandAttempts() {
|
||||
return Collections.unmodifiableMap(commandAttempts);
|
||||
}
|
||||
|
||||
private int increment(Map<String, Integer> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user