Step 11: Storage areas. Reserving places

In the previous step, we successfully implemented queuing for gates and forklifts. Now it’s time to make the storage areas do their job — storing pallets. We can note that all storage spaces of the warehouse are also a limited resource. Thus, at some point, we can run out of them.

During this step, we will implement the process of unloading pallets from incoming trucks to the storage area near the gates. Also, symmetrically, we will implement the usage of storage areas for loading outgoing trucks.

1. Cleaning the code

Before we go on, remove the factor of 20 from the Model.spawnTrucks() method. Remember how we added it in the previous step for experimental purposes.

2. Reserving

When several forklifts operate simultaneously, there is a danger that more than one of them will want to grab the same pallet or put the pallets into the same storage space. This will obviously lead to an inadequacy of our model: a single storage space cannot store multiple pallets. To avoid such conflicts, let’s add reservation logic for the pallet spaces.

It is important to note that we will need to be notified when a pallet space becomes available or occupied.

PalletPosition.java
public class PalletPosition implements PalletContainer {
...
    /**
     * Two callbacks to notify about position busy state change.
     * Former, when pallet is placed in this position.
     * Latter, when place is freed.
     */
    private final Map<Boolean, Optional<Consumer<PalletPosition>>> onBusyCallbacks = new HashMap<>(Map.of(
            true, Optional.empty(),
            false, Optional.empty()));
...
    private boolean reserved;
    public boolean isReserved() {
        return reserved;
    }
...
    public void reserve() {
        if (reserved) {
            throw new RuntimeException("Can't reserve already reserved position");
        }
        reserved = true;
    }
...
// update to use callbacks and reserved state
    public void placePallet(boolean loading) {
        if (busy == loading) {
            throw new RuntimeException("Can't " + (busy ? "put to a busy" : "take from a free") + " position");
        }
        busy = loading;
        reserved = false;
        onBusyCallbacks.get(busy).ifPresent(callback -> callback.accept(this));
    }
...
    public void onBusySubscribe(boolean busy, Consumer<PalletPosition> callback) {
        onBusyCallbacks.get(busy)
            .ifPresent(c -> {throw new RuntimeException("onBusy callback already present");});
        onBusyCallbacks.put(busy, Optional.of(callback));
        if (this.busy == busy) {
            callback.accept(this);
        }
    }
...
// update to display reserved state
    public String toString() {
        return "PalletPosition [" + node + ", busy=" + busy + ", reserved=" + reserved + "]";
    }

Let’s update the visualization of pallet spaces so that we can quickly see whether they are reserved or not.

To do this, replace its unconditionally gray border line with the following code:

PalletPositionShape.java
//                .withLineColor(Color.gray)
                .withLineColor(() -> p.isReserved() ? Color.red : Color.gray)

3. Storage areas

As you might remember, our warehouse has several storage areas. Specifically, these are the main storage area and dispatch areas next to each gate.

These zones have different purposes. However, all storage spaces within each zone are equal. Let’s create a StorageArea class that would allow us to ask for the available and occupied storage spaces.

StorageArea.java
package com.company.warehouse.simulation;

import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.stream.Stream;

public class StorageArea {

    /**
     * All the pallet positions of this area.
     */
    private LinkedList<PalletPosition> places;

    /**
     * Two queues for requests of different kinds.
     * Former for empty postions.
     * Latter for busy ones.
     */
    private Map<Boolean, Queue<Consumer<PalletPosition>>> requests = Map.of(
            false, new LinkedList<Consumer<PalletPosition>>(),
            true, new LinkedList<Consumer<PalletPosition>>());

    public StorageArea(LinkedList<PalletPosition> places) {
        this.places = places;
        for (var p : places) {
            p.onBusySubscribe(false, this::placeBusyChanged);
            p.onBusySubscribe(true, this::placeBusyChanged);
        }
    }

    /**
     * Returns all non reserved positions that are available for loading (empty) or unloading (busy)
     * @param loading  are empty positions requested?
     * @return  a stream of pallet positions
     */
    public Stream<PalletPosition> getPlacesAvailableFor(boolean loading) {
        return places.stream()
            .filter(p -> p.isAvailableFor(loading) && !p.isReserved())
            .peek(p -> p.reserve())
            ;
    }

    /**
     * Requests a position of a specified busy state.
     * @param busy  state of requested position
     * @param request  callback that will receive requested position as soon as one become available
     */
    public void reservePlace(boolean busy, Consumer<PalletPosition> request) {
        final var iterator = busy ? places.iterator() : places.descendingIterator();

        while (iterator.hasNext()) {
            final var p = iterator.next();
            if (p.isAvailableFor(!busy) && !p.isReserved()) {
                reserveAndComplete(p, request);
                return;
            }
        }

        requests.get(busy).add(request);
    }

    /**
     * Is called upon any of position busy state change
     */
    private void placeBusyChanged(PalletPosition p) {
        final var request = requests.get(p.isAvailableFor(false)).poll();
        if (request != null) {
            reserveAndComplete(p, request);
        }
    }

    /**
     * Reserves the position and invokes the callback
     */
    private void reserveAndComplete(PalletPosition p, Consumer<PalletPosition> request) {
        p.reserve();
        request.accept(p);
    }

}

Now we associate each gate with its own dispatch storage area:

Gate.java
public class Gate {
...
    private final StorageArea storageArea;
    public StorageArea getStorageArea() {
        return storageArea;
    }
...
    public Gate(Direction direction, Node entrance, StorageArea storage) {
        this.direction = direction;
        this.entrance = entrance;
        this.storageArea = storage;
    }
...

In the model, we assign a storage area to every instance of the gate. Additionally, let’s make all incoming dispatch areas empty and all outgoing dispatch areas full. This will help us start the simulation smoothly, i.e., without long queues of incoming and outgoing trucks.

Model.java
...
public class Model extends com.amalgamasimulation.engine.Model {
...
    private void initializeGates() {
        for (var scenarioGate : scenario.getGates()) {
            final var direction = scenarioGate.getDirection();
            final var entrance = mapping.nodesMap.get(scenarioGate.getEntrance());
            final var places = scenarioGate.getPlaces().stream()
                    .map(scenarioNode -> newPalletPosition(scenarioNode, direction == Direction.OUT))
                    .collect(Collectors.toCollection(LinkedList::new));
            final var gate = new Gate(direction, entrance, new StorageArea(places));
            getGatesInDirection(direction).add(gate);
        }
    }
...

Great! It’s time to use all the new functionality we’ve added so far.

4. Tasks for trucks loading and unloading

It is not difficult to see that loading and unloading a truck are symmetrical tasks. Indeed, in both cases a single forklift should either:

  • move all the pallets from the dispatch area to the initially empty truck (loading), or

  • move all the pallets from the truck to the dispatch area (unloading).

HandleTruckTask.java
package com.company.warehouse.simulation.tasks;

import com.amalgamasimulation.engine.Engine;
import com.company.warehouse.datamodel.Direction;
import com.company.warehouse.simulation.Gate;
import com.company.warehouse.simulation.equipment.Forklift;
import com.company.warehouse.simulation.equipment.Truck;

public class HandleTruckTask extends Task {

    private final Truck truck;
    private final Gate gate;
    private final Forklift forklift;
    private final boolean loading;

    public HandleTruckTask(Engine engine, Truck truck, Gate gate, Forklift forklift) {
        super(engine);
        this.truck = truck;
        this.gate = gate;
        this.forklift = forklift;
        loading = truck.getDirection() == Direction.OUT;
    }

    @Override
    public void run() {
        if (truck.isAvailableFor(loading)) {
            gate.getStorageArea().reservePlace(loading, place ->
                new MovePalletTask(engine, forklift, truck, place, loading).start(
                    () -> run()
                )
            );
        } else {
            finish();
        }
    }

}

The run() method consequently requests the pallet spaces at the corresponding storage area. When the method receives an available storage space, it assigns the task to a forklift. The task is to move one pallet from/to this storage space. After completion, the run() method recursively calls itself until the truck is either emptied or fully loaded.

Here, we have used a new constructor of MovePalletTask that assigns the direction of transportation (either loading or unloading) based on a boolean parameter value:

MovePalletTask.java, a new constructor (keep the old one, too!)
    public MovePalletTask(Engine engine, Forklift forklift, PalletContainer a, PalletContainer b, boolean reverse) {
        this(engine, forklift, reverse ? b : a, reverse ? a : b);
    }

5. Putting it all together

To make Dispatcher make use of storage areas, we will update the handleTruckOnGate() method like this:

Dispatcher.java
...
    private void handleTruckOnGate(Truck truck, Gate gate, Runnable onComplete) {
        handleTruck(truck, gate, onComplete);
    }
...
    private void handleTruck(Truck truck, Gate gate, Runnable onComplete) {
        gate.parkTruck(truck);
        requestForklift((forklift, forkliftReleaser) ->
            new HandleTruckTask(engine, truck, gate, forklift).start(() -> {
                forkliftReleaser.run();
                gate.unparkTruck(truck);
                onComplete.run();
            })
        );
    }
...

You may also remove handleGatePair(), handleGatePairWithForklift() and newMovePalletTask() methods. We no longer need them.

After that, let’s run the simulation…​

As we can see, the forklifts are constantly busy loading and unloading the trucks until the dispatch area is fully occupied with pallets.

Dispatch area

So, our current goal is achieved. In the next section, we will add using the main storage zone to our model.