fix: enforce stable shadow snapshot boundaries
This commit is contained in:
@@ -1,26 +1,43 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record InboundShadowObservation(
|
||||
boolean observed,
|
||||
long snapshotGeneration,
|
||||
String warehouseId,
|
||||
Instant snapshotSynchronizedAt,
|
||||
String sourceSignature,
|
||||
String requestId,
|
||||
String resultCode,
|
||||
String proposedSpaceId,
|
||||
String reason) {
|
||||
|
||||
public InboundShadowObservation(boolean observed, long snapshotGeneration, String warehouseId,
|
||||
String requestId, String resultCode, String proposedSpaceId, String reason) {
|
||||
this(observed, snapshotGeneration, warehouseId, null, null,
|
||||
requestId, resultCode, proposedSpaceId, reason);
|
||||
}
|
||||
|
||||
public InboundShadowObservation(boolean observed, String warehouseId, String requestId,
|
||||
String resultCode, String proposedSpaceId, String reason) {
|
||||
this(observed, 0L, warehouseId, requestId, resultCode, proposedSpaceId, reason);
|
||||
this(observed, 0L, warehouseId, null, null, requestId, resultCode, proposedSpaceId, reason);
|
||||
}
|
||||
|
||||
public static InboundShadowObservation empty() {
|
||||
return new InboundShadowObservation(false, 0L, null, null, "NOT_OBSERVED", null, null);
|
||||
return new InboundShadowObservation(false, 0L, null, null, null,
|
||||
null, "NOT_OBSERVED", null, null);
|
||||
}
|
||||
|
||||
public static InboundShadowObservation unavailable(long snapshotGeneration, String warehouseId,
|
||||
String requestId, String resultCode, String reason) {
|
||||
return new InboundShadowObservation(true, snapshotGeneration, warehouseId, requestId,
|
||||
resultCode, null, reason);
|
||||
return new InboundShadowObservation(true, snapshotGeneration, warehouseId, null, null,
|
||||
requestId, resultCode, null, reason);
|
||||
}
|
||||
|
||||
public static InboundShadowObservation unavailable(PublishedShadowSnapshot snapshot,
|
||||
String requestId, String resultCode, String reason) {
|
||||
return new InboundShadowObservation(true, snapshot.generation(), snapshot.warehouseId(),
|
||||
snapshot.synchronizedAt(), snapshot.sourceSignature(), requestId, resultCode, null, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record OutboundShadowObservation(
|
||||
boolean observed,
|
||||
long snapshotGeneration,
|
||||
String warehouseId,
|
||||
Instant snapshotSynchronizedAt,
|
||||
String sourceSignature,
|
||||
String requestId,
|
||||
String resultCode,
|
||||
String proposedStockId,
|
||||
String proposedSpaceId,
|
||||
String reason) {
|
||||
|
||||
public OutboundShadowObservation(boolean observed, long snapshotGeneration, String warehouseId,
|
||||
String requestId, String resultCode, String proposedStockId, String proposedSpaceId,
|
||||
String reason) {
|
||||
this(observed, snapshotGeneration, warehouseId, null, null,
|
||||
requestId, resultCode, proposedStockId, proposedSpaceId, reason);
|
||||
}
|
||||
|
||||
public OutboundShadowObservation(boolean observed, String warehouseId, String requestId,
|
||||
String resultCode, String proposedStockId, String proposedSpaceId, String reason) {
|
||||
this(observed, 0L, warehouseId, requestId, resultCode, proposedStockId, proposedSpaceId, reason);
|
||||
this(observed, 0L, warehouseId, null, null,
|
||||
requestId, resultCode, proposedStockId, proposedSpaceId, reason);
|
||||
}
|
||||
|
||||
public static OutboundShadowObservation empty() {
|
||||
return new OutboundShadowObservation(false, 0L, null, null, "NOT_OBSERVED", null, null, null);
|
||||
return new OutboundShadowObservation(false, 0L, null, null, null,
|
||||
null, "NOT_OBSERVED", null, null, null);
|
||||
}
|
||||
|
||||
public static OutboundShadowObservation unavailable(long snapshotGeneration, String warehouseId,
|
||||
String requestId, String resultCode, String reason) {
|
||||
return new OutboundShadowObservation(true, snapshotGeneration, warehouseId, requestId,
|
||||
resultCode, null, null, reason);
|
||||
return new OutboundShadowObservation(true, snapshotGeneration, warehouseId, null, null,
|
||||
requestId, resultCode, null, null, reason);
|
||||
}
|
||||
|
||||
public static OutboundShadowObservation unavailable(PublishedShadowSnapshot snapshot,
|
||||
String requestId, String resultCode, String reason) {
|
||||
return new OutboundShadowObservation(true, snapshot.generation(), snapshot.warehouseId(),
|
||||
snapshot.synchronizedAt(), snapshot.sourceSignature(),
|
||||
requestId, resultCode, null, null, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.function.Supplier;
|
||||
@Component
|
||||
public class ShadowPlanningEngine {
|
||||
private static final int INSTANCE_STALE_SECONDS = 120;
|
||||
private static final int DECISION_REASON_MAX_LENGTH = 2000;
|
||||
private static final String PUBLISHED = "PUBLISHED";
|
||||
|
||||
private final SchedulingFacade schedulingFacade;
|
||||
@@ -81,8 +82,8 @@ public class ShadowPlanningEngine {
|
||||
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(
|
||||
warehouseId, snapshot.generation(), stockoutDetailId);
|
||||
if (reference == null || isBlank(reference.getProjectNo())) {
|
||||
return OutboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
||||
stockoutDetailId, "SKIPPED_MISSING_PROJECT",
|
||||
return OutboundShadowObservation.unavailable(snapshot, stockoutDetailId,
|
||||
"SKIPPED_MISSING_PROJECT",
|
||||
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
||||
"No project number was available for shadow planning"));
|
||||
}
|
||||
@@ -100,8 +101,8 @@ public class ShadowPlanningEngine {
|
||||
ShadowStockReference reference = inputMapper.selectStockReference(
|
||||
warehouseId, snapshot.generation(), stockId);
|
||||
if (reference == null || isBlank(reference.getTrayNo())) {
|
||||
return OutboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
||||
stockoutDetailId, "SKIPPED_MISSING_STOCK",
|
||||
return OutboundShadowObservation.unavailable(snapshot, stockoutDetailId,
|
||||
"SKIPPED_MISSING_STOCK",
|
||||
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
||||
"Selected stock was unavailable to shadow planning"));
|
||||
}
|
||||
@@ -116,37 +117,41 @@ public class ShadowPlanningEngine {
|
||||
String legacySpaceId) {
|
||||
Objects.requireNonNull(observation, "observation");
|
||||
transactions.executeWithoutResult(status -> {
|
||||
requirePublishedGeneration(observation.snapshotGeneration(), observation.warehouseId(),
|
||||
"published snapshot changed before comparison");
|
||||
inputMapper.insertComparison(
|
||||
validateComparisonSnapshot(observation.snapshotGeneration(), observation.warehouseId(),
|
||||
observation.snapshotSynchronizedAt(), observation.sourceSignature());
|
||||
int inserted = inputMapper.insertComparison(
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
||||
observation.snapshotGeneration(),
|
||||
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
|
||||
observation.sourceSignature(),
|
||||
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
|
||||
"snapshotGeneration=" + observation.snapshotGeneration()
|
||||
abbreviate("snapshotGeneration=" + observation.snapshotGeneration()
|
||||
+ "; request=" + observation.requestId()
|
||||
+ "; shadow=" + observation.resultCode()
|
||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||
+ "; shadowReason=" + nullSafe(observation.reason())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode)
|
||||
+ "; legacySpace=" + nullSafe(legacySpaceId));
|
||||
+ "; legacySpace=" + nullSafe(legacySpaceId), DECISION_REASON_MAX_LENGTH));
|
||||
requireComparisonInserted(inserted);
|
||||
});
|
||||
}
|
||||
|
||||
public void compareOutbound(OutboundShadowObservation observation, String legacyResultCode) {
|
||||
Objects.requireNonNull(observation, "observation");
|
||||
transactions.executeWithoutResult(status -> {
|
||||
requirePublishedGeneration(observation.snapshotGeneration(), observation.warehouseId(),
|
||||
"published snapshot changed before comparison");
|
||||
inputMapper.insertComparison(
|
||||
validateComparisonSnapshot(observation.snapshotGeneration(), observation.warehouseId(),
|
||||
observation.snapshotSynchronizedAt(), observation.sourceSignature());
|
||||
int inserted = inputMapper.insertComparison(
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
||||
observation.snapshotGeneration(), "LEGACY_" + nullSafe(legacyResultCode),
|
||||
"snapshotGeneration=" + observation.snapshotGeneration()
|
||||
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())
|
||||
+ "; shadowReason=" + nullSafe(observation.reason())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode));
|
||||
+ "; legacy=" + nullSafe(legacyResultCode), DECISION_REASON_MAX_LENGTH));
|
||||
requireComparisonInserted(inserted);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,7 +161,7 @@ public class ShadowPlanningEngine {
|
||||
ShadowInboundReference reference = inputMapper.selectInboundInput(
|
||||
warehouseId, snapshot.generation(), taskNo, trayNo);
|
||||
if (reference == null || isBlank(reference.getRequestId()) || isBlank(reference.getTrayNo())) {
|
||||
return InboundShadowObservation.unavailable(snapshot.generation(), warehouseId, taskNo,
|
||||
return InboundShadowObservation.unavailable(snapshot, taskNo,
|
||||
"SKIPPED_MISSING_INPUT",
|
||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference,
|
||||
"Inbound task details were unavailable to shadow planning"));
|
||||
@@ -169,11 +174,12 @@ public class ShadowPlanningEngine {
|
||||
if (plan.assignment().isPresent()) {
|
||||
var assignment = plan.assignment().orElseThrow();
|
||||
return new InboundShadowObservation(true, snapshot.generation(), warehouseId,
|
||||
snapshot.synchronizedAt(), snapshot.sourceSignature(),
|
||||
reference.getRequestId(), "READY", assignment.spaceId(),
|
||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference, assignment.reason()));
|
||||
}
|
||||
var rejection = plan.rejection().orElseThrow();
|
||||
return InboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
||||
return InboundShadowObservation.unavailable(snapshot,
|
||||
reference.getRequestId(), "REJECTED_" + rejection.code().name(),
|
||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference, rejection.reason()));
|
||||
}
|
||||
@@ -186,17 +192,18 @@ public class ShadowPlanningEngine {
|
||||
if (plan.assignment().isPresent()) {
|
||||
var assignment = plan.assignment().orElseThrow();
|
||||
return new OutboundShadowObservation(true, snapshot.generation(), snapshot.warehouseId(),
|
||||
snapshot.synchronizedAt(), snapshot.sourceSignature(),
|
||||
requestId, "READY", reference.getStockId(), assignment.sourceSpaceId(),
|
||||
outboundDiagnostic(snapshot, requestId, reference, assignment.reason()));
|
||||
}
|
||||
if (plan.waiting().isPresent()) {
|
||||
var waiting = plan.waiting().orElseThrow();
|
||||
return OutboundShadowObservation.unavailable(snapshot.generation(), snapshot.warehouseId(),
|
||||
return OutboundShadowObservation.unavailable(snapshot,
|
||||
requestId, "WAITING_" + waiting.code().name(),
|
||||
outboundDiagnostic(snapshot, requestId, reference, waiting.reason()));
|
||||
}
|
||||
var error = plan.error().orElseThrow();
|
||||
return OutboundShadowObservation.unavailable(snapshot.generation(), snapshot.warehouseId(),
|
||||
return OutboundShadowObservation.unavailable(snapshot,
|
||||
requestId, "ERROR_" + error.code().name(),
|
||||
outboundDiagnostic(snapshot, requestId, reference, error.message()));
|
||||
}
|
||||
@@ -253,16 +260,17 @@ public class ShadowPlanningEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private void requirePublishedGeneration(long generation, String warehouseId, String message) {
|
||||
if (generation <= 0 || isBlank(warehouseId)) {
|
||||
throw new IllegalStateException(message);
|
||||
private void validateComparisonSnapshot(long generation, String warehouseId,
|
||||
Instant synchronizedAt, String sourceSignature) {
|
||||
if (generation <= 0 || isBlank(warehouseId) || synchronizedAt == null) {
|
||||
throw new IllegalStateException("published snapshot changed before comparison");
|
||||
}
|
||||
ShadowSnapshotDescriptor descriptor = inputMapper.selectPublishedSnapshot(warehouseId);
|
||||
if (descriptor == null
|
||||
|| !PUBLISHED.equals(descriptor.getSnapshotStatus())
|
||||
|| descriptor.getSnapshotGeneration() != generation
|
||||
|| !WarehouseIdNormalizer.equivalent(descriptor.getWarehouseId(), warehouseId)) {
|
||||
throw new IllegalStateException(message);
|
||||
requireUppercaseHexSignature(sourceSignature, "comparison source signature");
|
||||
}
|
||||
|
||||
private void requireComparisonInserted(int inserted) {
|
||||
if (inserted != 1) {
|
||||
throw new IllegalStateException("published snapshot changed before comparison");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import com.ruoyi.algorithm.shadow.ShadowStockReference;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Mapper
|
||||
public interface ShadowInputMapper {
|
||||
ShadowInboundReference selectInboundInput(@Param("warehouseId") String warehouseId,
|
||||
@@ -31,6 +33,8 @@ public interface ShadowInputMapper {
|
||||
@Param("warehouseId") String warehouseId,
|
||||
@Param("decisionType") String decisionType,
|
||||
@Param("inputVersion") long inputVersion,
|
||||
@Param("snapshotSynchronizedAt") Instant snapshotSynchronizedAt,
|
||||
@Param("sourceSignature") String sourceSignature,
|
||||
@Param("resultCode") String resultCode,
|
||||
@Param("reason") String reason);
|
||||
}
|
||||
|
||||
@@ -96,15 +96,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
state.snapshot_status as snapshotStatus,
|
||||
state.synchronized_at as synchronizedAt,
|
||||
convert(varchar(64), state.source_signature, 2) as sourceSignature
|
||||
from alg.shadow_snapshot_state state
|
||||
from alg.shadow_snapshot_state state with (readcommittedlock)
|
||||
where state.warehouse_id = #{warehouseId}
|
||||
</select>
|
||||
|
||||
<insert id="insertComparison">
|
||||
insert into alg.decision_log (
|
||||
decision_id, warehouse_id, decision_type, input_version, result_code, reason)
|
||||
values (
|
||||
try_convert(uniqueidentifier, #{decisionId}), #{warehouseId}, #{decisionType},
|
||||
convert(binary(8), #{inputVersion}), #{resultCode}, #{reason})
|
||||
select try_convert(uniqueidentifier, #{decisionId}), state.warehouse_id, #{decisionType},
|
||||
convert(binary(8), #{inputVersion}), #{resultCode}, #{reason}
|
||||
from alg.shadow_snapshot_state state with (updlock, holdlock)
|
||||
where state.warehouse_id = #{warehouseId}
|
||||
and state.snapshot_status = 'PUBLISHED'
|
||||
and state.snapshot_generation = #{inputVersion}
|
||||
and state.synchronized_at = #{snapshotSynchronizedAt}
|
||||
and convert(varchar(64), state.source_signature, 2) = #{sourceSignature}
|
||||
</insert>
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user