fix(shadow): repair field routing and harden evidence collection
This commit is contained in:
@@ -16,6 +16,7 @@ import com.ruoyi.wcs.application.ShadowDispatchResult;
|
||||
import com.ruoyi.wcs.application.dispatch.DispatchSnapshot;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -30,6 +31,7 @@ public final class Ft546ShadowCycle {
|
||||
private final ShadowEvidenceRepository evidenceRepository;
|
||||
private final ShadowCycleEvidenceCollector evidenceCollector;
|
||||
private final ShadowStartupReport statusReport;
|
||||
private final Clock clock;
|
||||
private final AtomicReference<String> degradedReason =
|
||||
new AtomicReference<String>();
|
||||
|
||||
@@ -48,6 +50,7 @@ public final class Ft546ShadowCycle {
|
||||
this.evidenceRepository = null;
|
||||
this.evidenceCollector = null;
|
||||
this.statusReport = null;
|
||||
this.clock = Clock.systemUTC();
|
||||
}
|
||||
|
||||
public Ft546ShadowCycle(
|
||||
@@ -59,6 +62,19 @@ public final class Ft546ShadowCycle {
|
||||
ShadowEvidenceRepository evidenceRepository,
|
||||
ShadowCycleEvidenceCollector evidenceCollector,
|
||||
ShadowStartupReport statusReport) {
|
||||
this(validator, assembler, dispatchCycle, commandGateway, storageHealth,
|
||||
evidenceRepository, evidenceCollector, statusReport, Clock.systemUTC());
|
||||
}
|
||||
|
||||
public Ft546ShadowCycle(
|
||||
ShadowFactBatchValidator validator,
|
||||
Function<ShadowFactBatch, DispatchSnapshot> assembler,
|
||||
ReadOnlyDispatchCycle<Ft546SrgvCommandIntent> dispatchCycle,
|
||||
RecordingFt546CommandGateway commandGateway,
|
||||
ShadowEvidenceStorageHealth storageHealth,
|
||||
ShadowEvidenceRepository evidenceRepository,
|
||||
ShadowCycleEvidenceCollector evidenceCollector,
|
||||
ShadowStartupReport statusReport, Clock clock) {
|
||||
this.validator = Objects.requireNonNull(validator, "validator");
|
||||
this.assembler = Objects.requireNonNull(assembler, "assembler");
|
||||
this.dispatchCycle = Objects.requireNonNull(dispatchCycle,
|
||||
@@ -73,86 +89,103 @@ public final class Ft546ShadowCycle {
|
||||
"evidenceCollector");
|
||||
this.statusReport = Objects.requireNonNull(statusReport,
|
||||
"statusReport");
|
||||
this.clock = Objects.requireNonNull(clock, "clock");
|
||||
}
|
||||
|
||||
public ShadowCycleResult run(ShadowFactBatch batch, Instant now) {
|
||||
return run(batch, now, new ShadowCycleDiagnostics());
|
||||
}
|
||||
|
||||
ShadowCycleResult run(ShadowFactBatch batch, Instant now, ShadowCycleDiagnostics progress) {
|
||||
if (evidenceRepository == null) {
|
||||
return runWithoutEvidence(batch, now);
|
||||
return runWithoutEvidence(batch, now, progress);
|
||||
}
|
||||
String stoppedReason = degradedReason.get();
|
||||
if (stoppedReason != null) {
|
||||
return ShadowCycleResult.degraded(stoppedReason);
|
||||
}
|
||||
progress.enter("STORAGE_HEALTH");
|
||||
ShadowEvidenceStorageHealth.Check health = storageHealth.verify();
|
||||
if (!health.isAvailable()) {
|
||||
return degrade(now);
|
||||
return degrade(health.getReasonCode(), clock.instant());
|
||||
}
|
||||
try {
|
||||
progress.enter("TRANSACTION_BEGIN");
|
||||
ShadowCycleResult result = evidenceRepository.inTransaction(
|
||||
() -> runAndPersist(batch, now));
|
||||
() -> {
|
||||
ShadowCycleResult saved = runAndPersist(batch, now, progress);
|
||||
progress.enter("TRANSACTION_COMMIT");
|
||||
return saved;
|
||||
});
|
||||
progress.enter("RECORDING");
|
||||
commandGateway.rememberAll(result.getEncodedCommands());
|
||||
Instant completedAt = clock.instant();
|
||||
Long lag = batch.getDatabaseCapturedAt() == null ? null
|
||||
: Long.valueOf(Math.max(0L,
|
||||
java.time.Duration.between(
|
||||
batch.getDatabaseCapturedAt(), now).toMillis()));
|
||||
statusReport.markReady(now, lag);
|
||||
batch.getDatabaseCapturedAt(), completedAt).toMillis()));
|
||||
statusReport.markReady(completedAt, lag);
|
||||
return result;
|
||||
} catch (RuntimeException evidenceFailure) {
|
||||
return degrade(failureReason(evidenceFailure), now);
|
||||
return degrade(failureReason(evidenceFailure), clock.instant());
|
||||
}
|
||||
}
|
||||
|
||||
private ShadowCycleResult runAndPersist(ShadowFactBatch batch,
|
||||
Instant now) {
|
||||
Instant now, ShadowCycleDiagnostics progress) {
|
||||
progress.enter("VALIDATION");
|
||||
ShadowFactValidation validation = validator.validate(batch, now);
|
||||
if (validation.getStatus()
|
||||
!= ShadowFactValidation.Status.COMPARABLE) {
|
||||
return persistInsufficient(batch, validation.getReasonCode(), now);
|
||||
return persistInsufficient(batch, validation.getReasonCode(), progress);
|
||||
}
|
||||
final DispatchSnapshot snapshot;
|
||||
progress.enter("ASSEMBLY_AND_STATE_LOAD");
|
||||
try {
|
||||
snapshot = assembler.apply(batch);
|
||||
} catch (RuntimeException failure) {
|
||||
return persistInsufficient(batch,
|
||||
"INVALID_KERNEL_FACTS:" + message(failure), now);
|
||||
"INVALID_KERNEL_FACTS:" + message(failure), progress);
|
||||
}
|
||||
ShadowDispatchResult<Ft546SrgvCommandIntent> result;
|
||||
progress.enter("DISPATCH_AND_STATE_SAVE");
|
||||
try {
|
||||
result = dispatchCycle.run(snapshot);
|
||||
} catch (IllegalArgumentException failure) {
|
||||
return persistInsufficient(batch,
|
||||
"INVALID_COMMAND_FACTS:" + message(failure), now);
|
||||
"INVALID_COMMAND_FACTS:" + message(failure), progress);
|
||||
}
|
||||
if (result.getDecisions().isRetrySnapshotChanged()) {
|
||||
List<ShadowCycleEvidence> evidence = evidenceCollector.insufficient(
|
||||
batch, "SHADOW_TRAFFIC_STATE_CHANGED", now);
|
||||
evidenceRepository.saveAll(evidence);
|
||||
persistInsufficient(batch, "SHADOW_TRAFFIC_STATE_CHANGED", progress);
|
||||
return ShadowCycleResult.retry(result.getDecisions());
|
||||
}
|
||||
final List<Ft546EncodedCommand> encoded;
|
||||
progress.enter("ENCODING");
|
||||
try {
|
||||
encoded = commandGateway.previewAll(result.getIntents());
|
||||
} catch (RuntimeException failure) {
|
||||
return persistInsufficient(batch,
|
||||
"INVALID_COMMAND_FACTS:" + message(failure), now);
|
||||
"INVALID_COMMAND_FACTS:" + message(failure), progress);
|
||||
}
|
||||
evidenceRepository.saveAll(evidenceCollector.collect(batch,
|
||||
result.getDecisions(), encoded, now));
|
||||
progress.enter("EVIDENCE_BUILD");
|
||||
List<ShadowCycleEvidence> evidence = evidenceCollector.collect(batch,
|
||||
result.getDecisions(), encoded, clock.instant());
|
||||
progress.enter("EVIDENCE_SAVE");
|
||||
evidenceRepository.saveAll(evidence);
|
||||
return ShadowCycleResult.comparable(result.getDecisions(),
|
||||
result.getIntents(), encoded);
|
||||
}
|
||||
|
||||
private ShadowCycleResult persistInsufficient(
|
||||
ShadowFactBatch batch, String reasonCode, Instant now) {
|
||||
evidenceRepository.saveAll(evidenceCollector.insufficient(batch,
|
||||
reasonCode, now));
|
||||
ShadowFactBatch batch, String reasonCode, ShadowCycleDiagnostics progress) {
|
||||
progress.enter("EVIDENCE_BUILD");
|
||||
List<ShadowCycleEvidence> evidence = evidenceCollector.insufficient(batch,
|
||||
reasonCode, clock.instant());
|
||||
progress.enter("EVIDENCE_SAVE");
|
||||
evidenceRepository.saveAll(evidence);
|
||||
return ShadowCycleResult.insufficient(reasonCode);
|
||||
}
|
||||
|
||||
private ShadowCycleResult degrade(Instant now) {
|
||||
return degrade("EVIDENCE_STORAGE_UNAVAILABLE", now);
|
||||
}
|
||||
|
||||
private ShadowCycleResult degrade(String reasonCode, Instant now) {
|
||||
degradedReason.compareAndSet(null, reasonCode);
|
||||
String stoppedReason = degradedReason.get();
|
||||
@@ -165,7 +198,8 @@ public final class Ft546ShadowCycle {
|
||||
String reasonCode = ((ShadowEvidenceException) failure)
|
||||
.getReasonCode();
|
||||
if ("EVIDENCE_KEY_COLLISION".equals(reasonCode)
|
||||
|| "EVIDENCE_RUN_COLLISION".equals(reasonCode)) {
|
||||
|| "EVIDENCE_RUN_COLLISION".equals(reasonCode)
|
||||
|| "EVIDENCE_BATCH_PAYLOAD_LIMIT".equals(reasonCode)) {
|
||||
return reasonCode;
|
||||
}
|
||||
}
|
||||
@@ -173,13 +207,15 @@ public final class Ft546ShadowCycle {
|
||||
}
|
||||
|
||||
private ShadowCycleResult runWithoutEvidence(ShadowFactBatch batch,
|
||||
Instant now) {
|
||||
Instant now, ShadowCycleDiagnostics progress) {
|
||||
progress.enter("VALIDATION");
|
||||
ShadowFactValidation validation = validator.validate(batch, now);
|
||||
if (validation.getStatus()
|
||||
!= ShadowFactValidation.Status.COMPARABLE) {
|
||||
return ShadowCycleResult.insufficient(validation.getReasonCode());
|
||||
}
|
||||
final DispatchSnapshot snapshot;
|
||||
progress.enter("ASSEMBLY_AND_STATE_LOAD");
|
||||
try {
|
||||
snapshot = assembler.apply(batch);
|
||||
} catch (RuntimeException failure) {
|
||||
@@ -187,6 +223,7 @@ public final class Ft546ShadowCycle {
|
||||
"INVALID_KERNEL_FACTS:" + message(failure));
|
||||
}
|
||||
ShadowDispatchResult<Ft546SrgvCommandIntent> result;
|
||||
progress.enter("DISPATCH_AND_STATE_SAVE");
|
||||
try {
|
||||
result = dispatchCycle.run(snapshot);
|
||||
} catch (IllegalArgumentException failure) {
|
||||
@@ -197,6 +234,7 @@ public final class Ft546ShadowCycle {
|
||||
return ShadowCycleResult.retry(result.getDecisions());
|
||||
}
|
||||
final List<Ft546EncodedCommand> encoded;
|
||||
progress.enter("ENCODING");
|
||||
try {
|
||||
encoded = commandGateway.recordAll(result.getIntents());
|
||||
} catch (RuntimeException failure) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -22,6 +23,7 @@ import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class Ft546ShadowScheduler implements AutoCloseable {
|
||||
private static final Logger LOGGER =
|
||||
@@ -39,11 +41,17 @@ public final class Ft546ShadowScheduler implements AutoCloseable {
|
||||
private final Duration cycleDeadline;
|
||||
private final Duration sourceReadDeadline;
|
||||
private final ExecutorService cycleExecutor;
|
||||
private final Consumer<Map<String, Object>> runtimeWriter;
|
||||
private final Object runtimeLock = new Object();
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
private volatile Instant lastAttemptAt;
|
||||
private final AtomicBoolean inFlight = new AtomicBoolean();
|
||||
private final AtomicBoolean permanentlyStopped = new AtomicBoolean();
|
||||
private volatile CycleStage activeStage = CycleStage.IDLE;
|
||||
private volatile ShadowReadCursor cursor = ShadowReadCursor.initial();
|
||||
private volatile Instant nextAttemptAt = Instant.EPOCH;
|
||||
private volatile Thread cycleWorker;
|
||||
private long lastTimingLogNanos;
|
||||
|
||||
public Ft546ShadowScheduler(SqlServerShadowFactReader factReader,
|
||||
Ft546ShadowCycle shadowCycle,
|
||||
@@ -67,6 +75,21 @@ public final class Ft546ShadowScheduler implements AutoCloseable {
|
||||
Clock clock,
|
||||
Duration cycleDeadline,
|
||||
Duration sourceReadDeadline) {
|
||||
this(factReader, shadowCycle, statusReport, clock, cycleDeadline, sourceReadDeadline, state -> { });
|
||||
}
|
||||
|
||||
public Ft546ShadowScheduler(SqlServerShadowFactReader factReader,
|
||||
Ft546ShadowCycle shadowCycle, ShadowStartupReport statusReport,
|
||||
Clock clock, Duration cycleDeadline,
|
||||
Consumer<Map<String, Object>> runtimeWriter) {
|
||||
this(factReader, shadowCycle, statusReport, clock, cycleDeadline,
|
||||
shorter(cycleDeadline, SOURCE_READ_DEADLINE), runtimeWriter);
|
||||
}
|
||||
|
||||
private Ft546ShadowScheduler(SqlServerShadowFactReader factReader,
|
||||
Ft546ShadowCycle shadowCycle, ShadowStartupReport statusReport,
|
||||
Clock clock, Duration cycleDeadline, Duration sourceReadDeadline,
|
||||
Consumer<Map<String, Object>> runtimeWriter) {
|
||||
this.factReader = Objects.requireNonNull(factReader, "factReader");
|
||||
this.shadowCycle = Objects.requireNonNull(shadowCycle, "shadowCycle");
|
||||
this.statusReport = Objects.requireNonNull(statusReport,
|
||||
@@ -74,6 +97,7 @@ public final class Ft546ShadowScheduler implements AutoCloseable {
|
||||
this.clock = Objects.requireNonNull(clock, "clock");
|
||||
this.cycleDeadline = requirePositive(cycleDeadline);
|
||||
this.sourceReadDeadline = requirePositive(sourceReadDeadline);
|
||||
this.runtimeWriter = Objects.requireNonNull(runtimeWriter, "runtimeWriter");
|
||||
this.cycleExecutor = Executors.newSingleThreadExecutor(
|
||||
shadowCycleThreadFactory());
|
||||
}
|
||||
@@ -82,7 +106,11 @@ public final class Ft546ShadowScheduler implements AutoCloseable {
|
||||
initialDelayString = "${shadow.poll.initial-delay-ms:5000}",
|
||||
fixedDelayString = "${shadow.poll.fixed-delay-ms:5000}")
|
||||
public void runScheduledCycle() {
|
||||
runOnce();
|
||||
if (permanentlyStopped.get() || clock.instant().isBefore(nextAttemptAt)) {
|
||||
publishRuntimeState();
|
||||
} else {
|
||||
runOnce();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean runOnce() {
|
||||
@@ -91,11 +119,22 @@ public final class Ft546ShadowScheduler implements AutoCloseable {
|
||||
|| !inFlight.compareAndSet(false, true)) {
|
||||
return false;
|
||||
}
|
||||
Future<CycleAttempt> future = cycleExecutor.submit(
|
||||
() -> executeCycle(now));
|
||||
ShadowCycleDiagnostics diagnostics = new ShadowCycleDiagnostics();
|
||||
lastAttemptAt = now;
|
||||
if (!publishRuntimeState()) {
|
||||
inFlight.set(false);
|
||||
return false;
|
||||
}
|
||||
cycleWorker = null;
|
||||
String outcome = "FAILED";
|
||||
Future<CycleAttempt> future = null;
|
||||
try {
|
||||
if (permanentlyStopped.get()) return false;
|
||||
future = cycleExecutor.submit(() -> executeCycle(now, diagnostics));
|
||||
CycleAttempt attempt = future.get(cycleDeadline.toMillis(),
|
||||
TimeUnit.MILLISECONDS);
|
||||
outcome = attempt.result.getStatusCode();
|
||||
statusReport.recordCycleDiagnostics(diagnostics.finish(outcome, null));
|
||||
if ("DEGRADED".equals(attempt.result.getStatusCode())) {
|
||||
permanentlyStopped.set(true);
|
||||
statusReport.markPermanentlyDegraded(
|
||||
@@ -109,20 +148,34 @@ public final class Ft546ShadowScheduler implements AutoCloseable {
|
||||
return true;
|
||||
} catch (TimeoutException timeout) {
|
||||
CycleStage timedOutStage = activeStage;
|
||||
future.cancel(true);
|
||||
statusReport.recordCycleDiagnostics(diagnostics.cancel("TIMEOUT", cycleWorker));
|
||||
if (future != null) future.cancel(true);
|
||||
return stopPermanently("SHADOW_CYCLE_TIMEOUT", timeout,
|
||||
timedOutStage);
|
||||
} catch (InterruptedException interrupted) {
|
||||
CycleStage interruptedStage = activeStage;
|
||||
future.cancel(true);
|
||||
statusReport.recordCycleDiagnostics(diagnostics.cancel("INTERRUPTED", cycleWorker));
|
||||
if (future != null) future.cancel(true);
|
||||
Thread.currentThread().interrupt();
|
||||
return stopPermanently("SHADOW_CYCLE_INTERRUPTED", interrupted,
|
||||
interruptedStage);
|
||||
} catch (ExecutionException wrapped) {
|
||||
statusReport.recordCycleDiagnostics(diagnostics.finish("FAILED", null));
|
||||
return handleFailure(wrapped.getCause());
|
||||
} catch (java.util.concurrent.RejectedExecutionException shutdown) {
|
||||
return stopPermanently("SHADOW_EXECUTOR_STOPPED", shutdown, activeStage);
|
||||
} finally {
|
||||
Map<String, Object> completed = diagnostics.finish(outcome, null);
|
||||
statusReport.recordCycleDiagnostics(completed);
|
||||
long logTime = System.nanoTime();
|
||||
if (!permanentlyStopped.get() && (lastTimingLogNanos == 0L
|
||||
|| logTime - lastTimingLogNanos >= TimeUnit.MINUTES.toNanos(1))) {
|
||||
LOGGER.info("FT546 shadow cycle diagnostics: {}", completed);
|
||||
lastTimingLogNanos = logTime;
|
||||
}
|
||||
inFlight.set(false);
|
||||
activeStage = CycleStage.IDLE;
|
||||
publishRuntimeState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,17 +190,37 @@ public final class Ft546ShadowScheduler implements AutoCloseable {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.compareAndSet(false, true)) return;
|
||||
permanentlyStopped.set(true);
|
||||
statusReport.markStopped(clock.instant());
|
||||
cycleExecutor.shutdownNow();
|
||||
publishRuntimeState();
|
||||
}
|
||||
|
||||
private CycleAttempt executeCycle(Instant startedAt) {
|
||||
private boolean publishRuntimeState() {
|
||||
synchronized (runtimeLock) {
|
||||
try {
|
||||
runtimeWriter.accept(statusReport.snapshotRuntimeState(
|
||||
clock.instant(), lastAttemptAt, permanentlyStopped.get()));
|
||||
return true;
|
||||
} catch (RuntimeException failure) {
|
||||
permanentlyStopped.set(true);
|
||||
statusReport.markPermanentlyDegraded("EVIDENCE_RUNTIME_WRITE_FAILED", clock.instant());
|
||||
LOGGER.error("FT546 shadow runtime state unavailable; collection stopped", failure);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CycleAttempt executeCycle(Instant startedAt, ShadowCycleDiagnostics diagnostics) {
|
||||
cycleWorker = Thread.currentThread();
|
||||
activeStage = CycleStage.SOURCE_READ;
|
||||
diagnostics.enter("SOURCE_READ");
|
||||
ShadowFactBatch batch = factReader.read(cursor,
|
||||
startedAt.plus(sourceReadDeadline));
|
||||
activeStage = CycleStage.SHADOW_CYCLE;
|
||||
return new CycleAttempt(batch,
|
||||
shadowCycle.run(batch, clock.instant()));
|
||||
shadowCycle.run(batch, clock.instant(), diagnostics));
|
||||
}
|
||||
|
||||
private boolean handleFailure(Throwable failure) {
|
||||
@@ -208,8 +281,8 @@ public final class Ft546ShadowScheduler implements AutoCloseable {
|
||||
permanentlyStopped.set(true);
|
||||
nextAttemptAt = Instant.EPOCH;
|
||||
statusReport.markPermanentlyDegraded(reasonCode, clock.instant());
|
||||
LOGGER.error("FT546 shadow collection stopped: {}; stage={}",
|
||||
reasonCode, stage, failure);
|
||||
LOGGER.error("FT546 shadow collection stopped: {}; stage={}; diagnostics={}",
|
||||
reasonCode, stage, statusReport.getCycleDiagnostics(), failure);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,8 +88,33 @@ public final class JdbcShadowTrafficStateRepository
|
||||
throw new IllegalStateException(
|
||||
"persisted traffic resources do not match topology");
|
||||
}
|
||||
return new ShadowTrafficState(ledger,
|
||||
WaitForGraph.restore(persisted.waitForEdges), row.version,
|
||||
// Only explicitly tagged predictions expire; untagged state is preserved conservatively.
|
||||
List<ResourceReservationState> protectedReservations = new ArrayList<ResourceReservationState>();
|
||||
Set<String> predictedVehicles = new LinkedHashSet<String>();
|
||||
Set<String> protectedVehicles = new LinkedHashSet<String>();
|
||||
for (ResourceReservationState reservation : persisted.ledger.getReservations()) {
|
||||
if (!persisted.predictionIds.contains(reservation.getReservationId())
|
||||
|| reservation.getState() == ReservationState.OCCUPIED
|
||||
|| reservation.isCommandUnknown() || reservation.isRecoveryUnresolved()) {
|
||||
protectedReservations.add(reservation);
|
||||
protectedVehicles.add(reservation.getVehicleId());
|
||||
} else {
|
||||
predictedVehicles.add(reservation.getVehicleId());
|
||||
}
|
||||
}
|
||||
predictedVehicles.removeAll(protectedVehicles);
|
||||
Map<String, Set<String>> remainingWaits = new LinkedHashMap<String, Set<String>>();
|
||||
for (Map.Entry<String, Set<String>> entry : persisted.waitForEdges.entrySet()) {
|
||||
if (!predictedVehicles.contains(entry.getKey())) {
|
||||
Set<String> holders = new LinkedHashSet<String>(entry.getValue());
|
||||
holders.removeAll(predictedVehicles);
|
||||
if (!holders.isEmpty()) remainingWaits.put(entry.getKey(), holders);
|
||||
}
|
||||
}
|
||||
WaitForGraph graph = WaitForGraph.restore(remainingWaits);
|
||||
ledger = ResourceLedger.restore(new ResourceLedgerState(persisted.ledger.getCapacities(),
|
||||
protectedReservations, persisted.ledger.getNextFencingToken()));
|
||||
return new ShadowTrafficState(ledger, graph, row.version,
|
||||
persisted.holdCounts);
|
||||
}
|
||||
|
||||
@@ -110,8 +135,10 @@ public final class JdbcShadowTrafficStateRepository
|
||||
Map<String, Integer> nextHolds = nextHoldCounts(previousHolds,
|
||||
commit.getHeld());
|
||||
long nextVersion = increment(commit.getSnapshotVersion());
|
||||
Set<String> predictionIds = new TreeSet<String>();
|
||||
commit.getGranted().forEach(grant -> predictionIds.addAll(grant.getReservationIds()));
|
||||
String stateJson = encode(commit.getResultingLedger().snapshotState(),
|
||||
commit.getResultingWaitForGraph().snapshotEdges(), nextHolds);
|
||||
commit.getResultingWaitForGraph().snapshotEdges(), nextHolds, predictionIds);
|
||||
Timestamp now = Timestamp.from(clock.instant());
|
||||
if (current == null) {
|
||||
jdbc.update("INSERT INTO shadow_traffic_state "
|
||||
@@ -197,9 +224,12 @@ public final class JdbcShadowTrafficStateRepository
|
||||
|
||||
private String encode(ResourceLedgerState ledger,
|
||||
Map<String, Set<String>> waitForEdges,
|
||||
Map<String, Integer> holdCounts) {
|
||||
Map<String, Integer> holdCounts,
|
||||
Set<String> predictionIds) {
|
||||
Map<String, Object> root = new LinkedHashMap<String, Object>();
|
||||
root.put("schemaVersion", 1);
|
||||
root.put("evaluationMode", "PER_SNAPSHOT_PREDICTION");
|
||||
root.put("predictionReservationIds", new ArrayList<String>(predictionIds));
|
||||
root.put("ledger", ledgerMap(ledger));
|
||||
Map<String, Object> edges = new TreeMap<String, Object>();
|
||||
for (Map.Entry<String, Set<String>> entry : waitForEdges.entrySet()) {
|
||||
@@ -267,7 +297,13 @@ public final class JdbcShadowTrafficStateRepository
|
||||
requiredObject(root, "waitForEdges"));
|
||||
Map<String, Integer> holds = integerMap(
|
||||
requiredObject(root, "holdCounts"));
|
||||
return new PersistedState(ledgerState, edges, holds);
|
||||
Set<String> predictionIds = new LinkedHashSet<String>();
|
||||
if (root.has("predictionReservationIds")) {
|
||||
for (JsonNode id : requiredArray(root, "predictionReservationIds")) {
|
||||
predictionIds.add(requiredNodeText(id, "predictionReservationId"));
|
||||
}
|
||||
}
|
||||
return new PersistedState(ledgerState, edges, holds, predictionIds);
|
||||
} catch (RuntimeException failure) {
|
||||
throw failure;
|
||||
} catch (Exception failure) {
|
||||
@@ -426,13 +462,16 @@ public final class JdbcShadowTrafficStateRepository
|
||||
private final ResourceLedgerState ledger;
|
||||
private final Map<String, Set<String>> waitForEdges;
|
||||
private final Map<String, Integer> holdCounts;
|
||||
private final Set<String> predictionIds;
|
||||
|
||||
private PersistedState(ResourceLedgerState ledger,
|
||||
Map<String, Set<String>> waitForEdges,
|
||||
Map<String, Integer> holdCounts) {
|
||||
Map<String, Integer> holdCounts,
|
||||
Set<String> predictionIds) {
|
||||
this.ledger = ledger;
|
||||
this.waitForEdges = waitForEdges;
|
||||
this.holdCounts = holdCounts;
|
||||
this.predictionIds = predictionIds;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ruoyi.shadow.kernel;
|
||||
|
||||
import com.ruoyi.ft546.protocol.Ft546SrgvCommandIntent;
|
||||
import com.ruoyi.wcs.application.dispatch.GrantedDispatch;
|
||||
import com.ruoyi.wcs.domain.traffic.RoutePlan;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -20,7 +21,8 @@ public final class ShadowCommandIntentFactory {
|
||||
short taskType = signedShort(parameters, "taskType");
|
||||
Integer stage = optionalStage(parameters.get("stage"));
|
||||
Short crossFloorTarget = crossFloorTarget(parameters, taskType, stage);
|
||||
List<Integer> path = path(grant);
|
||||
List<Integer> path = path(grant, taskType == 3 && stage == null
|
||||
&& "LEGACY_PHASE_3".equals(phase));
|
||||
String commandKey = parentTaskId + ':' + phase + ':' + routeVersion
|
||||
+ ':' + requiredText(grant.getStepId(), "stepId");
|
||||
return new Ft546SrgvCommandIntent(
|
||||
@@ -29,20 +31,33 @@ public final class ShadowCommandIntentFactory {
|
||||
taskType, path, crossFloorTarget, stage);
|
||||
}
|
||||
|
||||
private static List<Integer> path(GrantedDispatch grant) {
|
||||
private static List<Integer> path(GrantedDispatch grant, boolean sameSiteCharge) {
|
||||
RoutePlan route = grant.getProtectedRoute();
|
||||
List<String> nodes = route.getNodeIds();
|
||||
if (!route.hasTurnGeometry()) {
|
||||
throw new IllegalArgumentException("SHADOW_ROUTE_GEOMETRY_UNAVAILABLE");
|
||||
}
|
||||
if (nodes.size() == 1 && !sameSiteCharge) {
|
||||
throw new IllegalArgumentException("SHADOW_SINGLE_NODE_TRANSPORT_ROUTE");
|
||||
}
|
||||
List<Integer> points = new ArrayList<Integer>();
|
||||
for (String nodeId : grant.getProtectedRoute().getNodeIds()) {
|
||||
try {
|
||||
points.add(Integer.valueOf(requiredText(nodeId, "route node")));
|
||||
} catch (NumberFormatException failure) {
|
||||
throw new IllegalArgumentException(
|
||||
"route node must be a numeric FT546 site: " + nodeId,
|
||||
failure);
|
||||
}
|
||||
points.add(site(nodes.get(0)));
|
||||
points.add(site(nodes.get(nodes.size() - 1)));
|
||||
for (String turn : route.getTurnNodeIds()) points.add(site(turn));
|
||||
if (points.size() > 8) {
|
||||
throw new IllegalArgumentException("SHADOW_ROUTE_EXCEEDS_SIX_TURNS");
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
private static Integer site(String node) {
|
||||
String value = requiredText(node, "command point");
|
||||
if (!value.matches("[0-9]{1,6}") || Integer.parseInt(value) == 0) {
|
||||
throw new IllegalArgumentException("SHADOW_COMMAND_POINT_NOT_NUMERIC:" + value);
|
||||
}
|
||||
return Integer.valueOf(value);
|
||||
}
|
||||
|
||||
private static Short crossFloorTarget(Map<String, String> parameters,
|
||||
short taskType, Integer stage) {
|
||||
String value = parameters.get("crossFloorTarget");
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ruoyi.shadow.kernel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
/** One bounded diagnostic snapshot per poll, including polls with no task evidence. */
|
||||
final class ShadowCycleDiagnostics {
|
||||
private final LongSupplier ticker;
|
||||
private final long started;
|
||||
private long phaseStarted;
|
||||
private String phase = "QUEUED";
|
||||
private final Map<String, Long> phaseNanos = new LinkedHashMap<String, Long>();
|
||||
private Map<String, Object> completed;
|
||||
private boolean cancelled;
|
||||
|
||||
ShadowCycleDiagnostics() { this(System::nanoTime); }
|
||||
|
||||
ShadowCycleDiagnostics(LongSupplier ticker) {
|
||||
this.ticker = ticker;
|
||||
this.started = ticker.getAsLong();
|
||||
this.phaseStarted = started;
|
||||
}
|
||||
|
||||
synchronized void enter(String next) {
|
||||
if (cancelled || Thread.currentThread().isInterrupted()) {
|
||||
throw new CancellationException("SHADOW_CYCLE_CANCELLED");
|
||||
}
|
||||
if (completed != null) return;
|
||||
long now = ticker.getAsLong();
|
||||
accumulate(now);
|
||||
phase = next;
|
||||
phaseStarted = now;
|
||||
}
|
||||
|
||||
synchronized Map<String, Object> finish(String outcome, Thread worker) {
|
||||
if (completed != null) return completed;
|
||||
long now = ticker.getAsLong();
|
||||
accumulate(now);
|
||||
Map<String, Object> snapshot = new LinkedHashMap<String, Object>();
|
||||
snapshot.put("outcome", outcome);
|
||||
snapshot.put("phase", phase);
|
||||
snapshot.put("elapsedMillis", TimeUnit.NANOSECONDS.toMillis(now - started));
|
||||
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));
|
||||
if (worker != null) {
|
||||
snapshot.put("workerState", worker.getState().name());
|
||||
List<String> stack = new ArrayList<String>();
|
||||
StackTraceElement[] frames = worker.getStackTrace();
|
||||
for (int i = 0; i < Math.min(16, frames.length); i++) stack.add(frames[i].toString());
|
||||
snapshot.put("workerStack", Collections.unmodifiableList(stack));
|
||||
}
|
||||
completed = Collections.unmodifiableMap(snapshot);
|
||||
return completed;
|
||||
}
|
||||
|
||||
synchronized Map<String, Object> cancel(String outcome, Thread worker) {
|
||||
cancelled = true;
|
||||
return finish(outcome, worker);
|
||||
}
|
||||
|
||||
private void accumulate(long now) {
|
||||
phaseNanos.merge(phase, now - phaseStarted, Long::sum);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.shadow.kernel;
|
||||
import com.ruoyi.shadow.facts.ShadowFactBatch;
|
||||
import com.ruoyi.shadow.facts.ShadowMapFact;
|
||||
import com.ruoyi.shadow.facts.ShadowQueueFact;
|
||||
import com.ruoyi.shadow.facts.ShadowQueueEndpoints;
|
||||
import com.ruoyi.shadow.facts.ShadowVehicleFact;
|
||||
import com.ruoyi.wcs.application.dispatch.DispatchSnapshot;
|
||||
import com.ruoyi.wcs.domain.dispatch.DispatchTask;
|
||||
@@ -106,8 +107,8 @@ public final class ShadowDispatchSnapshotAssembler {
|
||||
if (!ACTIVE_QUEUE.equals(fact.getTableName())) {
|
||||
continue;
|
||||
}
|
||||
String source = required(fact.getStartSite(), "startSite");
|
||||
String destination = required(fact.getEndSite(), "endSite");
|
||||
String source = required(ShadowQueueEndpoints.start(fact, batch), "startSite");
|
||||
String destination = required(ShadowQueueEndpoints.end(fact, batch), "endSite");
|
||||
if (!topology.containsNode(source)
|
||||
|| !topology.containsNode(destination)) {
|
||||
throw new IllegalArgumentException(
|
||||
@@ -174,8 +175,8 @@ public final class ShadowDispatchSnapshotAssembler {
|
||||
EnumSet.of(DeviceCapability.FOUR_WAY_MOVE,
|
||||
DeviceCapability.LOAD_HANDLING,
|
||||
DeviceCapability.LIFT_TRANSFER));
|
||||
boolean connectedAndSendable = "1".equals(optional(
|
||||
fact.getAgvStatus()))
|
||||
// Read-only projection of the legacy PLC gate; agvStatus is not refreshed by PLC polling.
|
||||
boolean connectedAndSendable = Integer.valueOf(5).equals(fact.getWorkMode())
|
||||
&& Integer.valueOf(1).equals(fact.getAllowSend());
|
||||
boolean healthy = zeroOrMissing(fact.getAlarm())
|
||||
&& zeroOrMissing(fact.getErrorCode());
|
||||
|
||||
Reference in New Issue
Block a user