Step 7: Assigning requests to trucks

Here comes the most complex part of the simulation model logic: we need to coordinate trucks to take transportation requests.

The process of assigning a truck to a request will be triggered by events of two types:

  • a new transportation request has come;

  • a truck that was busy is now free.

The truck selection logic depends on the trigger event:

  • In case of a new transportation request, we will assign the request to the first available truck; but if all trucks are busy - the request should be placed in the waiting queue.

  • In case of a truck becoming free, we will take that truck and pick the first request from the request waiting queue, if any.

Now that we have discussed the request processing, we will put this logic into the Dispatcher class.

The Dispatcher class

Create the following Dispatcher class in the 'tutorial.model' package:

Dispatcher.java
package tutorial.model;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.function.Consumer;

public class Dispatcher {
    private final Model model;
    private int lastTaskId = 0;
    private List<TransportationTask> transportationTasks = new ArrayList<>();
    private Queue<TransportationTask> waitingTasks = new LinkedList<>();
    private List<Consumer<TransportationTask>> taskStateChangeHandlers = new ArrayList<>();

    public Dispatcher(Model model) {
        this.model = model;
    }

    public List<TransportationTask> getTransportationTasks() {
        return transportationTasks;
    }
    
    public void addTaskStateChangeHandler(Consumer<TransportationTask> handler) {
        taskStateChangeHandlers.add(handler);
    }
    
    public void clearTaskStateChangeHandlers() {
        taskStateChangeHandlers.clear();
    }
    
    public void onNewRequest(TransportationRequest newRequest) {
        TransportationTask task = new TransportationTask(String.valueOf(++lastTaskId), newRequest, this::onTaskCompleted, model);
        transportationTasks.add(task);
        onTaskStateChanged(task);
        Optional<Truck> freeTruck = model.getTrucks().stream().filter(Truck::isIdle).findFirst();
        if (freeTruck.isPresent()) {
            task.execute(freeTruck.get());
            onTaskStateChanged(task);
        } else {
            addWaitingTask(task);
        }
    }

    private void onTaskCompleted(TransportationTask task) {
        onTaskStateChanged(task);
        TransportationTask waitingTask = getNextWaitingTask();
        if (waitingTask != null) {
            waitingTask.execute(task.getTruck());
            onTaskStateChanged(waitingTask);
        }
    }
    
    private TransportationTask getNextWaitingTask() {
        return waitingTasks.poll();
    }
    
    private void addWaitingTask(TransportationTask task) {
        waitingTasks.add(task);
    }
    
    private void onTaskStateChanged(TransportationTask task) {
        taskStateChangeHandlers.forEach(handler -> handler.accept(task));
    }
}

To keep our Dispatcher object updated about any new requests, we add a onNewRequest() method that gets called from outside the class whenever a transportation request is generated. In this method, we look for the first available truck and assign the received transportation request to the found truck (or place the request in the queue).

The onTruckRelease() method gets called from a TransportationTask object upon cargo delivery.

Changes to the Model class

In the Model class, a new Dispatcher object will be created and subscribed to new requests.

Add a new field to the Model class:

Model.java, new field
private final Dispatcher dispatcher;

Add the following code to the Model class constructor:

Model.java, creating a Dispatcher object in the class constructor
dispatcher = new Dispatcher(this);
requestGenerator.addNewRequestHandler(dispatcher::onNewRequest);

Check the result

Run the program and see if you get the following output to the console:

0,000	Request #1 created
0,000	Task #1 : TRANSPORTATION_STARTED. Request #1; Truck #1 at Store 1; From Warehouse 3 -> To Store 2
1,024	Request #2 created
2,500	Task #1 : TRANSPORTATION_FINISHED
2,500	Task #2 : TRANSPORTATION_STARTED. Request #2; Truck #1 at Store 2; From Warehouse 1 -> To Store 3
3,733	Request #3 created
3,871	Request #4 created
3,877	Request #5 created
4,037	Request #6 created
4,750	Task #2 : TRANSPORTATION_FINISHED
4,750	Task #3 : TRANSPORTATION_STARTED. Request #3; Truck #1 at Store 3; From Warehouse 3 -> To Store 3
4,789	Request #7 created
6,750	Task #3 : TRANSPORTATION_FINISHED
6,750	Task #4 : TRANSPORTATION_STARTED. Request #4; Truck #1 at Store 3; From Warehouse 2 -> To Store 1
7,358	Request #8 created
8,120	Request #9 created
8,197	Request #10 created
9,000	Task #4 : TRANSPORTATION_FINISHED
9,000	Task #5 : TRANSPORTATION_STARTED. Request #5; Truck #1 at Store 1; From Warehouse 1 -> To Store 3
9,129	Request #11 created
10,034	Request #12 created
10,803	Request #13 created
11,250	Task #5 : TRANSPORTATION_FINISHED
11,250	Task #6 : TRANSPORTATION_STARTED. Request #6; Truck #1 at Store 3; From Warehouse 1 -> To Store 2

Note how the request waiting queue is used here. When Request #2 gets created (at time 1.024), there are no available trucks, so the request goes to the waiting queue. Truck #1 then finishes its transportation at 2.500 and immediately takes the Request #2 from the waiting queue.

What about truck travel duration, is it correct? Consider Task #1. Truck speed is 40 km/h. It needs to travel empty from its initial position at Store-1 to Warehouse-3 (50 km, see static{} section in the Main class) and then bring cargo from Warehouse-3 to Store-2 (another 50 km), for a total of 100 km. Time required is 100 km / 40 km/h = 2.5 hours, and we see that Task #1 is finished exactly at that time.