feat(wcs): coordinate and persist traffic decisions
This commit is contained in:
@@ -34,5 +34,10 @@
|
||||
<version>1.2.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.ruoyi.wcs.application.dispatch;
|
||||
|
||||
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
|
||||
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class DispatchCommit {
|
||||
private final String warehouseCode;
|
||||
private final long topologyVersion;
|
||||
private final long snapshotVersion;
|
||||
private final List<GrantedDispatch> granted;
|
||||
private final List<HeldDispatch> held;
|
||||
private final List<HeldDispatch> yielded;
|
||||
private final List<DispatchDecisionEvent> decisionEvents;
|
||||
private final ResourceLedger resultingLedger;
|
||||
private final WaitForGraph resultingWaitForGraph;
|
||||
|
||||
public DispatchCommit(String warehouseCode,
|
||||
long topologyVersion, long snapshotVersion,
|
||||
List<GrantedDispatch> granted,
|
||||
List<HeldDispatch> held,
|
||||
List<HeldDispatch> yielded,
|
||||
List<DispatchDecisionEvent> decisionEvents,
|
||||
ResourceLedger resultingLedger,
|
||||
WaitForGraph resultingWaitForGraph) {
|
||||
if (warehouseCode == null || warehouseCode.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("warehouseCode must not be blank");
|
||||
}
|
||||
this.warehouseCode = warehouseCode.trim();
|
||||
this.topologyVersion = topologyVersion;
|
||||
this.snapshotVersion = snapshotVersion;
|
||||
this.granted = immutable(granted);
|
||||
this.held = immutable(held);
|
||||
this.yielded = immutable(yielded);
|
||||
this.decisionEvents = immutable(decisionEvents);
|
||||
this.resultingLedger = resultingLedger.copy();
|
||||
this.resultingWaitForGraph = resultingWaitForGraph.copy();
|
||||
}
|
||||
|
||||
public String getWarehouseCode() {
|
||||
return warehouseCode;
|
||||
}
|
||||
|
||||
public long getTopologyVersion() {
|
||||
return topologyVersion;
|
||||
}
|
||||
|
||||
public long getSnapshotVersion() {
|
||||
return snapshotVersion;
|
||||
}
|
||||
|
||||
public List<GrantedDispatch> getGranted() {
|
||||
return granted;
|
||||
}
|
||||
|
||||
public List<HeldDispatch> getHeld() {
|
||||
return held;
|
||||
}
|
||||
|
||||
public List<HeldDispatch> getYielded() {
|
||||
return yielded;
|
||||
}
|
||||
|
||||
public List<DispatchDecisionEvent> getDecisionEvents() {
|
||||
return decisionEvents;
|
||||
}
|
||||
|
||||
public ResourceLedger getResultingLedger() {
|
||||
return resultingLedger.copy();
|
||||
}
|
||||
|
||||
public WaitForGraph getResultingWaitForGraph() {
|
||||
return resultingWaitForGraph.copy();
|
||||
}
|
||||
|
||||
private static <T> List<T> immutable(List<T> source) {
|
||||
return Collections.unmodifiableList(new ArrayList<T>(source));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.ruoyi.wcs.application.dispatch;
|
||||
|
||||
import com.ruoyi.wcs.domain.dispatch.DispatchTask;
|
||||
import com.ruoyi.wcs.domain.dispatch.VehicleAssignmentDecision;
|
||||
import com.ruoyi.wcs.domain.dispatch.VehicleAssignmentPolicy;
|
||||
import com.ruoyi.wcs.domain.equipment.VehicleSnapshot;
|
||||
import com.ruoyi.wcs.domain.shared.IdGenerator;
|
||||
import com.ruoyi.wcs.domain.shared.WcsClock;
|
||||
import com.ruoyi.wcs.domain.traffic.DeadlockGuard;
|
||||
import com.ruoyi.wcs.domain.traffic.ReservationDecision;
|
||||
import com.ruoyi.wcs.domain.traffic.ReservationRequest;
|
||||
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
|
||||
import com.ruoyi.wcs.domain.traffic.TrafficCandidate;
|
||||
import com.ruoyi.wcs.domain.traffic.TrafficDecision;
|
||||
import com.ruoyi.wcs.domain.traffic.TrafficDecisionType;
|
||||
import com.ruoyi.wcs.port.out.DispatchSnapshotProvider;
|
||||
import com.ruoyi.wcs.port.out.TrafficStateRepository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class DispatchCoordinator {
|
||||
private final DispatchSnapshotProvider snapshotProvider;
|
||||
private final VehicleAssignmentPolicy assignmentPolicy;
|
||||
private final DeadlockGuard deadlockGuard;
|
||||
private final TrafficStateRepository trafficStateRepository;
|
||||
private final IdGenerator idGenerator;
|
||||
private final WcsClock clock;
|
||||
|
||||
public DispatchCoordinator(VehicleAssignmentPolicy assignmentPolicy,
|
||||
DeadlockGuard deadlockGuard,
|
||||
TrafficStateRepository trafficStateRepository,
|
||||
IdGenerator idGenerator) {
|
||||
this(null, assignmentPolicy, deadlockGuard, trafficStateRepository,
|
||||
idGenerator, Instant::now);
|
||||
}
|
||||
|
||||
public DispatchCoordinator(DispatchSnapshotProvider snapshotProvider,
|
||||
VehicleAssignmentPolicy assignmentPolicy,
|
||||
DeadlockGuard deadlockGuard,
|
||||
TrafficStateRepository trafficStateRepository,
|
||||
IdGenerator idGenerator,
|
||||
WcsClock clock) {
|
||||
this.snapshotProvider = snapshotProvider;
|
||||
this.assignmentPolicy = Objects.requireNonNull(assignmentPolicy,
|
||||
"assignmentPolicy");
|
||||
this.deadlockGuard = Objects.requireNonNull(deadlockGuard, "deadlockGuard");
|
||||
this.trafficStateRepository = Objects.requireNonNull(
|
||||
trafficStateRepository, "trafficStateRepository");
|
||||
this.idGenerator = Objects.requireNonNull(idGenerator, "idGenerator");
|
||||
this.clock = Objects.requireNonNull(clock, "clock");
|
||||
}
|
||||
|
||||
public DispatchCycleResult dispatch() {
|
||||
if (snapshotProvider == null) {
|
||||
throw new IllegalStateException("dispatch snapshot provider is not configured");
|
||||
}
|
||||
return dispatch(snapshotProvider.load());
|
||||
}
|
||||
|
||||
public DispatchCycleResult dispatch(DispatchSnapshot snapshot) {
|
||||
Objects.requireNonNull(snapshot, "snapshot");
|
||||
List<DispatchTask> tasks = new ArrayList<DispatchTask>(snapshot.getReadySteps());
|
||||
Collections.sort(tasks, Comparator
|
||||
.comparingInt(DispatchTask::getPriority).reversed()
|
||||
.thenComparing(Comparator
|
||||
.comparingLong(DispatchTask::getWaitingAgeSeconds)
|
||||
.reversed())
|
||||
.thenComparing(DispatchTask::getOrderId)
|
||||
.thenComparing(DispatchTask::getStepId));
|
||||
List<VehicleSnapshot> available = new ArrayList<VehicleSnapshot>(
|
||||
snapshot.getVehicleSnapshots());
|
||||
ResourceLedger ledger = snapshot.workingLedger();
|
||||
DeadlockGuard cycleGuard = deadlockGuard.withGraph(
|
||||
snapshot.workingWaitForGraph());
|
||||
|
||||
List<GrantedDispatch> granted = new ArrayList<GrantedDispatch>();
|
||||
List<HeldDispatch> held = new ArrayList<HeldDispatch>();
|
||||
List<HeldDispatch> yielded = new ArrayList<HeldDispatch>();
|
||||
List<DispatchDecisionEvent> events = new ArrayList<DispatchDecisionEvent>();
|
||||
List<String> violations = detectInputViolations(tasks, available);
|
||||
|
||||
for (DispatchTask task : tasks) {
|
||||
VehicleAssignmentDecision assignment = assignmentPolicy.assign(
|
||||
task, available, snapshot.getTopology());
|
||||
if (!assignment.isAssigned()) {
|
||||
HeldDispatch noVehicle = new HeldDispatch(task.getOrderId(),
|
||||
task.getStepId(), null, TrafficDecisionType.HOLD,
|
||||
"NO_ELIGIBLE_VEHICLE", Collections.<String>emptySet(),
|
||||
null, null);
|
||||
held.add(noVehicle);
|
||||
events.add(event(task, null, TrafficDecisionType.HOLD,
|
||||
noVehicle.getReasonCode(), snapshot));
|
||||
continue;
|
||||
}
|
||||
|
||||
VehicleSnapshot vehicle = removeVehicle(
|
||||
available, assignment.getVehicleId());
|
||||
String reservationId = requireGeneratedId(idGenerator.nextId());
|
||||
ReservationRequest request = new ReservationRequest(reservationId,
|
||||
task.getOrderId(), task.getStepId(), assignment.getVehicleId(),
|
||||
assignment.getRoute().getResourceKeys());
|
||||
TrafficDecision traffic = cycleGuard.decide(new TrafficCandidate(
|
||||
request, vehicle.getLocationNodeId(), assignment.getRoute()),
|
||||
ledger, snapshot.getTopology());
|
||||
if (traffic.getType() == TrafficDecisionType.GRANT) {
|
||||
ReservationDecision reservation = traffic.getReservationDecision();
|
||||
granted.add(new GrantedDispatch(task.getOrderId(), task.getStepId(),
|
||||
assignment.getVehicleId(), assignment.getRoute(),
|
||||
Collections.singletonList(reservation.getReservationId()),
|
||||
snapshot.getTopology().version(), reservation.getFencingToken()));
|
||||
} else {
|
||||
HeldDispatch blocked = new HeldDispatch(task.getOrderId(),
|
||||
task.getStepId(), assignment.getVehicleId(), traffic.getType(),
|
||||
traffic.getReasonCode(), traffic.getBlockingVehicleIds(),
|
||||
traffic.getYieldNodeId(), traffic.getYieldRoute());
|
||||
if (traffic.getType() == TrafficDecisionType.YIELD) {
|
||||
yielded.add(blocked);
|
||||
} else {
|
||||
held.add(blocked);
|
||||
}
|
||||
}
|
||||
events.add(event(task, assignment.getVehicleId(), traffic.getType(),
|
||||
traffic.getReasonCode(), snapshot));
|
||||
}
|
||||
|
||||
DispatchCommit commit = new DispatchCommit(
|
||||
snapshot.getTopology().getWarehouseCode(),
|
||||
snapshot.getTopology().version(),
|
||||
snapshot.getSnapshotVersion(), granted, held, yielded, events,
|
||||
ledger, cycleGuard.snapshotWaitForGraph());
|
||||
if (!trafficStateRepository.commit(commit)) {
|
||||
return DispatchCycleResult.retrySnapshotChanged();
|
||||
}
|
||||
return DispatchCycleResult.committed(granted, held, yielded, violations);
|
||||
}
|
||||
|
||||
private DispatchDecisionEvent event(DispatchTask task, String vehicleId,
|
||||
TrafficDecisionType type, String reason,
|
||||
DispatchSnapshot snapshot) {
|
||||
return new DispatchDecisionEvent(requireGeneratedId(idGenerator.nextId()),
|
||||
task.getOrderId(), task.getStepId(), vehicleId, type, reason,
|
||||
snapshot.getTopology().version(), snapshot.getSnapshotVersion(),
|
||||
clock.now());
|
||||
}
|
||||
|
||||
private static VehicleSnapshot removeVehicle(List<VehicleSnapshot> vehicles,
|
||||
String vehicleId) {
|
||||
for (int i = 0; i < vehicles.size(); i++) {
|
||||
if (vehicleId.equals(vehicles.get(i).getVehicleId())) {
|
||||
return vehicles.remove(i);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("assigned vehicle missing from snapshot: " + vehicleId);
|
||||
}
|
||||
|
||||
private static List<String> detectInputViolations(
|
||||
List<DispatchTask> tasks, List<VehicleSnapshot> vehicles) {
|
||||
List<String> violations = new ArrayList<String>();
|
||||
Set<String> stepIds = new HashSet<String>();
|
||||
for (DispatchTask task : tasks) {
|
||||
if (!stepIds.add(task.getStepId())) {
|
||||
violations.add("DUPLICATE_READY_STEP:" + task.getStepId());
|
||||
}
|
||||
}
|
||||
Set<String> vehicleIds = new HashSet<String>();
|
||||
for (VehicleSnapshot vehicle : vehicles) {
|
||||
if (!vehicleIds.add(vehicle.getVehicleId())) {
|
||||
violations.add("DUPLICATE_VEHICLE_SNAPSHOT:" + vehicle.getVehicleId());
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
private static String requireGeneratedId(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalStateException("id generator returned a blank id");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.ruoyi.wcs.application.dispatch;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class DispatchCycleResult {
|
||||
private final String statusCode;
|
||||
private final List<GrantedDispatch> granted;
|
||||
private final List<HeldDispatch> held;
|
||||
private final List<HeldDispatch> yielded;
|
||||
private final List<String> invariantViolations;
|
||||
|
||||
private DispatchCycleResult(String statusCode,
|
||||
List<GrantedDispatch> granted,
|
||||
List<HeldDispatch> held,
|
||||
List<HeldDispatch> yielded,
|
||||
List<String> invariantViolations) {
|
||||
this.statusCode = statusCode;
|
||||
this.granted = immutable(granted);
|
||||
this.held = immutable(held);
|
||||
this.yielded = immutable(yielded);
|
||||
this.invariantViolations = immutable(invariantViolations);
|
||||
}
|
||||
|
||||
public static DispatchCycleResult committed(List<GrantedDispatch> granted,
|
||||
List<HeldDispatch> held,
|
||||
List<HeldDispatch> yielded,
|
||||
List<String> violations) {
|
||||
return new DispatchCycleResult("COMMITTED", granted, held, yielded, violations);
|
||||
}
|
||||
|
||||
public static DispatchCycleResult retrySnapshotChanged() {
|
||||
return new DispatchCycleResult("RETRY_SNAPSHOT_CHANGED",
|
||||
Collections.<GrantedDispatch>emptyList(),
|
||||
Collections.<HeldDispatch>emptyList(),
|
||||
Collections.<HeldDispatch>emptyList(),
|
||||
Collections.<String>emptyList());
|
||||
}
|
||||
|
||||
public String getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
public boolean isRetrySnapshotChanged() {
|
||||
return "RETRY_SNAPSHOT_CHANGED".equals(statusCode);
|
||||
}
|
||||
|
||||
public List<GrantedDispatch> getGranted() {
|
||||
return granted;
|
||||
}
|
||||
|
||||
public List<HeldDispatch> getHeld() {
|
||||
return held;
|
||||
}
|
||||
|
||||
public List<HeldDispatch> getYielded() {
|
||||
return yielded;
|
||||
}
|
||||
|
||||
public List<String> getInvariantViolations() {
|
||||
return invariantViolations;
|
||||
}
|
||||
|
||||
private static <T> List<T> immutable(List<T> source) {
|
||||
return Collections.unmodifiableList(new ArrayList<T>(source));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.ruoyi.wcs.application.dispatch;
|
||||
|
||||
import com.ruoyi.wcs.domain.traffic.TrafficDecisionType;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public final class DispatchDecisionEvent {
|
||||
private final String eventId;
|
||||
private final String orderId;
|
||||
private final String stepId;
|
||||
private final String vehicleId;
|
||||
private final TrafficDecisionType decisionType;
|
||||
private final String reasonCode;
|
||||
private final long topologyVersion;
|
||||
private final long snapshotVersion;
|
||||
private final Instant occurredAt;
|
||||
|
||||
public DispatchDecisionEvent(String eventId, String orderId, String stepId,
|
||||
String vehicleId, TrafficDecisionType decisionType,
|
||||
String reasonCode, long topologyVersion,
|
||||
long snapshotVersion, Instant occurredAt) {
|
||||
this.eventId = eventId;
|
||||
this.orderId = orderId;
|
||||
this.stepId = stepId;
|
||||
this.vehicleId = vehicleId;
|
||||
this.decisionType = decisionType;
|
||||
this.reasonCode = reasonCode;
|
||||
this.topologyVersion = topologyVersion;
|
||||
this.snapshotVersion = snapshotVersion;
|
||||
this.occurredAt = occurredAt;
|
||||
}
|
||||
|
||||
public String getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
public String getStepId() {
|
||||
return stepId;
|
||||
}
|
||||
|
||||
public String getVehicleId() {
|
||||
return vehicleId;
|
||||
}
|
||||
|
||||
public TrafficDecisionType getDecisionType() {
|
||||
return decisionType;
|
||||
}
|
||||
|
||||
public String getReasonCode() {
|
||||
return reasonCode;
|
||||
}
|
||||
|
||||
public long getTopologyVersion() {
|
||||
return topologyVersion;
|
||||
}
|
||||
|
||||
public long getSnapshotVersion() {
|
||||
return snapshotVersion;
|
||||
}
|
||||
|
||||
public Instant getOccurredAt() {
|
||||
return occurredAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.ruoyi.wcs.application.dispatch;
|
||||
|
||||
import com.ruoyi.wcs.domain.dispatch.DispatchTask;
|
||||
import com.ruoyi.wcs.domain.equipment.VehicleSnapshot;
|
||||
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
|
||||
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
|
||||
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class DispatchSnapshot {
|
||||
private final List<DispatchTask> readySteps;
|
||||
private final List<VehicleSnapshot> vehicleSnapshots;
|
||||
private final EffectiveTopology topology;
|
||||
private final ResourceLedger resourceLedger;
|
||||
private final WaitForGraph waitForGraph;
|
||||
private final long snapshotVersion;
|
||||
|
||||
public DispatchSnapshot(List<DispatchTask> readySteps,
|
||||
List<VehicleSnapshot> vehicleSnapshots,
|
||||
EffectiveTopology topology,
|
||||
ResourceLedger resourceLedger,
|
||||
long snapshotVersion) {
|
||||
this(readySteps, vehicleSnapshots, topology, resourceLedger,
|
||||
new WaitForGraph(), snapshotVersion);
|
||||
}
|
||||
|
||||
public DispatchSnapshot(List<DispatchTask> readySteps,
|
||||
List<VehicleSnapshot> vehicleSnapshots,
|
||||
EffectiveTopology topology,
|
||||
ResourceLedger resourceLedger,
|
||||
WaitForGraph waitForGraph,
|
||||
long snapshotVersion) {
|
||||
this.readySteps = immutable(readySteps, "readySteps");
|
||||
this.vehicleSnapshots = immutable(vehicleSnapshots, "vehicleSnapshots");
|
||||
this.topology = Objects.requireNonNull(topology, "topology");
|
||||
this.resourceLedger = Objects.requireNonNull(resourceLedger,
|
||||
"resourceLedger").copy();
|
||||
this.waitForGraph = Objects.requireNonNull(waitForGraph, "waitForGraph").copy();
|
||||
if (snapshotVersion < 0L) {
|
||||
throw new IllegalArgumentException("snapshotVersion must not be negative");
|
||||
}
|
||||
this.snapshotVersion = snapshotVersion;
|
||||
}
|
||||
|
||||
public List<DispatchTask> getReadySteps() {
|
||||
return readySteps;
|
||||
}
|
||||
|
||||
public List<VehicleSnapshot> getVehicleSnapshots() {
|
||||
return vehicleSnapshots;
|
||||
}
|
||||
|
||||
public EffectiveTopology getTopology() {
|
||||
return topology;
|
||||
}
|
||||
|
||||
public ResourceLedger workingLedger() {
|
||||
return resourceLedger.copy();
|
||||
}
|
||||
|
||||
public WaitForGraph workingWaitForGraph() {
|
||||
return waitForGraph.copy();
|
||||
}
|
||||
|
||||
public long getSnapshotVersion() {
|
||||
return snapshotVersion;
|
||||
}
|
||||
|
||||
private static <T> List<T> immutable(List<T> source, String field) {
|
||||
if (source == null) {
|
||||
throw new IllegalArgumentException(field + " must not be null");
|
||||
}
|
||||
return Collections.unmodifiableList(new ArrayList<T>(source));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.ruoyi.wcs.application.dispatch;
|
||||
|
||||
import com.ruoyi.wcs.domain.traffic.RoutePlan;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class GrantedDispatch {
|
||||
private final String orderId;
|
||||
private final String stepId;
|
||||
private final String vehicleId;
|
||||
private final RoutePlan protectedRoute;
|
||||
private final List<String> reservationIds;
|
||||
private final long topologyVersion;
|
||||
private final long fencingToken;
|
||||
|
||||
public GrantedDispatch(String orderId, String stepId, String vehicleId,
|
||||
RoutePlan protectedRoute, List<String> reservationIds,
|
||||
long topologyVersion, long fencingToken) {
|
||||
this.orderId = orderId;
|
||||
this.stepId = stepId;
|
||||
this.vehicleId = vehicleId;
|
||||
this.protectedRoute = protectedRoute;
|
||||
this.reservationIds = Collections.unmodifiableList(
|
||||
new ArrayList<String>(reservationIds));
|
||||
this.topologyVersion = topologyVersion;
|
||||
this.fencingToken = fencingToken;
|
||||
}
|
||||
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
public String getStepId() {
|
||||
return stepId;
|
||||
}
|
||||
|
||||
public String getVehicleId() {
|
||||
return vehicleId;
|
||||
}
|
||||
|
||||
public RoutePlan getProtectedRoute() {
|
||||
return protectedRoute;
|
||||
}
|
||||
|
||||
public List<String> getReservationIds() {
|
||||
return reservationIds;
|
||||
}
|
||||
|
||||
public long getTopologyVersion() {
|
||||
return topologyVersion;
|
||||
}
|
||||
|
||||
public long getFencingToken() {
|
||||
return fencingToken;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ruoyi.wcs.application.dispatch;
|
||||
|
||||
import com.ruoyi.wcs.domain.traffic.RoutePlan;
|
||||
import com.ruoyi.wcs.domain.traffic.TrafficDecisionType;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class HeldDispatch {
|
||||
private final String orderId;
|
||||
private final String stepId;
|
||||
private final String vehicleId;
|
||||
private final TrafficDecisionType decisionType;
|
||||
private final String reasonCode;
|
||||
private final Set<String> blockingVehicleIds;
|
||||
private final String yieldNodeId;
|
||||
private final RoutePlan yieldRoute;
|
||||
|
||||
public HeldDispatch(String orderId, String stepId, String vehicleId,
|
||||
TrafficDecisionType decisionType, String reasonCode,
|
||||
Set<String> blockingVehicleIds,
|
||||
String yieldNodeId, RoutePlan yieldRoute) {
|
||||
this.orderId = orderId;
|
||||
this.stepId = stepId;
|
||||
this.vehicleId = vehicleId;
|
||||
this.decisionType = decisionType;
|
||||
this.reasonCode = reasonCode;
|
||||
this.blockingVehicleIds = Collections.unmodifiableSet(
|
||||
new LinkedHashSet<String>(blockingVehicleIds));
|
||||
this.yieldNodeId = yieldNodeId;
|
||||
this.yieldRoute = yieldRoute;
|
||||
}
|
||||
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
public String getStepId() {
|
||||
return stepId;
|
||||
}
|
||||
|
||||
public String getVehicleId() {
|
||||
return vehicleId;
|
||||
}
|
||||
|
||||
public TrafficDecisionType getDecisionType() {
|
||||
return decisionType;
|
||||
}
|
||||
|
||||
public String getReasonCode() {
|
||||
return reasonCode;
|
||||
}
|
||||
|
||||
public Set<String> getBlockingVehicleIds() {
|
||||
return blockingVehicleIds;
|
||||
}
|
||||
|
||||
public String getYieldNodeId() {
|
||||
return yieldNodeId;
|
||||
}
|
||||
|
||||
public RoutePlan getYieldRoute() {
|
||||
return yieldRoute;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,14 @@ public final class DeadlockGuard {
|
||||
this.routePlanner = Objects.requireNonNull(routePlanner, "routePlanner");
|
||||
}
|
||||
|
||||
public DeadlockGuard withGraph(WaitForGraph graph) {
|
||||
return new DeadlockGuard(graph, routePlanner);
|
||||
}
|
||||
|
||||
public WaitForGraph snapshotWaitForGraph() {
|
||||
return waitForGraph.copy();
|
||||
}
|
||||
|
||||
public TrafficDecision decide(TrafficCandidate candidate,
|
||||
ResourceLedger ledger,
|
||||
EffectiveTopology topology) {
|
||||
|
||||
@@ -57,6 +57,14 @@ public final class WaitForGraph {
|
||||
return Collections.unmodifiableSet(new LinkedHashSet<String>(holders));
|
||||
}
|
||||
|
||||
public synchronized WaitForGraph copy() {
|
||||
WaitForGraph copy = new WaitForGraph();
|
||||
for (Map.Entry<String, Set<String>> entry : edges.entrySet()) {
|
||||
copy.edges.put(entry.getKey(), new TreeSet<String>(entry.getValue()));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private Map<String, Set<String>> copyEdges() {
|
||||
Map<String, Set<String>> copy = new LinkedHashMap<String, Set<String>>();
|
||||
for (Map.Entry<String, Set<String>> entry : edges.entrySet()) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.wcs.port.out;
|
||||
|
||||
import com.ruoyi.wcs.application.dispatch.DispatchDecisionEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface DecisionEventRepository {
|
||||
void saveAllWithinTrafficCommit(List<DispatchDecisionEvent> events);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.ruoyi.wcs.port.out;
|
||||
|
||||
import com.ruoyi.wcs.application.dispatch.DispatchSnapshot;
|
||||
|
||||
public interface DispatchSnapshotProvider {
|
||||
DispatchSnapshot load();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.ruoyi.wcs.port.out;
|
||||
|
||||
import com.ruoyi.wcs.application.dispatch.DispatchCommit;
|
||||
|
||||
public interface TrafficStateRepository {
|
||||
boolean commit(DispatchCommit commit);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.ruoyi.wcs.application.dispatch;
|
||||
|
||||
import com.ruoyi.wcs.domain.dispatch.DefaultVehicleAssignmentPolicy;
|
||||
import com.ruoyi.wcs.domain.dispatch.DispatchTask;
|
||||
import com.ruoyi.wcs.domain.equipment.DeviceCapability;
|
||||
import com.ruoyi.wcs.domain.equipment.DeviceDescriptor;
|
||||
import com.ruoyi.wcs.domain.equipment.DeviceType;
|
||||
import com.ruoyi.wcs.domain.equipment.VehicleSnapshot;
|
||||
import com.ruoyi.wcs.domain.shared.IdGenerator;
|
||||
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
|
||||
import com.ruoyi.wcs.domain.topology.ResourceDefinition;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyCompiler;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyDefinition;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyEdge;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyNode;
|
||||
import com.ruoyi.wcs.domain.traffic.AStarRoutePlanner;
|
||||
import com.ruoyi.wcs.domain.traffic.DeadlockGuard;
|
||||
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
|
||||
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
|
||||
import com.ruoyi.wcs.port.out.DispatchPolicy;
|
||||
import com.ruoyi.wcs.port.out.TrafficStateRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DispatchCoordinatorTest {
|
||||
|
||||
@Test
|
||||
void grantsOneVehicleAndHoldsTheOtherForTheSameIntersection() {
|
||||
EffectiveTopology topology = topology();
|
||||
RecordingTrafficRepository repository = new RecordingTrafficRepository(true);
|
||||
DispatchCoordinator coordinator = coordinator(repository);
|
||||
|
||||
DispatchCycleResult result = coordinator.dispatch(new DispatchSnapshot(
|
||||
Arrays.asList(task("O1", "S1", "A", 2),
|
||||
task("O2", "S2", "B", 1)),
|
||||
Arrays.asList(vehicle("V1", "A"), vehicle("V2", "B")),
|
||||
topology, new ResourceLedger(topology), 11L));
|
||||
|
||||
assertEquals(1, result.getGranted().size());
|
||||
assertEquals("S1", result.getGranted().get(0).getStepId());
|
||||
assertEquals("V1", result.getGranted().get(0).getVehicleId());
|
||||
assertEquals(Collections.singletonList("R-1"),
|
||||
result.getGranted().get(0).getReservationIds());
|
||||
assertEquals(1, result.getHeld().size());
|
||||
assertEquals("NO_SAFE_YIELD_POINT", result.getHeld().get(0).getReasonCode());
|
||||
assertEquals(0, result.getInvariantViolations().size());
|
||||
assertEquals(1, repository.commitCount);
|
||||
assertEquals("FT546", repository.lastCommit.getWarehouseCode());
|
||||
assertEquals(2, repository.lastCommit.getDecisionEvents().size());
|
||||
assertFalse(result.isRetrySnapshotChanged());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsRetryWithoutExposingPartialGrantWhenSnapshotChanged() {
|
||||
EffectiveTopology topology = topology();
|
||||
RecordingTrafficRepository repository = new RecordingTrafficRepository(false);
|
||||
DispatchCoordinator coordinator = coordinator(repository);
|
||||
|
||||
DispatchCycleResult result = coordinator.dispatch(new DispatchSnapshot(
|
||||
Collections.singletonList(task("O1", "S1", "A", 1)),
|
||||
Collections.singletonList(vehicle("V1", "A")),
|
||||
topology, new ResourceLedger(topology), 11L));
|
||||
|
||||
assertTrue(result.isRetrySnapshotChanged());
|
||||
assertEquals("RETRY_SNAPSHOT_CHANGED", result.getStatusCode());
|
||||
assertEquals(0, result.getGranted().size());
|
||||
assertEquals(0, result.getHeld().size());
|
||||
assertEquals(1, repository.commitCount);
|
||||
}
|
||||
|
||||
private static DispatchCoordinator coordinator(TrafficStateRepository repository) {
|
||||
AStarRoutePlanner routePlanner = new AStarRoutePlanner();
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
IdGenerator ids = () -> "R-" + sequence.incrementAndGet();
|
||||
return new DispatchCoordinator(
|
||||
new DefaultVehicleAssignmentPolicy(routePlanner, DispatchPolicy.NONE),
|
||||
new DeadlockGuard(new WaitForGraph(), routePlanner),
|
||||
repository, ids);
|
||||
}
|
||||
|
||||
private static DispatchTask task(String orderId, String stepId,
|
||||
String source, int priority) {
|
||||
return new DispatchTask(orderId, stepId, source, "X", priority,
|
||||
60L, 30, Collections.<DeviceCapability>emptySet());
|
||||
}
|
||||
|
||||
private static VehicleSnapshot vehicle(String id, String location) {
|
||||
DeviceDescriptor descriptor = new DeviceDescriptor(id,
|
||||
DeviceType.FOUR_WAY_SHUTTLE, "F1",
|
||||
EnumSet.of(DeviceCapability.FOUR_WAY_MOVE));
|
||||
return new VehicleSnapshot(descriptor,
|
||||
VehicleSnapshot.ConnectionState.ONLINE,
|
||||
VehicleSnapshot.OperatingMode.AUTO,
|
||||
VehicleSnapshot.HealthState.NORMAL,
|
||||
VehicleSnapshot.OccupationState.IDLE,
|
||||
location, null, 80, null, "F1",
|
||||
Instant.parse("2026-08-04T00:00:00Z"));
|
||||
}
|
||||
|
||||
private static EffectiveTopology topology() {
|
||||
TopologyDefinition definition = new TopologyDefinition("FT546", 7L, true,
|
||||
Arrays.asList(
|
||||
new TopologyNode("A", "F1", 0, 0, 1, false, false),
|
||||
new TopologyNode("X", "F1", 1, 0, 1, false, false),
|
||||
new TopologyNode("B", "F1", 2, 0, 1, false, false)),
|
||||
Arrays.asList(
|
||||
new TopologyEdge("A", "X", 1, 1),
|
||||
new TopologyEdge("B", "X", 1, 1)),
|
||||
Collections.<ResourceDefinition>emptyList(),
|
||||
Collections.<String>emptyList(), Collections.<String>emptyList());
|
||||
return new TopologyCompiler().compile(definition);
|
||||
}
|
||||
|
||||
private static final class RecordingTrafficRepository
|
||||
implements TrafficStateRepository {
|
||||
private final boolean commitResult;
|
||||
private int commitCount;
|
||||
private DispatchCommit lastCommit;
|
||||
|
||||
private RecordingTrafficRepository(boolean commitResult) {
|
||||
this.commitResult = commitResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commit(DispatchCommit commit) {
|
||||
commitCount++;
|
||||
lastCommit = commit;
|
||||
return commitResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package com.ruoyi.wcs.infrastructure.persistence.traffic;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
@Tag("sqlserver")
|
||||
class TrafficConcurrencyIT {
|
||||
private String jdbcUrl;
|
||||
private String username;
|
||||
private String password;
|
||||
private String warehouseCode;
|
||||
private String floorId;
|
||||
private String resourceKey;
|
||||
|
||||
@Test
|
||||
void packagesTheTrafficMigrationUsedByTheSqlServerVerification() throws Exception {
|
||||
String root = System.getProperty("maven.multiModuleProjectDirectory", ".");
|
||||
String migration = readMigration(root, "V002__wcs_topology_and_traffic.sql");
|
||||
|
||||
assertFalse(migration.trim().isEmpty());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanFixture() throws Exception {
|
||||
if (jdbcUrl == null || resourceKey == null) {
|
||||
return;
|
||||
}
|
||||
try (Connection connection = connect()) {
|
||||
delete(connection, "DELETE FROM dbo.wcs_resource_reservation WHERE resource_key = ?", resourceKey);
|
||||
delete(connection, "DELETE FROM dbo.wcs_resource_state WHERE resource_key = ?", resourceKey);
|
||||
delete(connection, "DELETE FROM dbo.wcs_resource_definition WHERE resource_key = ?", resourceKey);
|
||||
delete(connection, "DELETE FROM dbo.wcs_topology_version WHERE warehouse_code = ?", warehouseCode);
|
||||
delete(connection, "DELETE FROM dbo.wcs_floor WHERE floor_id = ?", floorId);
|
||||
delete(connection, "DELETE FROM dbo.wcs_warehouse WHERE warehouse_code = ?", warehouseCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyOneConcurrentReservationCommitsForCapacityOneResource() throws Exception {
|
||||
configureIsolatedDatabase();
|
||||
applyMigration();
|
||||
createCapacityOneFixture();
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
CountDownLatch ready = new CountDownLatch(2);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
try {
|
||||
Future<Boolean> first = executor.submit(() -> reserve("IT-R1", "IT-V1", ready, start));
|
||||
Future<Boolean> second = executor.submit(() -> reserve("IT-R2", "IT-V2", ready, start));
|
||||
ready.await();
|
||||
start.countDown();
|
||||
|
||||
int committed = (first.get() ? 1 : 0) + (second.get() ? 1 : 0);
|
||||
assertEquals(1, committed);
|
||||
assertEquals(1, scalarInt(
|
||||
"SELECT reserved_count FROM dbo.wcs_resource_state WHERE resource_key = ?",
|
||||
resourceKey));
|
||||
assertEquals(1, scalarInt(
|
||||
"SELECT COUNT(*) FROM dbo.wcs_resource_reservation "
|
||||
+ "WHERE resource_key = ? AND reservation_state = 'GRANTED'",
|
||||
resourceKey));
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private void configureIsolatedDatabase() {
|
||||
Assumptions.assumeTrue("true".equalsIgnoreCase(
|
||||
System.getenv("WCS_SQLSERVER_TEST_ENABLED")),
|
||||
"set WCS_SQLSERVER_TEST_ENABLED=true only for an isolated SQL Server database");
|
||||
jdbcUrl = requireEnvironment("WCS_SQLSERVER_TEST_URL");
|
||||
username = requireEnvironment("WCS_SQLSERVER_TEST_USER");
|
||||
password = requireEnvironment("WCS_SQLSERVER_TEST_PASSWORD");
|
||||
}
|
||||
|
||||
private void applyMigration() throws Exception {
|
||||
String root = System.getProperty("maven.multiModuleProjectDirectory", ".");
|
||||
try (Connection connection = connect(); Statement statement = connection.createStatement()) {
|
||||
statement.execute(readMigration(root, "V001__wcs_task_foundation.sql"));
|
||||
statement.execute(readMigration(root, "V002__wcs_topology_and_traffic.sql"));
|
||||
}
|
||||
}
|
||||
|
||||
private static String readMigration(String root, String filename) throws Exception {
|
||||
Path current = Paths.get(root).toAbsolutePath().normalize();
|
||||
while (current != null) {
|
||||
Path migration = current.resolve(Paths.get("sql", "wcs", filename));
|
||||
if (Files.isRegularFile(migration)) {
|
||||
return new String(Files.readAllBytes(migration), StandardCharsets.UTF_8);
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
throw new java.nio.file.NoSuchFileException(filename);
|
||||
}
|
||||
|
||||
private void createCapacityOneFixture() throws Exception {
|
||||
String suffix = UUID.randomUUID().toString().replace("-", "");
|
||||
warehouseCode = "IT" + suffix;
|
||||
floorId = "IT-F-" + suffix;
|
||||
resourceKey = "IT:RESOURCE:" + suffix;
|
||||
try (Connection connection = connect()) {
|
||||
insert(connection,
|
||||
"INSERT INTO dbo.wcs_warehouse "
|
||||
+ "(warehouse_code, warehouse_name, active_configuration_version, state_version) "
|
||||
+ "VALUES (?, ?, NULL, 0)",
|
||||
warehouseCode, "traffic concurrency test");
|
||||
insert(connection,
|
||||
"INSERT INTO dbo.wcs_floor "
|
||||
+ "(floor_id, warehouse_code, floor_code, floor_level) VALUES (?, ?, ?, 1)",
|
||||
floorId, warehouseCode, "IT-F1");
|
||||
insert(connection,
|
||||
"INSERT INTO dbo.wcs_topology_version "
|
||||
+ "(warehouse_code, topology_version, publication_state, published_time) "
|
||||
+ "VALUES (?, 1, 'PUBLISHED', ?)",
|
||||
warehouseCode, Instant.now());
|
||||
insert(connection,
|
||||
"INSERT INTO dbo.wcs_resource_definition "
|
||||
+ "(resource_key, warehouse_code, topology_version, resource_type, capacity) "
|
||||
+ "VALUES (?, ?, 1, 'NODE', 1)",
|
||||
resourceKey, warehouseCode);
|
||||
insert(connection,
|
||||
"INSERT INTO dbo.wcs_resource_state "
|
||||
+ "(warehouse_code, topology_version, resource_key, capacity, "
|
||||
+ "reserved_count, occupied_count, fencing_token, state_version) "
|
||||
+ "VALUES (?, 1, ?, 1, 0, 0, 0, 0)",
|
||||
warehouseCode, resourceKey);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean reserve(String reservationId, String vehicleId,
|
||||
CountDownLatch ready, CountDownLatch start) throws Exception {
|
||||
try (Connection connection = connect()) {
|
||||
connection.setAutoCommit(false);
|
||||
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
|
||||
ready.countDown();
|
||||
start.await();
|
||||
try {
|
||||
int claimed;
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE dbo.wcs_resource_state WITH (UPDLOCK, ROWLOCK) "
|
||||
+ "SET reserved_count = reserved_count + 1, "
|
||||
+ "fencing_token = fencing_token + 1, state_version = state_version + 1 "
|
||||
+ "WHERE warehouse_code = ? AND topology_version = 1 "
|
||||
+ "AND resource_key = ? "
|
||||
+ "AND reserved_count + occupied_count < capacity")) {
|
||||
statement.setString(1, warehouseCode);
|
||||
statement.setString(2, resourceKey);
|
||||
claimed = statement.executeUpdate();
|
||||
}
|
||||
if (claimed == 0) {
|
||||
connection.rollback();
|
||||
return false;
|
||||
}
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO dbo.wcs_resource_reservation "
|
||||
+ "(reservation_id, warehouse_code, topology_version, resource_key, "
|
||||
+ "order_id, step_id, vehicle_id, reservation_state, fencing_token, "
|
||||
+ "created_time, updated_time) "
|
||||
+ "SELECT ?, ?, 1, ?, 'IT-O', 'IT-S', ?, 'GRANTED', fencing_token, ?, ? "
|
||||
+ "FROM dbo.wcs_resource_state WHERE warehouse_code = ? "
|
||||
+ "AND topology_version = 1 AND resource_key = ?")) {
|
||||
Instant now = Instant.now();
|
||||
statement.setString(1, reservationId + '-' + warehouseCode);
|
||||
statement.setString(2, warehouseCode);
|
||||
statement.setString(3, resourceKey);
|
||||
statement.setString(4, vehicleId);
|
||||
statement.setTimestamp(5, Timestamp.from(now));
|
||||
statement.setTimestamp(6, Timestamp.from(now));
|
||||
statement.setString(7, warehouseCode);
|
||||
statement.setString(8, resourceKey);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
connection.commit();
|
||||
return true;
|
||||
} catch (Exception error) {
|
||||
connection.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int scalarInt(String sql, String value) throws Exception {
|
||||
try (Connection connection = connect();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, value);
|
||||
try (ResultSet result = statement.executeQuery()) {
|
||||
if (!result.next()) {
|
||||
throw new AssertionError("query returned no rows: " + sql);
|
||||
}
|
||||
return result.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Connection connect() throws SQLException {
|
||||
return DriverManager.getConnection(jdbcUrl, username, password);
|
||||
}
|
||||
|
||||
private static void insert(Connection connection, String sql,
|
||||
Object... values) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
Object value = values[index];
|
||||
statement.setObject(index + 1,
|
||||
value instanceof Instant ? Timestamp.from((Instant) value) : value);
|
||||
}
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void delete(Connection connection, String sql,
|
||||
String value) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, value);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static String requireEnvironment(String name) {
|
||||
String value = System.getenv(name);
|
||||
Assumptions.assumeTrue(value != null && !value.trim().isEmpty(),
|
||||
name + " must point to the isolated SQL Server test database");
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
297
sql/wcs/V002__wcs_topology_and_traffic.sql
Normal file
297
sql/wcs/V002__wcs_topology_and_traffic.sql
Normal file
@@ -0,0 +1,297 @@
|
||||
SET XACT_ABORT ON;
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_warehouse', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_warehouse (
|
||||
warehouse_code varchar(64) NOT NULL PRIMARY KEY,
|
||||
warehouse_name nvarchar(128) NOT NULL,
|
||||
active_configuration_version bigint NULL,
|
||||
state_version bigint NOT NULL CONSTRAINT df_wcs_warehouse_state_version DEFAULT (0)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_floor', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_floor (
|
||||
floor_id varchar(64) NOT NULL PRIMARY KEY,
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
floor_code varchar(64) NOT NULL,
|
||||
floor_level int NOT NULL,
|
||||
CONSTRAINT fk_wcs_floor_warehouse FOREIGN KEY (warehouse_code)
|
||||
REFERENCES dbo.wcs_warehouse(warehouse_code),
|
||||
CONSTRAINT uk_wcs_floor_code UNIQUE (warehouse_code, floor_code),
|
||||
CONSTRAINT uk_wcs_floor_level UNIQUE (warehouse_code, floor_level)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_topology_version', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_topology_version (
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
publication_state varchar(32) NOT NULL,
|
||||
published_time datetime2(3) NULL,
|
||||
snapshot_version bigint NOT NULL
|
||||
CONSTRAINT df_wcs_topology_snapshot_version DEFAULT (0),
|
||||
CONSTRAINT pk_wcs_topology_version PRIMARY KEY (
|
||||
warehouse_code, topology_version),
|
||||
CONSTRAINT fk_wcs_topology_version_warehouse FOREIGN KEY (warehouse_code)
|
||||
REFERENCES dbo.wcs_warehouse(warehouse_code)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_topology_node', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_topology_node (
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
node_id varchar(128) NOT NULL,
|
||||
floor_id varchar(64) NOT NULL,
|
||||
coordinate_x int NOT NULL,
|
||||
coordinate_y int NOT NULL,
|
||||
capacity int NOT NULL,
|
||||
safe_wait_flag bit NOT NULL CONSTRAINT df_wcs_node_safe_wait DEFAULT (0),
|
||||
safe_yield_flag bit NOT NULL CONSTRAINT df_wcs_node_safe_yield DEFAULT (0),
|
||||
CONSTRAINT pk_wcs_topology_node PRIMARY KEY (
|
||||
warehouse_code, topology_version, node_id),
|
||||
CONSTRAINT fk_wcs_topology_node_version FOREIGN KEY (
|
||||
warehouse_code, topology_version)
|
||||
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version),
|
||||
CONSTRAINT fk_wcs_topology_node_floor FOREIGN KEY (floor_id)
|
||||
REFERENCES dbo.wcs_floor(floor_id),
|
||||
CONSTRAINT ck_wcs_topology_node_capacity CHECK (capacity > 0),
|
||||
CONSTRAINT uk_wcs_topology_node_coordinate UNIQUE (
|
||||
warehouse_code, topology_version, floor_id, coordinate_x, coordinate_y)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_topology_edge', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_topology_edge (
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
resource_key varchar(320) NOT NULL,
|
||||
source_node_id varchar(128) NOT NULL,
|
||||
target_node_id varchar(128) NOT NULL,
|
||||
distance_cost int NOT NULL,
|
||||
capacity int NOT NULL,
|
||||
CONSTRAINT pk_wcs_topology_edge PRIMARY KEY (
|
||||
warehouse_code, topology_version, resource_key),
|
||||
CONSTRAINT fk_wcs_topology_edge_version FOREIGN KEY (
|
||||
warehouse_code, topology_version)
|
||||
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version),
|
||||
CONSTRAINT ck_wcs_topology_edge_distance CHECK (distance_cost >= 0),
|
||||
CONSTRAINT ck_wcs_topology_edge_capacity CHECK (capacity > 0),
|
||||
CONSTRAINT ck_wcs_topology_edge_direction CHECK (
|
||||
source_node_id <> target_node_id)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_traffic_zone', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_traffic_zone (
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
zone_id varchar(128) NOT NULL,
|
||||
zone_name nvarchar(128) NOT NULL,
|
||||
coordination_mode varchar(32) NOT NULL,
|
||||
CONSTRAINT pk_wcs_traffic_zone PRIMARY KEY (
|
||||
warehouse_code, topology_version, zone_id),
|
||||
CONSTRAINT fk_wcs_traffic_zone_version FOREIGN KEY (
|
||||
warehouse_code, topology_version)
|
||||
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_resource_definition', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_resource_definition (
|
||||
resource_key varchar(320) NOT NULL,
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
resource_type varchar(32) NOT NULL,
|
||||
capacity int NOT NULL,
|
||||
zone_id varchar(128) NULL,
|
||||
CONSTRAINT pk_wcs_resource_definition PRIMARY KEY (
|
||||
warehouse_code, topology_version, resource_key),
|
||||
CONSTRAINT fk_wcs_resource_definition_version FOREIGN KEY (
|
||||
warehouse_code, topology_version)
|
||||
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version),
|
||||
CONSTRAINT ck_wcs_resource_definition_capacity CHECK (capacity > 0)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_device', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_device (
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
device_id varchar(64) NOT NULL,
|
||||
device_type varchar(32) NOT NULL,
|
||||
floor_id varchar(64) NULL,
|
||||
enabled_flag bit NOT NULL CONSTRAINT df_wcs_device_enabled DEFAULT (1),
|
||||
CONSTRAINT pk_wcs_device PRIMARY KEY (
|
||||
warehouse_code, topology_version, device_id),
|
||||
CONSTRAINT fk_wcs_device_version FOREIGN KEY (
|
||||
warehouse_code, topology_version)
|
||||
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version),
|
||||
CONSTRAINT fk_wcs_device_floor FOREIGN KEY (floor_id)
|
||||
REFERENCES dbo.wcs_floor(floor_id)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_device_capability', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_device_capability (
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
device_id varchar(64) NOT NULL,
|
||||
capability_code varchar(64) NOT NULL,
|
||||
CONSTRAINT pk_wcs_device_capability PRIMARY KEY (
|
||||
warehouse_code, topology_version, device_id, capability_code),
|
||||
CONSTRAINT fk_wcs_device_capability_device FOREIGN KEY (
|
||||
warehouse_code, topology_version, device_id)
|
||||
REFERENCES dbo.wcs_device(warehouse_code, topology_version, device_id)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_device_port', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_device_port (
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
port_id varchar(64) NOT NULL,
|
||||
device_id varchar(64) NOT NULL,
|
||||
node_id varchar(128) NOT NULL,
|
||||
port_role varchar(32) NOT NULL,
|
||||
CONSTRAINT pk_wcs_device_port PRIMARY KEY (
|
||||
warehouse_code, topology_version, port_id),
|
||||
CONSTRAINT fk_wcs_device_port_device FOREIGN KEY (
|
||||
warehouse_code, topology_version, device_id)
|
||||
REFERENCES dbo.wcs_device(warehouse_code, topology_version, device_id)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_charger', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_charger (
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
charger_id varchar(64) NOT NULL,
|
||||
node_id varchar(128) NOT NULL,
|
||||
enabled_flag bit NOT NULL CONSTRAINT df_wcs_charger_enabled DEFAULT (1),
|
||||
CONSTRAINT pk_wcs_charger PRIMARY KEY (
|
||||
warehouse_code, topology_version, charger_id),
|
||||
CONSTRAINT fk_wcs_charger_version FOREIGN KEY (
|
||||
warehouse_code, topology_version)
|
||||
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_vehicle_assignment', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_vehicle_assignment (
|
||||
assignment_id varchar(128) NOT NULL PRIMARY KEY,
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
snapshot_version bigint NOT NULL,
|
||||
order_id varchar(64) NOT NULL,
|
||||
step_id varchar(128) NOT NULL,
|
||||
vehicle_id varchar(64) NOT NULL,
|
||||
fencing_token bigint NOT NULL,
|
||||
assignment_state varchar(32) NOT NULL,
|
||||
created_time datetime2(3) NOT NULL,
|
||||
CONSTRAINT uk_wcs_vehicle_assignment_step UNIQUE (step_id),
|
||||
CONSTRAINT ck_wcs_vehicle_assignment_fencing CHECK (fencing_token > 0)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_route_plan', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_route_plan (
|
||||
route_plan_id varchar(128) NOT NULL PRIMARY KEY,
|
||||
assignment_id varchar(128) NOT NULL,
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
node_path_json nvarchar(max) NOT NULL,
|
||||
resource_path_json nvarchar(max) NOT NULL,
|
||||
distance_cost int NOT NULL,
|
||||
turn_count int NOT NULL,
|
||||
route_state varchar(32) NOT NULL,
|
||||
created_time datetime2(3) NOT NULL,
|
||||
CONSTRAINT fk_wcs_route_plan_assignment FOREIGN KEY (assignment_id)
|
||||
REFERENCES dbo.wcs_vehicle_assignment(assignment_id),
|
||||
CONSTRAINT uk_wcs_route_plan_assignment UNIQUE (assignment_id),
|
||||
CONSTRAINT ck_wcs_route_plan_distance CHECK (distance_cost >= 0),
|
||||
CONSTRAINT ck_wcs_route_plan_turns CHECK (turn_count >= 0)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_resource_state', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_resource_state (
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
resource_key varchar(320) NOT NULL,
|
||||
capacity int NOT NULL,
|
||||
reserved_count int NOT NULL,
|
||||
occupied_count int NOT NULL,
|
||||
fencing_token bigint NOT NULL,
|
||||
state_version bigint NOT NULL,
|
||||
CONSTRAINT pk_wcs_resource_state PRIMARY KEY (
|
||||
warehouse_code, topology_version, resource_key),
|
||||
CONSTRAINT fk_wcs_resource_state_definition FOREIGN KEY (
|
||||
warehouse_code, topology_version, resource_key)
|
||||
REFERENCES dbo.wcs_resource_definition(
|
||||
warehouse_code, topology_version, resource_key),
|
||||
CONSTRAINT ck_wcs_resource_state_counts CHECK (
|
||||
capacity > 0 AND reserved_count >= 0 AND occupied_count >= 0
|
||||
AND reserved_count + occupied_count <= capacity),
|
||||
CONSTRAINT ck_wcs_resource_state_fencing CHECK (fencing_token >= 0),
|
||||
CONSTRAINT ck_wcs_resource_state_version CHECK (state_version >= 0)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_resource_reservation', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_resource_reservation (
|
||||
reservation_id varchar(128) NOT NULL,
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
resource_key varchar(320) NOT NULL,
|
||||
order_id varchar(64) NOT NULL,
|
||||
step_id varchar(128) NOT NULL,
|
||||
vehicle_id varchar(64) NOT NULL,
|
||||
reservation_state varchar(32) NOT NULL,
|
||||
fencing_token bigint NOT NULL,
|
||||
release_evidence_id varchar(128) NULL,
|
||||
created_time datetime2(3) NOT NULL,
|
||||
updated_time datetime2(3) NOT NULL,
|
||||
CONSTRAINT pk_wcs_resource_reservation PRIMARY KEY (
|
||||
reservation_id, resource_key),
|
||||
CONSTRAINT fk_wcs_resource_reservation_state FOREIGN KEY (
|
||||
warehouse_code, topology_version, resource_key)
|
||||
REFERENCES dbo.wcs_resource_state(
|
||||
warehouse_code, topology_version, resource_key),
|
||||
CONSTRAINT ck_wcs_resource_reservation_fencing CHECK (fencing_token > 0)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID('dbo.wcs_traffic_decision_event', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.wcs_traffic_decision_event (
|
||||
event_id varchar(128) NOT NULL PRIMARY KEY,
|
||||
warehouse_code varchar(64) NOT NULL,
|
||||
topology_version bigint NOT NULL,
|
||||
snapshot_version bigint NOT NULL,
|
||||
order_id varchar(64) NOT NULL,
|
||||
step_id varchar(128) NOT NULL,
|
||||
vehicle_id varchar(64) NULL,
|
||||
decision_type varchar(16) NOT NULL,
|
||||
reason_code varchar(128) NOT NULL,
|
||||
occurred_time datetime2(3) NOT NULL
|
||||
);
|
||||
END;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
32
sql/wcs/verify_V002__wcs_topology_and_traffic.sql
Normal file
32
sql/wcs/verify_V002__wcs_topology_and_traffic.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT name AS table_name
|
||||
FROM sys.tables
|
||||
WHERE name IN (
|
||||
'wcs_warehouse',
|
||||
'wcs_floor',
|
||||
'wcs_topology_version',
|
||||
'wcs_topology_node',
|
||||
'wcs_topology_edge',
|
||||
'wcs_traffic_zone',
|
||||
'wcs_resource_definition',
|
||||
'wcs_device',
|
||||
'wcs_device_capability',
|
||||
'wcs_device_port',
|
||||
'wcs_charger',
|
||||
'wcs_vehicle_assignment',
|
||||
'wcs_route_plan',
|
||||
'wcs_resource_state',
|
||||
'wcs_resource_reservation',
|
||||
'wcs_traffic_decision_event'
|
||||
)
|
||||
ORDER BY name;
|
||||
|
||||
SELECT c.name AS constraint_name
|
||||
FROM sys.check_constraints c
|
||||
WHERE c.name IN (
|
||||
'ck_wcs_resource_definition_capacity',
|
||||
'ck_wcs_resource_state_counts',
|
||||
'ck_wcs_resource_reservation_fencing'
|
||||
)
|
||||
ORDER BY c.name;
|
||||
Reference in New Issue
Block a user