Step 9: Queues and ServiceWithResources
We can use queues so that a temporary lack of resources does not disrupt warehouse operations. When some resource is requested, and all suitable resources are busy, we will put the request in the queue. As busy resources are released, we will take the requests from the queue and assign these requests to the released resources.
For the forklifts, we will use standard JDK collections.
And for the gates, we will use a special class from the Amalgama Platform toolkit - ServiceWithResources.
The new version of the Dispatcher class will look as follows:
package com.company.warehouse.simulation;
import com.amalgamasimulation.engine.Engine;
import com.amalgamasimulation.service.ServiceWithResources;
import com.amalgamasimulation.service.ServiceWithResources.ResourceSeizingRule;
import com.company.warehouse.datamodel.Direction;
import com.company.warehouse.simulation.equipment.Forklift;
import com.company.warehouse.simulation.equipment.Truck;
import com.company.warehouse.simulation.tasks.MovePalletTask;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.function.BiConsumer;
public class Dispatcher {
private final Engine engine;
private final Model model;
private final Map<Direction, ServiceWithResources<Truck, Gate>> gates = new HashMap<>();
private final Map<Direction, Queue<Gate>> pendingGatesWithTrucks = Map.of(
Direction.IN, new LinkedList<>(),
Direction.OUT, new LinkedList<>()
);
// tag::availableForklifts[]
private final Queue<Forklift> availableForklifts = new LinkedList<>();
// end::availableForklifts[]
private final Queue<BiConsumer<Forklift, Runnable>> forkliftRequests = new LinkedList<>();
public Dispatcher(Model model) {
engine = model.engine();
this.model = model;
model.getForklifts().forEach(f -> addAvailableForklift(f));
for (var d : Direction.values()) {
gates.put(d, new ServiceWithResources<>(engine, model.getGatesInDirection(d), ResourceSeizingRule.LEAST_USED));
}
}
// tag::addAvailableForklift[]
private void addAvailableForklift(Forklift forklift) {
final var request = forkliftRequests.poll();
if (request != null) {
request.accept(forklift, () -> addAvailableForklift(forklift));
} else {
availableForklifts.add(forklift);
}
}
// end::addAvailableForklift[]
// tag::requestForklift[]
private void requestForklift(BiConsumer<Forklift, Runnable> request) {
final var forklift = availableForklifts.poll();
if (forklift != null) {
request.accept(forklift, () -> addAvailableForklift(forklift));
} else {
forkliftRequests.add(request);
}
}
// end::requestForklift[]
public void truckArrived(Truck truck) {
final var suitableGates = gates.get(truck.getDirection());
suitableGates.placeRequest(truck, (s, gate) ->
handleTruckOnGate(truck, gate, () ->
suitableGates.release(truck)
)
);
}
private void handleTruckOnGate(Truck truck, Gate gate, Runnable onComplete) {
final var direction = gate.getDirection();
final var oppositeDirection = (direction == Direction.IN) ? Direction.OUT : Direction.IN;
gate.parkTruck(truck);
final var oppositeGate = pendingGatesWithTrucks.get(oppositeDirection).poll();
if (oppositeGate == null) {
pendingGatesWithTrucks.get(direction).add(gate);
return;
}
handleGatePair(gate, oppositeGate, () -> {
gate.unparkTruck(truck);
onComplete.run();
});
}
private void handleGatePair(Gate gate, Gate oppositeGate, Runnable onComplete) {
final var truck = gate.getTruck().get();
final var oppositeTruck = oppositeGate.getTruck().get();
requestForklift((forklift, forkliftReleaser) ->
handleGatePairWithForklift(truck, oppositeTruck, forklift, () -> {
forkliftReleaser.run();
oppositeGate.unparkTruck(oppositeTruck);
gates.get(oppositeTruck.getDirection()).release(oppositeTruck);
onComplete.run();
})
);
}
private void handleGatePairWithForklift(Truck truck, Truck oppositeTruck, Forklift forklift, Runnable onComplete) {
final boolean loading = truck.getDirection() == Direction.OUT;
if (!truck.isAvailableFor(loading)) {
onComplete.run();
return;
}
newMovePalletTask(forklift, truck, oppositeTruck, loading).start(() ->
handleGatePairWithForklift(truck, oppositeTruck, forklift, onComplete)
);
}
private MovePalletTask newMovePalletTask(Forklift forklift, PalletContainer a, PalletContainer b, boolean reverse) {
final PalletContainer from = reverse ? b : a;
final PalletContainer to = reverse ? a : b;
return new MovePalletTask(engine, forklift, from, to);
}
}
|
Add
|
Idle forklifts, as well as requests for them, are stored as LinkedList collections.
A request will be represented by a BiConsumer that will accept a Forklift and a Runnable that will be called after the task is completed in order to return this forklift to the queue of available equipment units.
We call the addAvailableForklift() method from the constructor for all forklifts.
This method is also called from the Runnable that releases the forklift to return the forklift back to the queue of available equipment units.
The requestForklift() method can satisfy the request immediately, using the first available forklift found, if any.
Alternatively, if there are no available forklifts, the method enqueues the request, and the request is satisfied later.
As we can see, it took us 2 collections (one for forklifts and another one for requests) to implement the ability to cope with a temporary lack of resources.
For the gates, we will make use of the ready-to-go solution that the Amalgama Platform offers.
The ServiceWithResources class is intended to cater to queuing logic.
As we have gates of 2 types, we will need 2 instances of the ServiceWithResources class - one for each type.
Let us put them into the map, having Direction as its key.
We will pass our resources (i.e., the gates of each direction) to the constructor of the ServiceWithResources class.
Whenever we want to seize a gate, we place a request for it using the placeRequest() method.
When we stop using the gate, we release it by calling the release() method.
Note how the ServiceWithResources class frees us from organizing the queue manually.
Moreover, it also provides an API for standard statistics of queuing and resources utilization.
The structure of our warehouse remained conceptually the same as it was in the previous step.
However, now we correctly handle the case of the required resource (gate or forklift) not being instantly available.
In such cases, we use the Runnable to specify the action to be executed when a resource becomes available.
So, we pass this Runnable to the completion step methods of our Dispatcher class for asynchronous execution in the future.
Let us now run the simulation and make sure the forklifts operate constantly throughout the simulation period.