Wednesday, November 16, 2016

Finite State Machine using Akka (2): modeling multiple elevators in a hotel

This is second part of the series that I am writing on this topic. The first part is here.

In this post, I will elaborate the code structure a bit. This may help in getting hold of the design I have chosen.

To help us get the context, here’s a summary of the Use-Case we are targeting:
A hotel employs a number of lifts/elevators. A Controller coordinates their movement. A lift’s carriage gets its inputs (press on a button) either from a passenger waiting at floor’s lobby or from a passenger inside it, who chooses the floor she wants to go to, using the panel mounted inside.

Here’s is a block diagram showing for components that our model is using.

Implementation

We will form the right solution for the Use-Case (outlined at the beginning), step-by-step.

A carriage is represented as an Actor:
class LiftCarriageWithMovingState (val movementHWIndicator: ActorRef) extends Actor
 with LoggingFSM[LiftState,LiftData]
 with ActorLogging
{ // …
}

The construction parameter movementHWIndicator is the actor which represents the hardware circuit associated the carriage which indicates end of movement (i.e., arrival at a floor). We are representing that as a trait:
trait MovingStateSimulator extends Actor with ActorLogging {
// ..
}

So, at the time of construction, LiftCarriageWithMovingState receives an ActorRef which refers to an instance of MovingStateSimulator.

The states that the carriage can be in, are enumerated as follows:
sealed trait LiftState
object PoweredOff extends LiftState
object PoweredOn  extends LiftState
object Ready      extends LiftState
object Waiting    extends LiftState
object Moving     extends LiftState
object Stopped    extends LiftState

A vector named pendingPassengerRequests holds the request for movement that have reached the carriage so far. The head of pendingPassengerRequests    always represents the next floor to stop.
private var pendingPassengerRequests: Vector[NextStop] = Vector.empty

Every stop is represented as a tuple of floor’s number and the reason why it is moving to it:
case class NextStop(floorID: Int, purposeOfMovement: PurposeOfMovement)

object PurposeOfMovement extends Enumeration {
 type PurposeOfMovement = Value
 val ToWelcomeInAnWaitingPassenger, ToAllowATransportedPassengerAlight = Value
}

After it is switched on, every carriage is at the ground floor and is in Ready state.

While in the Moving state, the carriage responds to three events:
when (Moving)              {
   case Event(ReachedFloor(currentStop),_) =>
     currentFloorID = pendingPassengerRequests.head.floorID
     pendingPassengerRequests = pendingPassengerRequests.tail
     goto (Stopped)

   case Event(PassengerIsWaitingAt(floorID),_)      =>
        this.pendingPassengerRequests = accumulateWaitingRequest(floorID)
        stay

   case Event(PassengerRequestsATransportTo(floorIDs),_)                =>
     this.pendingPassengerRequests = accumulateTransportRequest(floorIDs)
     stay
 }

While in the Stopped state, the carriage waits for some time for the door to open, to let the passengers come in or go out, and then for it to close. If there is no pending requests, it goes back to Ready state:
private val actWhenStoppedForLongEnough: StateFunction = {
   case Event(StateTimeout, _)   =>
     if (this.pendingPassengerRequests isEmpty) {
       log.debug("Stopped.timeout, No pending passenger requests")
       goto (Ready)
     }
     else {
       log.debug( s"Stopped.timeout, moving to floor:( ${this.pendingPassengerRequests.head} )")
       movementHWIndicator ! InformMeOnReaching(
                                 this.currentFloorID,
                                 this.pendingPassengerRequests.head)
       goto(Moving)
     }
 }

Whenever pendingPassengerRequests is empty, the carriage is in the Stopped or Ready state and is stationary at the last floor it has reached.  

Simulation of consumption of time while Moving

When the carriage is moving, it must expend time doing that. Moreover, while it is expending time, asynchronous events are possible to arrive (from the Controller or the Button Panel: both actors in their own rights). Because movementHWIndicator is an Actor, it is easy to model the asynchronicity by generating its signal using a Scheduler, after a delay.
Because the Scheduler runs on ActorSystem’s scheduler thread, it becomes messy when we want to test behaviour of LiftCarriageWithMovingState together with a MovingStateSimulator. I had to mock the behaviour of MovingStateSimulator. However, Instead of mocking it using a framework, I have decided to use a do-nothing Actor to construct a LiftCarriageWithMovingState. Its behaviour is much like that of an echo Actor:

trait MovingStateSimulator extends Actor with ActorLogging {

 case class SpentTimeToReach(nextStop: NextStop, carriageToBeInformed: ActorRef)

 val timeToReachNextFloor: FiniteDuration = 1000 millis  // up  or down, same time taken
 def simulateMovementTo(
      fromFloorID: Int,
      nextStop: NextStop): Unit

 override def receive: Receive = {
   case InformMeOnReaching(fromFloorID,nextStop) =>
     if (fromFloorID == nextStop.floorID) // Just a regular edge-case check
       sender ! ReachedFloor(nextStop)
     else
       simulateMovementTo(fromFloorID,nextStop)

   case SpentTimeToReach(nextStop,carriageToBeInformed)   =>
     log.debug(s"Informing ${carriageToBeInformed} after reaching the floor.")
     carriageToBeInformed ! ReachedFloor(nextStop)
 }
}

object DefaultMovingStateSimulatorActor extends MovingStateSimulator
 with ActorLogging {
 override def simulateMovementTo(
                fromFloorID: Int,
                toNextStop: NextStop) = {
   // Immediate response, no time consumption in moving
   sender ! ReachedFloor(toNextStop)
 }
}

Finally, we have a  Controller and a InlaidButtonPanel to complete the story:
class InlaidButtonPanel (
        panelID: Int, attachedToCarriage: ActorRef, val noOfFloors: Int = 10)   
extends Actor
with ActorLogging{ // ..
}

class LiftController (carriages: Vector[ActorRef]) extends Actor
 with LoggingFSM[LiftState,LiftData]
 with ActorLogging { // ..
}

Remember that a Carriage receives the instruction to pick a passenger from the Controller and to drop a passenger, from the button panel.

The following is a sample output log of a hotel having two lifts (enumerated 0 and 1) available. The Driver code emulates a instruction sequence of
A passenger presses a button at the lobby of floor 2
A passenger presses a button at the lobby of floor 4
Controller checks with Carriage(1), at which floor it is now
// Carriages reach floor 2 and 4 after some time...
Passenger inside Carriage[0]wants to go to floor 5, presses button
2 Passengers inside Carriage[1]; one wants to go to 6 and the other, to 2; press buttons
Controller checks with Carriage(0), at which floor it is now
Controller checks with Carriage(1), at which floor it is now
Controller checks with Carriage(0), at which floor it is now
Controller checks with Carriage(1), at which floor it is now

The output (pruned for space and readability):
Selection_195.png
The latest code is here.

While writing this blog, I have read a number of blogs on the same / similar topic. Some of them which I enjoyed reading were:



No comments:

Post a Comment