fix(shadow): respect queue phases and compress input evidence

This commit is contained in:
zgw
2026-09-08 11:23:50 +08:00
parent 414b3bf4a9
commit af8b5f1830
40 changed files with 1035 additions and 46 deletions

View File

@@ -168,6 +168,7 @@ public final class Ft546ShadowCycle {
"INVALID_COMMAND_FACTS:" + message(failure), progress);
}
progress.enter("EVIDENCE_BUILD");
progress.dispatch(batch, snapshot, result.getDecisions());
List<ShadowCycleEvidence> evidence = evidenceCollector.collect(batch,
result.getDecisions(), encoded, clock.instant());
progress.enter("EVIDENCE_SAVE");
@@ -199,7 +200,9 @@ public final class Ft546ShadowCycle {
.getReasonCode();
if ("EVIDENCE_KEY_COLLISION".equals(reasonCode)
|| "EVIDENCE_RUN_COLLISION".equals(reasonCode)
|| "EVIDENCE_BATCH_PAYLOAD_LIMIT".equals(reasonCode)) {
|| "EVIDENCE_BATCH_PAYLOAD_LIMIT".equals(reasonCode)
|| "EVIDENCE_LOGICAL_BATCH_LIMIT".equals(reasonCode)
|| reasonCode.startsWith("INPUT_")) {
return reasonCode;
}
}

View File

@@ -18,6 +18,7 @@ final class ShadowCycleDiagnostics {
private final Map<String, Long> phaseNanos = new LinkedHashMap<String, Long>();
private Map<String, Object> completed;
private boolean cancelled;
private Map<String, Object> dispatch = Collections.emptyMap();
ShadowCycleDiagnostics() { this(System::nanoTime); }
@@ -27,6 +28,43 @@ final class ShadowCycleDiagnostics {
this.phaseStarted = started;
}
synchronized void dispatch(com.ruoyi.shadow.facts.ShadowFactBatch batch,
com.ruoyi.wcs.application.dispatch.DispatchSnapshot snapshot,
com.ruoyi.wcs.application.dispatch.DispatchCycleResult result) {
if (completed != null || cancelled) return;
Map<String, Object> values = new LinkedHashMap<String, Object>();
values.put("eligiblePhases", snapshot.getReadySteps().size());
values.put("granted", result.getGranted().size());
values.put("held", result.getHeld().size());
values.put("yielded", result.getYielded().size());
Map<String, Integer> observations = new LinkedHashMap<String, Integer>();
for (com.ruoyi.shadow.facts.ShadowQueueFact queue : batch.getQueues()) {
if (!queue.isActive()) continue;
String reason = com.ruoyi.shadow.facts.ShadowQueueDispatchEligibility.observationReason(queue, batch);
if (reason != null) observations.merge(reason, 1, Integer::sum);
}
values.put("observations", Collections.unmodifiableMap(observations));
int safe = 0;
for (String id : snapshot.getTopology().getNodeIds()) {
com.ruoyi.wcs.domain.topology.TopologyNode node = snapshot.getTopology().node(id);
if (node.isSafeWait() || node.isSafeYield()) safe++;
}
values.put("validatedSafePointCount", safe);
values.put("safePointStatus", safe == 0 ? "UNCONFIGURED" : "TOPOLOGY_VALIDATED");
List<Map<String, Object>> holds = new ArrayList<Map<String, Object>>();
for (com.ruoyi.wcs.application.dispatch.HeldDispatch hold : result.getHeld()) {
if (holds.size() == 20) break;
Map<String, Object> row = new LinkedHashMap<String, Object>();
row.put("taskId", hold.getOrderId()); row.put("vehicleId", hold.getVehicleId());
row.put("reasonCode", hold.getReasonCode());
List<String> blockers = new ArrayList<String>(hold.getBlockingVehicleIds());
row.put("blockingVehicleIds", Collections.unmodifiableList(blockers.subList(0, Math.min(20, blockers.size()))));
holds.add(Collections.unmodifiableMap(row));
}
values.put("holdSamples", Collections.unmodifiableList(holds));
dispatch = Collections.unmodifiableMap(values);
}
synchronized void enter(String next) {
if (cancelled || Thread.currentThread().isInterrupted()) {
throw new CancellationException("SHADOW_CYCLE_CANCELLED");
@@ -49,6 +87,7 @@ final class ShadowCycleDiagnostics {
Map<String, Long> timings = new LinkedHashMap<String, Long>();
phaseNanos.forEach((key, value) -> timings.put(key, TimeUnit.NANOSECONDS.toMillis(value)));
snapshot.put("phaseMillis", Collections.unmodifiableMap(timings));
snapshot.put("dispatch", dispatch);
if (worker != null) {
snapshot.put("workerState", worker.getState().name());
List<String> stack = new ArrayList<String>();

View File

@@ -39,12 +39,20 @@ public final class ShadowDispatchSnapshotAssembler {
private final long topologyVersion;
private final Function<EffectiveTopology, ShadowTrafficState> stateLoader;
private final Clock clock;
private final java.util.Set<String> safeWaitSites;
private final java.util.Set<String> safeYieldSites;
public ShadowDispatchSnapshotAssembler(
String warehouseCode,
long topologyVersion,
Function<EffectiveTopology, ShadowTrafficState> stateLoader,
Clock clock) {
this(warehouseCode, topologyVersion, stateLoader, clock, Collections.emptySet(), Collections.emptySet());
}
public ShadowDispatchSnapshotAssembler(String warehouseCode, long topologyVersion,
Function<EffectiveTopology, ShadowTrafficState> stateLoader, Clock clock,
java.util.Set<String> safeWaitSites, java.util.Set<String> safeYieldSites) {
this.warehouseCode = required(warehouseCode, "warehouseCode");
if (topologyVersion <= 0L) {
throw new IllegalArgumentException(
@@ -53,6 +61,8 @@ public final class ShadowDispatchSnapshotAssembler {
this.topologyVersion = topologyVersion;
this.stateLoader = Objects.requireNonNull(stateLoader, "stateLoader");
this.clock = Objects.requireNonNull(clock, "clock");
this.safeWaitSites = safeSites(safeWaitSites);
this.safeYieldSites = safeSites(safeYieldSites);
}
public DispatchSnapshot assemble(ShadowFactBatch batch) {
@@ -74,7 +84,8 @@ public final class ShadowDispatchSnapshotAssembler {
continue;
}
nodes.add(new TopologyNode(fact.getSiteId(), floor(fact.getZ()),
fact.getX(), fact.getY(), 1, false, false));
fact.getX(), fact.getY(), 1, safeWaitSites.contains(fact.getSiteId()),
safeYieldSites.contains(fact.getSiteId())));
String coordinate = coordinate(fact.getX(), fact.getY(), fact.getZ());
if (coordinates.put(coordinate, fact) != null) {
throw new IllegalArgumentException(
@@ -96,7 +107,26 @@ public final class ShadowDispatchSnapshotAssembler {
Collections.<ResourceDefinition>emptyList(),
Collections.<String>emptyList(),
Collections.<String>emptyList());
return new TopologyCompiler().compile(definition);
EffectiveTopology topology = new TopologyCompiler().compile(definition);
java.util.Set<String> configured = new java.util.LinkedHashSet<String>(safeWaitSites);
configured.addAll(safeYieldSites);
for (String id : configured) {
if (!topology.containsNode(id) || topology.neighbors(id).size() < 2) {
throw new IllegalArgumentException("INVALID_CONFIGURED_SAFE_SITE:" + id);
}
}
return topology;
}
public static java.util.Set<String> safeSites(java.util.Set<String> values) {
java.util.Set<String> normalized = new java.util.TreeSet<String>();
for (String value : Objects.requireNonNull(values, "safeSites")) {
String id = required(value, "safeSite");
if (!id.matches("[0-9]{1,6}") || Integer.parseInt(id) == 0)
throw new IllegalArgumentException("INVALID_CONFIGURED_SAFE_SITE:" + id);
normalized.add(id);
}
return Collections.unmodifiableSet(normalized);
}
private List<DispatchTask> tasks(ShadowFactBatch batch,
@@ -104,10 +134,10 @@ public final class ShadowDispatchSnapshotAssembler {
List<DispatchTask> tasks = new ArrayList<DispatchTask>();
Instant now = clock.instant();
for (ShadowQueueFact fact : batch.getQueues()) {
if (!ACTIVE_QUEUE.equals(fact.getTableName())) {
if (com.ruoyi.shadow.facts.ShadowQueueDispatchEligibility.observationReason(fact, batch) != null) {
continue;
}
String source = required(ShadowQueueEndpoints.start(fact, batch), "startSite");
String source = required(ShadowQueueEndpoints.dispatchStart(fact, batch), "startSite");
String destination = required(ShadowQueueEndpoints.end(fact, batch), "endSite");
if (!topology.containsNode(source)
|| !topology.containsNode(destination)) {
@@ -130,6 +160,10 @@ public final class ShadowDispatchSnapshotAssembler {
values.put("parentTaskId", parentTaskId);
values.put("phase", phase(required(fact.getTaskType(), "taskType")));
values.put("routeVersion", Long.toString(topologyVersion));
String boundVehicle = optional(fact.getAgvId());
if (boundVehicle != null && !"0".equals(boundVehicle)) {
values.put("shadowBoundVehicleId", boundVehicle);
}
values.put("taskNo", positiveNumber(parentTaskId, "taskNo"));
short taskType = parsedShort(required(fact.getDefault02(), "default02"),
"default02");

View File

@@ -0,0 +1,31 @@
package com.ruoyi.shadow.kernel;
import com.ruoyi.wcs.domain.dispatch.DispatchTask;
import com.ruoyi.wcs.domain.dispatch.VehicleAssignmentDecision;
import com.ruoyi.wcs.domain.dispatch.VehicleAssignmentPolicy;
import com.ruoyi.wcs.domain.equipment.VehicleSnapshot;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/** Keeps the source phase's vehicle binding without relaxing the normal safety gates. */
public final class ShadowVehicleAssignmentPolicy implements VehicleAssignmentPolicy {
private final VehicleAssignmentPolicy delegate;
public ShadowVehicleAssignmentPolicy(VehicleAssignmentPolicy delegate) {
this.delegate = Objects.requireNonNull(delegate, "delegate");
}
@Override
public VehicleAssignmentDecision assign(DispatchTask task, List<VehicleSnapshot> vehicles,
EffectiveTopology topology) {
String bound = task.getCommandParameters().get("shadowBoundVehicleId");
if (bound == null) return delegate.assign(task, vehicles, topology);
List<VehicleSnapshot> candidates = new ArrayList<VehicleSnapshot>();
for (VehicleSnapshot vehicle : vehicles) {
if (bound.equals(vehicle.getVehicleId())) candidates.add(vehicle);
}
return delegate.assign(task, candidates, topology);
}
}