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 {
private static final int INSTANCE_STALE_SECONDS = 120;
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 final SchedulingFacade schedulingFacade;
@@ -124,13 +130,7 @@ public class ShadowPlanningEngine {
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
observation.sourceSignature(),
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
abbreviate("snapshotGeneration=" + observation.snapshotGeneration()
+ "; request=" + observation.requestId()
+ "; shadow=" + observation.resultCode()
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
+ "; legacy=" + nullSafe(legacyResultCode)
+ "; legacySpace=" + nullSafe(legacySpaceId)
+ "; shadowReason=" + nullSafe(observation.reason()), DECISION_REASON_MAX_LENGTH));
inboundComparisonReason(observation, legacyResultCode, legacySpaceId));
requireComparisonInserted(inserted);
});
}
@@ -144,13 +144,7 @@ public class ShadowPlanningEngine {
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
observation.sourceSignature(), "LEGACY_" + nullSafe(legacyResultCode),
abbreviate("snapshotGeneration=" + observation.snapshotGeneration()
+ "; 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));
outboundComparisonReason(observation, legacyResultCode));
requireComparisonInserted(inserted);
});
}
@@ -299,42 +293,98 @@ public class ShadowPlanningEngine {
private String outboundDiagnostic(PublishedShadowSnapshot snapshot, String requestId,
ShadowStockReference reference, String resultReason) {
return snapshotDiagnostic(snapshot)
+ "; detail=" + nullSafe(requestId)
+ "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId())
+ "; project=" + nullSafe(reference == null ? null : reference.getProjectNo())
+ "; product=" + nullSafe(reference == null ? null : reference.getProductId())
+ "; tray=" + nullSafe(reference == null ? null : reference.getTrayNo())
+ "; stock=" + nullSafe(reference == null ? null : reference.getStockId())
+ "; space=" + nullSafe(reference == null ? null : reference.getSpaceId())
+ "; requestedQuantity=" + objectOrEmpty(
reference == null ? null : reference.getRequestedQuantity())
+ "; inputSnapshotSync=" + nullSafe(
reference == null ? null : reference.getSnapshotSynchronizedAt())
+ "; shadowMatches=" + numberOrUnknown(
reference == null ? null : reference.getShadowMatchCount())
+ "; reason=" + abbreviate(nullSafe(resultReason), 600);
StringBuilder diagnostic = snapshotDiagnostic(snapshot);
appendField(diagnostic, "detail", requestId, IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "rawWarehouse", reference == null ? null : reference.getWarehouseId(),
IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "project", reference == null ? null : reference.getProjectNo(),
WIDE_IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "product", reference == null ? null : reference.getProductId(),
WIDE_IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "tray", reference == null ? null : reference.getTrayNo(),
WIDE_IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "stock", reference == null ? null : reference.getStockId(),
IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "space", reference == null ? null : reference.getSpaceId(),
IDENTIFIER_MAX_LENGTH);
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,
ShadowInboundReference reference, String resultReason) {
return snapshotDiagnostic(snapshot)
+ "; task=" + nullSafe(taskNo)
+ "; tray=" + nullSafe(trayNo)
+ "; request=" + nullSafe(reference == null ? null : reference.getRequestId())
+ "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId())
+ "; project=" + nullSafe(reference == null ? null : reference.getProjectNo())
+ "; product=" + nullSafe(reference == null ? null : reference.getProductId())
+ "; lot=" + nullSafe(reference == null ? null : reference.getLotNo())
+ "; shadowCandidates=" + numberOrUnknown(
reference == null ? null : reference.getShadowCandidateCount())
+ "; reason=" + abbreviate(nullSafe(resultReason), 600);
StringBuilder diagnostic = snapshotDiagnostic(snapshot);
appendField(diagnostic, "task", taskNo, IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "tray", trayNo, WIDE_IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "request", reference == null ? null : reference.getRequestId(),
IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "rawWarehouse", reference == null ? null : reference.getWarehouseId(),
IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "project", reference == null ? null : reference.getProjectNo(),
WIDE_IDENTIFIER_MAX_LENGTH);
appendField(diagnostic, "product", reference == null ? null : reference.getProductId(),
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) {
return "snapshotGeneration=" + snapshot.generation()
+ "; snapshotSync=" + instantOrEmpty(snapshot.synchronizedAt())
+ "; sourceSignature=" + nullSafe(snapshot.sourceSignature());
private String inboundComparisonReason(InboundShadowObservation observation,
String legacyResultCode, String legacySpaceId) {
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, "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) {
@@ -356,7 +406,15 @@ public class ShadowPlanningEngine {
}
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) {

View File

@@ -212,13 +212,22 @@ class ShadowPlanningEngineTest {
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
any(Instant.class), any(), any(), any())).thenReturn(1);
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
String shadowDiagnostic = "snapshotGeneration=42; snapshotSync=2026-09-03T00:00:00Z"
+ "; sourceSignature=ABC123; project=" + "中".repeat(255)
+ "; shadowCandidates=17; reason=" + "长".repeat(5000);
InboundShadowObservation observation = new InboundShadowObservation(
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
"JOB-1", "READY", "SPACE-9", shadowDiagnostic);
SchedulingFacade facade = mock(SchedulingFacade.class);
String oversized = "中".repeat(5000);
ShadowInboundReference reference = inboundReference(oversized);
reference.setRequestId(oversized);
reference.setProjectNo(oversized);
reference.setProductId(oversized);
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");
@@ -236,13 +245,23 @@ class ShadowPlanningEngineTest {
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
any(Instant.class), any(), any(), any())).thenReturn(1);
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
String shadowDiagnostic = "snapshotGeneration=42; snapshotSync=2026-09-03T00:00:00Z"
+ "; sourceSignature=ABC123; project=" + "中".repeat(255)
+ "; shadowMatches=23; reason=" + "长".repeat(5000);
OutboundShadowObservation observation = new OutboundShadowObservation(
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
"DETAIL-1", "READY", "STOCK-1", "SPACE-9", shadowDiagnostic);
SchedulingFacade facade = mock(SchedulingFacade.class);
String oversized = "中".repeat(5000);
ShadowStockReference reference = projectReference(oversized);
reference.setRequestId(oversized);
reference.setProjectNo(oversized);
reference.setProductId(oversized);
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");