feat: add legacy dispatch cycle demo

This commit is contained in:
zgw
2026-07-30 11:26:26 +08:00
parent 75ff7ec5de
commit 3860e3f014
6 changed files with 652 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
package com.ft546.wcs.demo.center;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class CycleResult {
private final int cycleNumber;
private final List<String> processedVehicleIds;
private final List<String> events;
public CycleResult(
int cycleNumber,
List<String> processedVehicleIds,
List<String> events
) {
this.cycleNumber = cycleNumber;
this.processedVehicleIds = Collections.unmodifiableList(
new ArrayList<String>(processedVehicleIds)
);
this.events = Collections.unmodifiableList(new ArrayList<String>(events));
}
public int getCycleNumber() {
return cycleNumber;
}
public List<String> getProcessedVehicleIds() {
return processedVehicleIds;
}
public List<String> getEvents() {
return events;
}
}

View File

@@ -0,0 +1,154 @@
package com.ft546.wcs.demo.center;
import com.ft546.wcs.demo.model.DecisionCode;
import com.ft546.wcs.demo.model.DispatchQueueEntry;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import com.ft546.wcs.demo.repository.InMemoryWcsState;
import com.ft546.wcs.demo.simulation.SimulatedProtocolAdapter;
import com.ft546.wcs.demo.traffic.TrafficController;
import com.ft546.wcs.demo.traffic.TrafficDecision;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public final class LegacyDispatchCenter {
private final InMemoryWcsState state;
private final QueueAdmissionService admission;
private final TrafficController traffic;
private final SimulatedProtocolAdapter protocol;
public LegacyDispatchCenter(
InMemoryWcsState state,
QueueAdmissionService admission,
TrafficController traffic,
SimulatedProtocolAdapter protocol
) {
if (state == null || admission == null || traffic == null || protocol == null) {
throw new IllegalArgumentException(
"state, admission, traffic and protocol are required"
);
}
this.state = state;
this.admission = admission;
this.traffic = traffic;
this.protocol = protocol;
}
public CycleResult runCycle() {
int eventStart = state.getEvents().size();
int cycleNumber = state.nextCycle();
state.addEvent("CYCLE " + cycleNumber);
admission.createPendingQueue(state);
List<VehicleSnapshot> vehicles =
new ArrayList<VehicleSnapshot>(state.getVehicles());
Collections.sort(vehicles, new Comparator<VehicleSnapshot>() {
@Override
public int compare(VehicleSnapshot left, VehicleSnapshot right) {
return left.getVehicleId().compareTo(right.getVehicleId());
}
});
List<String> processed = new ArrayList<String>();
for (VehicleSnapshot frozenVehicle : vehicles) {
processed.add(frozenVehicle.getVehicleId());
Optional<DispatchQueueEntry> optional =
state.findQueueByVehicle(frozenVehicle.getVehicleId());
if (!optional.isPresent()) {
continue;
}
DispatchQueueEntry queue = optional.get();
if (queue.isIssued()) {
processIssuedQueue(frozenVehicle, queue);
} else {
processUnissuedQueue(frozenVehicle, queue);
}
}
List<String> cycleEvents = new ArrayList<String>(
state.getEvents().subList(eventStart, state.getEvents().size())
);
return new CycleResult(cycleNumber, processed, cycleEvents);
}
private void processUnissuedQueue(
VehicleSnapshot vehicle,
DispatchQueueEntry queue
) {
TrafficDecision decision = traffic.plan(
vehicle,
queue,
state.getVehicles(),
state.getActiveQueues()
);
if (decision.getCode() == DecisionCode.AVOIDANCE_CREATED) {
DispatchQueueEntry avoidance = decision.getAvoidanceQueue().get();
state.addQueue(avoidance);
state.addEvent(
"AVOID queue=" + avoidance.getQueueId() + " reason="
+ decision.getReason()
);
return;
}
if (decision.getCode() != DecisionCode.PATH_READY) {
state.addEvent(
"HOLD " + decision.getCode() + " queue=" + queue.getQueueId()
+ " reason=" + decision.getReason()
);
return;
}
state.addEvent("PATH queue=" + queue.getQueueId() + " " + decision.getPath());
SimulatedProtocolAdapter.ProtocolResult result =
protocol.send(queue, decision.getPath());
if (!result.isSuccess()) {
state.addEvent(
"HOLD " + DecisionCode.HOLD_PROTOCOL_FAILURE + " queue="
+ queue.getQueueId() + " reason=" + result.getMessage()
);
return;
}
queue.markIssued(decision.getPath());
state.addEvent("SEND queue=" + queue.getQueueId() + " vehicle="
+ queue.getVehicleId());
}
private void processIssuedQueue(
VehicleSnapshot vehicle,
DispatchQueueEntry queue
) {
if (vehicle.getCurrentSite().equals(queue.getEndSite())) {
complete(queue);
return;
}
int index = queue.getPath().indexOf(vehicle.getCurrentSite());
if (index < 0 || index + 1 >= queue.getPath().size()) {
state.addEvent(
"HOLD " + DecisionCode.HOLD_NO_PATH + " queue=" + queue.getQueueId()
+ " reason=vehicle is outside issued path"
);
return;
}
String nextSite = queue.getPath().get(index + 1);
state.updateVehicle(vehicle.at(nextSite, vehicle.getFloor()));
state.addEvent(
"MOVE vehicle=" + vehicle.getVehicleId() + " site=" + nextSite
);
if (nextSite.equals(queue.getEndSite())) {
complete(queue);
}
}
private void complete(DispatchQueueEntry queue) {
if (state.completeQueue(queue.getQueueId())) {
state.addEvent(
"COMPLETE queue=" + queue.getQueueId() + " vehicle="
+ queue.getVehicleId()
);
}
}
}

View File

@@ -0,0 +1,71 @@
package com.ft546.wcs.demo.center;
import com.ft546.wcs.demo.model.DispatchQueueEntry;
import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import com.ft546.wcs.demo.repository.InMemoryWcsState;
import com.ft546.wcs.demo.traffic.GridMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public final class QueueAdmissionService {
private final VehicleAssignmentService assignmentService;
private final GridMap map;
private int invocationCount;
public QueueAdmissionService(
VehicleAssignmentService assignmentService,
GridMap map
) {
if (assignmentService == null || map == null) {
throw new IllegalArgumentException("assignment service and map are required");
}
this.assignmentService = assignmentService;
this.map = map;
}
public Optional<DispatchQueueEntry> createPendingQueue(InMemoryWcsState state) {
if (state == null) {
throw new IllegalArgumentException("state must not be null");
}
invocationCount++;
if (state.getPendingTasks().isEmpty()) {
return Optional.empty();
}
DispatchTask task = state.getPendingTasks().get(0);
List<VehicleSnapshot> available = new ArrayList<VehicleSnapshot>();
for (VehicleSnapshot vehicle : state.getVehicles()) {
if (!state.findQueueByVehicle(vehicle.getVehicleId()).isPresent()) {
available.add(vehicle);
}
}
VehicleAssignmentDecision decision =
assignmentService.select(task, available, map.getSites());
if (!decision.getVehicle().isPresent()) {
state.addEvent(
"HOLD " + decision.getCode() + " task=" + task.getTaskId()
+ " reason=" + decision.getReason()
);
return Optional.empty();
}
VehicleSnapshot selected = decision.getVehicle().get();
DispatchQueueEntry queue = DispatchQueueEntry.business(
task,
selected.getVehicleId()
);
state.admitTask(task, queue);
state.addEvent(
"ASSIGN task=" + task.getTaskId() + " vehicle="
+ selected.getVehicleId() + " reason=" + decision.getReason()
);
return Optional.of(queue);
}
public int getInvocationCount() {
return invocationCount;
}
}

View File

@@ -0,0 +1,152 @@
package com.ft546.wcs.demo.repository;
import com.ft546.wcs.demo.model.DispatchQueueEntry;
import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public final class InMemoryWcsState {
private final Map<String, VehicleSnapshot> vehicles =
new LinkedHashMap<String, VehicleSnapshot>();
private final Map<String, DispatchTask> pendingTasks =
new LinkedHashMap<String, DispatchTask>();
private final Set<String> knownTaskIds = new LinkedHashSet<String>();
private final Map<String, DispatchQueueEntry> activeQueues =
new LinkedHashMap<String, DispatchQueueEntry>();
private final List<DispatchQueueEntry> history =
new ArrayList<DispatchQueueEntry>();
private final List<String> events = new ArrayList<String>();
private int cycleNumber;
public void addVehicle(VehicleSnapshot vehicle) {
if (vehicle == null) {
throw new IllegalArgumentException("vehicle must not be null");
}
if (vehicles.containsKey(vehicle.getVehicleId())) {
throw new IllegalArgumentException(
"duplicate vehicle: " + vehicle.getVehicleId()
);
}
vehicles.put(vehicle.getVehicleId(), vehicle);
}
public void updateVehicle(VehicleSnapshot vehicle) {
if (vehicle == null || !vehicles.containsKey(vehicle.getVehicleId())) {
throw new IllegalArgumentException("cannot update unknown vehicle");
}
vehicles.put(vehicle.getVehicleId(), vehicle);
}
public void addTask(DispatchTask task) {
if (task == null) {
throw new IllegalArgumentException("task must not be null");
}
if (!knownTaskIds.add(task.getTaskId())) {
throw new IllegalArgumentException("duplicate task: " + task.getTaskId());
}
pendingTasks.put(task.getTaskId(), task);
}
public void admitTask(DispatchTask task, DispatchQueueEntry queue) {
if (task == null || !pendingTasks.containsKey(task.getTaskId())) {
throw new IllegalArgumentException("task is not pending: " + task);
}
addQueue(queue);
pendingTasks.remove(task.getTaskId());
}
public void addQueue(DispatchQueueEntry queue) {
if (queue == null) {
throw new IllegalArgumentException("queue must not be null");
}
if (activeQueues.containsKey(queue.getQueueId())) {
throw new IllegalArgumentException("duplicate queue: " + queue.getQueueId());
}
if (findQueueByVehicle(queue.getVehicleId()).isPresent()) {
throw new IllegalStateException(
"vehicle already has an active queue: " + queue.getVehicleId()
);
}
activeQueues.put(queue.getQueueId(), queue);
}
public boolean completeQueue(String queueId) {
DispatchQueueEntry queue = activeQueues.remove(queueId);
if (queue == null) {
return false;
}
queue.markCompleted();
history.add(queue);
return true;
}
public List<VehicleSnapshot> getVehicles() {
return Collections.unmodifiableList(
new ArrayList<VehicleSnapshot>(vehicles.values())
);
}
public List<DispatchTask> getPendingTasks() {
List<DispatchTask> result =
new ArrayList<DispatchTask>(pendingTasks.values());
Collections.sort(result, new Comparator<DispatchTask>() {
@Override
public int compare(DispatchTask left, DispatchTask right) {
int priority = Integer.compare(right.getPriority(), left.getPriority());
if (priority != 0) {
return priority;
}
return left.getTaskId().compareTo(right.getTaskId());
}
});
return Collections.unmodifiableList(result);
}
public List<DispatchQueueEntry> getActiveQueues() {
return Collections.unmodifiableList(
new ArrayList<DispatchQueueEntry>(activeQueues.values())
);
}
public List<DispatchQueueEntry> getHistory() {
return Collections.unmodifiableList(history);
}
public Optional<DispatchQueueEntry> findQueueByVehicle(String vehicleId) {
for (DispatchQueueEntry queue : activeQueues.values()) {
if (queue.getVehicleId().equals(vehicleId)) {
return Optional.of(queue);
}
}
return Optional.empty();
}
public void addEvent(String event) {
if (event == null || event.trim().isEmpty()) {
throw new IllegalArgumentException("event must not be blank");
}
events.add(event);
}
public List<String> getEvents() {
return Collections.unmodifiableList(events);
}
public int nextCycle() {
cycleNumber++;
return cycleNumber;
}
public int getCycleNumber() {
return cycleNumber;
}
}

View File

@@ -0,0 +1,62 @@
package com.ft546.wcs.demo.simulation;
import com.ft546.wcs.demo.model.DispatchQueueEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class SimulatedProtocolAdapter {
private final List<String> attemptedQueueIds = new ArrayList<String>();
private String nextFailure;
public ProtocolResult send(DispatchQueueEntry queue, List<String> path) {
if (queue == null || path == null || path.isEmpty()) {
throw new IllegalArgumentException("queue and non-empty path are required");
}
attemptedQueueIds.add(queue.getQueueId());
if (nextFailure != null) {
String failure = nextFailure;
nextFailure = null;
return ProtocolResult.failure(failure);
}
return ProtocolResult.success();
}
public void failNextSend(String reason) {
if (reason == null || reason.trim().isEmpty()) {
throw new IllegalArgumentException("failure reason must not be blank");
}
nextFailure = reason;
}
public List<String> getAttemptedQueueIds() {
return Collections.unmodifiableList(attemptedQueueIds);
}
public static final class ProtocolResult {
private final boolean success;
private final String message;
private ProtocolResult(boolean success, String message) {
this.success = success;
this.message = message;
}
public static ProtocolResult success() {
return new ProtocolResult(true, "accepted");
}
public static ProtocolResult failure(String message) {
return new ProtocolResult(false, message);
}
public boolean isSuccess() {
return success;
}
public String getMessage() {
return message;
}
}
}