feat: add legacy dispatch cycle demo
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user