fix: preserve required shadow comparison evidence

This commit is contained in:
zgw
2026-09-03 15:52:52 +08:00
parent 746a6b5647
commit 9617038e4c
2 changed files with 136 additions and 59 deletions

View File

@@ -22,6 +22,12 @@ import java.util.function.Supplier;
public class ShadowPlanningEngine { public class ShadowPlanningEngine {
private static final int INSTANCE_STALE_SECONDS = 120; private static final int INSTANCE_STALE_SECONDS = 120;
private static final int DECISION_REASON_MAX_LENGTH = 4000; private static final int DECISION_REASON_MAX_LENGTH = 4000;
private static final int IDENTIFIER_MAX_LENGTH = 80;
private static final int WIDE_IDENTIFIER_MAX_LENGTH = 255;
private static final int SNAPSHOT_SYNC_MAX_LENGTH = 40;
private static final int SOURCE_SIGNATURE_MAX_LENGTH = 64;
private static final int RESULT_REASON_MAX_LENGTH = 600;
private static final int NUMBER_MAX_LENGTH = 64;
private static final String PUBLISHED = "PUBLISHED"; private static final String PUBLISHED = "PUBLISHED";
private final SchedulingFacade schedulingFacade; private final SchedulingFacade schedulingFacade;
@@ -124,13 +130,7 @@ public class ShadowPlanningEngine {
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(), observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
observation.sourceSignature(), observation.sourceSignature(),
comparisonCode(observation.proposedSpaceId(), legacySpaceId), comparisonCode(observation.proposedSpaceId(), legacySpaceId),
abbreviate("snapshotGeneration=" + observation.snapshotGeneration() inboundComparisonReason(observation, legacyResultCode, legacySpaceId));
+ "; request=" + observation.requestId()
+ "; shadow=" + observation.resultCode()
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
+ "; legacy=" + nullSafe(legacyResultCode)
+ "; legacySpace=" + nullSafe(legacySpaceId)
+ "; shadowReason=" + nullSafe(observation.reason()), DECISION_REASON_MAX_LENGTH));
requireComparisonInserted(inserted); requireComparisonInserted(inserted);
}); });
} }
@@ -144,13 +144,7 @@ public class ShadowPlanningEngine {
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE", UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(), observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
observation.sourceSignature(), "LEGACY_" + nullSafe(legacyResultCode), observation.sourceSignature(), "LEGACY_" + nullSafe(legacyResultCode),
abbreviate("snapshotGeneration=" + observation.snapshotGeneration() outboundComparisonReason(observation, legacyResultCode));
+ "; request=" + observation.requestId()
+ "; shadow=" + observation.resultCode()
+ "; proposedStock=" + nullSafe(observation.proposedStockId())
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
+ "; legacy=" + nullSafe(legacyResultCode)
+ "; shadowReason=" + nullSafe(observation.reason()), DECISION_REASON_MAX_LENGTH));
requireComparisonInserted(inserted); requireComparisonInserted(inserted);
}); });
} }
@@ -299,42 +293,98 @@ public class ShadowPlanningEngine {
private String outboundDiagnostic(PublishedShadowSnapshot snapshot, String requestId, private String outboundDiagnostic(PublishedShadowSnapshot snapshot, String requestId,
ShadowStockReference reference, String resultReason) { ShadowStockReference reference, String resultReason) {
return snapshotDiagnostic(snapshot) StringBuilder diagnostic = snapshotDiagnostic(snapshot);
+ "; detail=" + nullSafe(requestId) appendField(diagnostic, "detail", requestId, IDENTIFIER_MAX_LENGTH);
+ "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId()) appendField(diagnostic, "rawWarehouse", reference == null ? null : reference.getWarehouseId(),
+ "; project=" + nullSafe(reference == null ? null : reference.getProjectNo()) IDENTIFIER_MAX_LENGTH);
+ "; product=" + nullSafe(reference == null ? null : reference.getProductId()) appendField(diagnostic, "project", reference == null ? null : reference.getProjectNo(),
+ "; tray=" + nullSafe(reference == null ? null : reference.getTrayNo()) WIDE_IDENTIFIER_MAX_LENGTH);
+ "; stock=" + nullSafe(reference == null ? null : reference.getStockId()) appendField(diagnostic, "product", reference == null ? null : reference.getProductId(),
+ "; space=" + nullSafe(reference == null ? null : reference.getSpaceId()) WIDE_IDENTIFIER_MAX_LENGTH);
+ "; requestedQuantity=" + objectOrEmpty( appendField(diagnostic, "tray", reference == null ? null : reference.getTrayNo(),
reference == null ? null : reference.getRequestedQuantity()) WIDE_IDENTIFIER_MAX_LENGTH);
+ "; inputSnapshotSync=" + nullSafe( appendField(diagnostic, "stock", reference == null ? null : reference.getStockId(),
reference == null ? null : reference.getSnapshotSynchronizedAt()) IDENTIFIER_MAX_LENGTH);
+ "; shadowMatches=" + numberOrUnknown( appendField(diagnostic, "space", reference == null ? null : reference.getSpaceId(),
reference == null ? null : reference.getShadowMatchCount()) IDENTIFIER_MAX_LENGTH);
+ "; reason=" + abbreviate(nullSafe(resultReason), 600); appendField(diagnostic, "requestedQuantity", objectOrEmpty(
reference == null ? null : reference.getRequestedQuantity()), NUMBER_MAX_LENGTH);
appendField(diagnostic, "inputSnapshotSync",
reference == null ? null : reference.getSnapshotSynchronizedAt(), SNAPSHOT_SYNC_MAX_LENGTH);
appendField(diagnostic, "shadowMatches", numberOrUnknown(
reference == null ? null : reference.getShadowMatchCount()), NUMBER_MAX_LENGTH);
appendField(diagnostic, "reason", resultReason, RESULT_REASON_MAX_LENGTH);
return diagnostic.toString();
} }
private String inboundDiagnostic(PublishedShadowSnapshot snapshot, String taskNo, String trayNo, private String inboundDiagnostic(PublishedShadowSnapshot snapshot, String taskNo, String trayNo,
ShadowInboundReference reference, String resultReason) { ShadowInboundReference reference, String resultReason) {
return snapshotDiagnostic(snapshot) StringBuilder diagnostic = snapshotDiagnostic(snapshot);
+ "; task=" + nullSafe(taskNo) appendField(diagnostic, "task", taskNo, IDENTIFIER_MAX_LENGTH);
+ "; tray=" + nullSafe(trayNo) appendField(diagnostic, "tray", trayNo, WIDE_IDENTIFIER_MAX_LENGTH);
+ "; request=" + nullSafe(reference == null ? null : reference.getRequestId()) appendField(diagnostic, "request", reference == null ? null : reference.getRequestId(),
+ "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId()) IDENTIFIER_MAX_LENGTH);
+ "; project=" + nullSafe(reference == null ? null : reference.getProjectNo()) appendField(diagnostic, "rawWarehouse", reference == null ? null : reference.getWarehouseId(),
+ "; product=" + nullSafe(reference == null ? null : reference.getProductId()) IDENTIFIER_MAX_LENGTH);
+ "; lot=" + nullSafe(reference == null ? null : reference.getLotNo()) appendField(diagnostic, "project", reference == null ? null : reference.getProjectNo(),
+ "; shadowCandidates=" + numberOrUnknown( WIDE_IDENTIFIER_MAX_LENGTH);
reference == null ? null : reference.getShadowCandidateCount()) appendField(diagnostic, "product", reference == null ? null : reference.getProductId(),
+ "; reason=" + abbreviate(nullSafe(resultReason), 600); WIDE_IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "lot", reference == null ? null : reference.getLotNo(),
IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "shadowCandidates", numberOrUnknown(
reference == null ? null : reference.getShadowCandidateCount()), NUMBER_MAX_LENGTH);
appendField(diagnostic, "reason", resultReason, RESULT_REASON_MAX_LENGTH);
return diagnostic.toString();
} }
private String snapshotDiagnostic(PublishedShadowSnapshot snapshot) { private String inboundComparisonReason(InboundShadowObservation observation,
return "snapshotGeneration=" + snapshot.generation() String legacyResultCode, String legacySpaceId) {
+ "; snapshotSync=" + instantOrEmpty(snapshot.synchronizedAt()) StringBuilder reason = new StringBuilder();
+ "; sourceSignature=" + nullSafe(snapshot.sourceSignature()); appendField(reason, "snapshotGeneration", Long.toString(observation.snapshotGeneration()),
NUMBER_MAX_LENGTH);
appendField(reason, "request", observation.requestId(), IDENTIFIER_MAX_LENGTH);
appendField(reason, "shadow", observation.resultCode(), IDENTIFIER_MAX_LENGTH);
appendField(reason, "proposedSpace", observation.proposedSpaceId(), IDENTIFIER_MAX_LENGTH);
appendField(reason, "legacy", legacyResultCode, IDENTIFIER_MAX_LENGTH);
appendField(reason, "legacySpace", legacySpaceId, IDENTIFIER_MAX_LENGTH);
appendRemainingField(reason, "shadowReason", observation.reason());
return reason.toString();
}
private String outboundComparisonReason(OutboundShadowObservation observation, String legacyResultCode) {
StringBuilder reason = new StringBuilder();
appendField(reason, "snapshotGeneration", Long.toString(observation.snapshotGeneration()),
NUMBER_MAX_LENGTH);
appendField(reason, "request", observation.requestId(), IDENTIFIER_MAX_LENGTH);
appendField(reason, "shadow", observation.resultCode(), IDENTIFIER_MAX_LENGTH);
appendField(reason, "proposedStock", observation.proposedStockId(), IDENTIFIER_MAX_LENGTH);
appendField(reason, "proposedSpace", observation.proposedSpaceId(), IDENTIFIER_MAX_LENGTH);
appendField(reason, "legacy", legacyResultCode, IDENTIFIER_MAX_LENGTH);
appendRemainingField(reason, "shadowReason", observation.reason());
return reason.toString();
}
private StringBuilder snapshotDiagnostic(PublishedShadowSnapshot snapshot) {
StringBuilder diagnostic = new StringBuilder();
appendField(diagnostic, "snapshotGeneration", Long.toString(snapshot.generation()), NUMBER_MAX_LENGTH);
appendField(diagnostic, "snapshotSync", instantOrEmpty(snapshot.synchronizedAt()),
SNAPSHOT_SYNC_MAX_LENGTH);
appendField(diagnostic, "sourceSignature", snapshot.sourceSignature(), SOURCE_SIGNATURE_MAX_LENGTH);
return diagnostic;
}
private void appendField(StringBuilder target, String name, String value, int maxValueLength) {
if (target.length() > 0) {
target.append("; ");
}
target.append(name).append('=').append(abbreviate(nullSafe(value), maxValueLength));
}
private void appendRemainingField(StringBuilder target, String name, String value) {
String prefix = target.length() == 0 ? name + "=" : "; " + name + "=";
int remaining = Math.max(0, DECISION_REASON_MAX_LENGTH - target.length() - prefix.length());
target.append(prefix).append(abbreviate(nullSafe(value), remaining));
} }
private void requireUppercaseHexSignature(String value, String description) { private void requireUppercaseHexSignature(String value, String description) {
@@ -356,7 +406,15 @@ public class ShadowPlanningEngine {
} }
private String abbreviate(String value, int maxLength) { private String abbreviate(String value, int maxLength) {
return value.length() <= maxLength ? value : value.substring(0, maxLength); if (value.length() <= maxLength) {
return value;
}
int end = maxLength;
if (end > 0 && Character.isHighSurrogate(value.charAt(end - 1))
&& Character.isLowSurrogate(value.charAt(end))) {
end--;
}
return value.substring(0, end);
} }
private String comparisonCode(String shadowSpaceId, String legacySpaceId) { private String comparisonCode(String shadowSpaceId, String legacySpaceId) {

View File

@@ -212,13 +212,22 @@ class ShadowPlanningEngineTest {
ShadowInputMapper mapper = mock(ShadowInputMapper.class); ShadowInputMapper mapper = mock(ShadowInputMapper.class);
when(mapper.insertComparison(any(), any(), any(), any(Long.class), when(mapper.insertComparison(any(), any(), any(), any(Long.class),
any(Instant.class), any(), any(), any())).thenReturn(1); any(Instant.class), any(), any(), any())).thenReturn(1);
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper); SchedulingFacade facade = mock(SchedulingFacade.class);
String shadowDiagnostic = "snapshotGeneration=42; snapshotSync=2026-09-03T00:00:00Z" String oversized = "中".repeat(5000);
+ "; sourceSignature=ABC123; project=" + "中".repeat(255) ShadowInboundReference reference = inboundReference(oversized);
+ "; shadowCandidates=17; reason=" + "长".repeat(5000); reference.setRequestId(oversized);
InboundShadowObservation observation = new InboundShadowObservation( reference.setProjectNo(oversized);
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(), reference.setProductId(oversized);
"JOB-1", "READY", "SPACE-9", shadowDiagnostic); reference.setLotNo(oversized);
reference.setShadowCandidateCount(17);
when(mapper.selectInboundInput("01", 42L, "TASK-1", "TRAY-1")).thenReturn(reference);
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
when(facade.planInboundFromPublishedSnapshot(eq("01"), any()))
.thenReturn(new InboundPlanningResult(InboundPlan.assigned(inboundAssignment()),
VersionToken.numeric(8)));
ShadowPlanningEngine engine = engine(facade, mapper);
InboundShadowObservation observation = engine.planInbound(SNAPSHOT, "TASK-1", "TRAY-1");
engine.compareInbound(observation, "READY", "LEGACY-SPACE-9"); engine.compareInbound(observation, "READY", "LEGACY-SPACE-9");
@@ -236,13 +245,23 @@ class ShadowPlanningEngineTest {
ShadowInputMapper mapper = mock(ShadowInputMapper.class); ShadowInputMapper mapper = mock(ShadowInputMapper.class);
when(mapper.insertComparison(any(), any(), any(), any(Long.class), when(mapper.insertComparison(any(), any(), any(), any(Long.class),
any(Instant.class), any(), any(), any())).thenReturn(1); any(Instant.class), any(), any(), any())).thenReturn(1);
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper); SchedulingFacade facade = mock(SchedulingFacade.class);
String shadowDiagnostic = "snapshotGeneration=42; snapshotSync=2026-09-03T00:00:00Z" String oversized = "中".repeat(5000);
+ "; sourceSignature=ABC123; project=" + "中".repeat(255) ShadowStockReference reference = projectReference(oversized);
+ "; shadowMatches=23; reason=" + "长".repeat(5000); reference.setRequestId(oversized);
OutboundShadowObservation observation = new OutboundShadowObservation( reference.setProjectNo(oversized);
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(), reference.setProductId(oversized);
"DETAIL-1", "READY", "STOCK-1", "SPACE-9", shadowDiagnostic); reference.setTrayNo(oversized);
reference.setStockId(oversized);
reference.setSpaceId(oversized);
reference.setShadowMatchCount(23);
when(mapper.selectProjectOutboundReference("01", 42L, "DETAIL-1")).thenReturn(reference);
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any()))
.thenReturn(OutboundPlan.ready(outboundAssignment(), TaskCountEstimate.zero()));
ShadowPlanningEngine engine = engine(facade, mapper);
OutboundShadowObservation observation = engine.planProjectOutbound(SNAPSHOT, "DETAIL-1");
engine.compareOutbound(observation, "10"); engine.compareOutbound(observation, "10");