feat(wcs): coordinate and persist traffic decisions
This commit is contained in:
@@ -34,5 +34,10 @@
|
|||||||
<version>1.2.1</version>
|
<version>1.2.1</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.microsoft.sqlserver</groupId>
|
||||||
|
<artifactId>mssql-jdbc</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</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");
|
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,
|
public TrafficDecision decide(TrafficCandidate candidate,
|
||||||
ResourceLedger ledger,
|
ResourceLedger ledger,
|
||||||
EffectiveTopology topology) {
|
EffectiveTopology topology) {
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ public final class WaitForGraph {
|
|||||||
return Collections.unmodifiableSet(new LinkedHashSet<String>(holders));
|
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() {
|
private Map<String, Set<String>> copyEdges() {
|
||||||
Map<String, Set<String>> copy = new LinkedHashMap<String, Set<String>>();
|
Map<String, Set<String>> copy = new LinkedHashMap<String, Set<String>>();
|
||||||
for (Map.Entry<String, Set<String>> entry : edges.entrySet()) {
|
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