fix(shadow): repair field routing and harden evidence collection

This commit is contained in:
zgw
2026-09-07 12:42:17 +08:00
parent 9829b9c0fa
commit 414b3bf4a9
37 changed files with 1751 additions and 103 deletions

View File

@@ -254,6 +254,41 @@ class Ft546ShadowCycleTest {
assertEquals(now, report.getLastCompletedCycle());
}
@Test
void evidenceCompletionAndReadyTimeIncludeWorkInsteadOfReusingEntryTime() {
Instant now = Instant.parse("2026-09-06T23:59:59Z");
java.util.concurrent.atomic.AtomicReference<Instant> time =
new java.util.concurrent.atomic.AtomicReference<Instant>(now);
Clock clock = new Clock() {
public java.time.ZoneId getZone() { return ZoneOffset.UTC; }
public Clock withZone(java.time.ZoneId zone) { return this; }
public Instant instant() { return time.get(); }
};
RecordingEvidenceRepository evidence = new RecordingEvidenceRepository();
java.util.concurrent.atomic.AtomicLong nanos = new java.util.concurrent.atomic.AtomicLong();
ShadowCycleDiagnostics progress = new ShadowCycleDiagnostics(nanos::get);
evidence.beforeCommit = () -> {
time.set(now.plusSeconds(3));
nanos.set(3000000000L);
};
ShadowStartupReport report = ShadowStartupReport.starting(now, Collections.emptyMap());
Ft546ShadowCycle cycle = evidenceCycle(now, evidence, report, gateway(), clock,
() -> {
time.set(now.plusSeconds(2));
nanos.set(2000000000L);
});
ShadowFactBatch batch = ShadowFactBatch.create(now.minusMillis(50), now,
Collections.singletonList(queueWithoutLegacyAssignment(now)),
Collections.singletonList(vehicle(now)), maps());
assertEquals("COMPARABLE", cycle.run(batch, now, progress).getStatusCode());
assertEquals(now.plusSeconds(2), evidence.saved.get(0).getCompletedAt());
assertEquals(now.plusSeconds(3), report.getLastCompletedCycle());
assertEquals(Long.valueOf(3000), report.getSourceLagMillis());
Map<?, ?> timings = (Map<?, ?>) progress.finish("COMPARABLE", null).get("phaseMillis");
assertEquals(1000L, timings.get("TRANSACTION_COMMIT"));
assertEquals(2000L, timings.get("DISPATCH_AND_STATE_SAVE"));
}
@Test
void evidenceFailureRecordsNoCommandAndStopsLaterCalculation() {
Instant now = Instant.parse("2026-08-14T06:00:00Z");
@@ -345,7 +380,7 @@ class Ft546ShadowCycleTest {
ShadowCycleResult second = cycle.run(batch, now.plusSeconds(1));
assertEquals("DEGRADED", first.getStatusCode());
assertEquals("EVIDENCE_STORAGE_UNAVAILABLE", first.getReasonCode());
assertEquals("FREE_SPACE_BELOW_MINIMUM", first.getReasonCode());
assertEquals("DEGRADED", second.getStatusCode());
assertEquals(1, healthChecks.get());
assertEquals(0, assemblyCalls.get());
@@ -357,6 +392,14 @@ class Ft546ShadowCycleTest {
Instant now, RecordingEvidenceRepository evidence,
ShadowStartupReport report,
RecordingFt546CommandGateway gateway) {
return evidenceCycle(now, evidence, report, gateway,
Clock.fixed(now, ZoneOffset.UTC), () -> { });
}
private static Ft546ShadowCycle evidenceCycle(
Instant now, RecordingEvidenceRepository evidence,
ShadowStartupReport report, RecordingFt546CommandGateway gateway,
Clock clock, Runnable onDispatch) {
RoutePlanner routePlanner = new AStarRoutePlanner();
DispatchCoordinator coordinator = new DispatchCoordinator(
new DefaultVehicleAssignmentPolicy(routePlanner,
@@ -365,6 +408,7 @@ class Ft546ShadowCycleTest {
commit -> {
evidence.dispatchObservedInsideTransaction =
evidence.insideTransaction;
onDispatch.run();
return true;
}, () -> "SHADOW-1");
ReadOnlyDispatchCycle<Ft546SrgvCommandIntent> readOnly =
@@ -384,7 +428,7 @@ class Ft546ShadowCycleTest {
assembler::assemble, readOnly, gateway,
() -> ShadowEvidenceStorageHealth.Check.available(
Long.MAX_VALUE),
evidence, collector(evidence.runId, 3L), report);
evidence, collector(evidence.runId, 3L), report, clock);
}
private static ShadowCycleEvidenceCollector collector(
@@ -542,6 +586,7 @@ class Ft546ShadowCycleTest {
private boolean insideTransaction;
private boolean dispatchObservedInsideTransaction;
private RuntimeException saveFailure;
private Runnable beforeCommit = () -> { };
@Override
public SavedCycle save(ShadowCycleEvidence evidence) {
@@ -570,7 +615,9 @@ class Ft546ShadowCycleTest {
transactionCalls.incrementAndGet();
insideTransaction = true;
try {
return work.get();
T result = work.get();
beforeCommit.run();
return result;
} finally {
insideTransaction = false;
}