Packages

An example open source Elixir application.

Current section

Files

Jump to
elevator lib orderHandler.ex
Raw

lib/orderHandler.ex

defmodule OrderHandler do
@moduledoc """
A module for keeping track of the orders being handled by a specific elevator.
"""
# order is on the form {floor, orderType, direction, handledBy}
use GenServer, restart: :permanent
require Logger
# Public functions
# --------------------------------------------
def init(orderQueue) do
{:ok, orderQueue}
end
@doc "Requests backup from the `BackupHandler`s and starts the `OrderHandler` in a supervision tree, see `Supervisor`."
def start_link([]) do
ordersFromBackup = extractBackup()
unless ordersFromBackup == [], do: Fsm.wakeUp()
GenServer.start_link(__MODULE__, ordersFromBackup, name: __MODULE__)
end
# API
# --------------------------------------------
@doc "Asks the `OrderHandler` if there are orders on the given floor."
def ordersOnFloor?(floor, travelDirection),
do: GenServer.call(__MODULE__, {:floorOrdersCheck, floor, travelDirection})
@doc "Signals to the `OrderHandler` that a floor is finished, triggering the deletion of all orders on that floor."
def finishedOrdersOnFloor(floor),
do: GenServer.cast(__MODULE__, {:finishedOrdersOnFloor, floor})
@doc "Asks the `OrderHandler` which floor the next order is on. Returns :tnil if the order queue is empty."
def floorForNextOrder?, do: GenServer.call({__MODULE__, node()}, :floorForNextOrder)
@doc "Asks the `OrderHandler` to add the order to the order queue."
def addOrder(order, node), do: GenServer.abcast([node], __MODULE__, {:addOrder, order})
@doc "Triggers all the `OrderHandler`s to retrieve backup from the `BackupHandler`s."
def multiTriggerRetrieveBackup(), do: GenServer.abcast(__MODULE__, :retrieveBackup)
@doc "Requests the order queue of all the `OrderHandler`s."
def getOrderQueues(nodes \\ [node() | Node.list()]),
do: GenServer.multi_call(nodes, __MODULE__, :getOrderQueue)
# Calls/Casts
# --------------------------------------------
# Handles the OrderHandler being asked if there are orders to complete on a given floor, returns a boolean.
def handle_call({:floorOrdersCheck, _floor, _direction}, _from, []), do: {:reply, false, []}
def handle_call({:floorOrdersCheck, floor, direction}, _from, orderQueue) do
compatibleOrderOnFloor? =
Enum.any?(orderQueue, fn {orderFloor, orderType, _handledBy} ->
{orderFloor, orderType} in [
{floor, :cab},
{floor, direction}
]
end)
# If the next order is on the given floor, we want to take it even if it is incompatible direction-wise.
[{nextFloor, _, _} | _remainingOrderQueue] = Enum.reverse(orderQueue)
nextOrderOnFloor? = floor == nextFloor
{:reply, compatibleOrderOnFloor? || nextOrderOnFloor?, orderQueue}
end
# Handles the OrderHandler being asked for the floor of the next order. Returns nil if there are no more orders.
def handle_call(:floorForNextOrder, _from, []), do: {:reply, nil, []}
def handle_call(:floorForNextOrder, _from, orderQueue) do
[{floorForNextOrder, _buttonType, _handledBy} | _tail] = Enum.reverse(orderQueue)
{:reply, floorForNextOrder, orderQueue}
end
def handle_call(:getOrderQueue, _from, orderQueue), do: {:reply, orderQueue, orderQueue}
# Handles the OrderHandler being asked to be responsible for a new order.
# This is safe to do in a cast as all lights and backup are done as a result of the cast, not by the method doing the cast.
def handle_cast({:addOrder, order}, orderQueue) do
responseBackupHandler = BackupHandler.backupOrder(order)
backupCompleted? = serverCallSuccessful?(responseBackupHandler)
orderWatchCompleted? =
if backupCompleted? do
responseOrderWatchdog = OrderWatchdog.addOrder(order)
serverCallSuccessful?(responseOrderWatchdog)
end
if backupCompleted? && orderWatchCompleted? do
{floor, buttonType, _handledBy} = order
case buttonType do
:cab -> Driver.set_order_button_light(buttonType, floor, :on)
_ -> Driver.set_order_button_light([node() | Node.list()], buttonType, floor, :on)
end
if orderQueue == [], do: Fsm.wakeUp()
{:noreply, [order | orderQueue]}
else
{:noreply, orderQueue}
end
end
# Handles the OrderHandler being informed that the orders on a given floor were completed.
def handle_cast({:finishedOrdersOnFloor, floor}, orderQueue) do
remainingOrderQueue =
orderQueue
|> Enum.reject(fn {orderFloor, _orderType, _orderHandledBy} -> orderFloor == floor end)
BackupHandler.floorFinished(floor)
OrderWatchdog.floorFinished(floor)
clearFloorLights(floor)
{:noreply, remainingOrderQueue}
end
# Handles the OrderHandler being asked to retrieve backup of orders.
def handle_cast(:retrieveBackup, orderQueue) do
ordersFromBackup = extractBackup()
# Concatenate the queues without duplicates, making sure the last element of duplicates (the non-backup one) is being kept.
fullOrderQueue =
[ordersFromBackup, orderQueue]
|> Enum.concat()
|> Enum.reverse()
|> Enum.uniq()
|> Enum.reverse()
if orderQueue == [] && fullOrderQueue != [], do: Fsm.wakeUp()
{:noreply, fullOrderQueue}
end
# Private functions
# --------------------------------------------
# Takes the response from a server multicall (for instance a backup request or orderWatchdog request) and checks if at least one actor responded affirmatively
defp serverCallSuccessful?({replies, _bad_nodes}) do
replies
|> Enum.reject(fn {_elevatorID, repl} -> repl == :error end)
|> Enum.any?()
end
# Requests backup from the BackupHandler, and extracts from the general backup the parts pertaining to the local elevator.
defp extractBackup() do
BackupHandler.requestBackup()
|> Enum.filter(fn {_floor, _type, handledBy} -> handledBy == node() end)
end
# Convenience function. Clears hall lights everywhere, clears local cab light.
defp clearFloorLights(floor) do
Enum.map([:up, :down], fn type ->
Driver.set_order_button_light([node() | Node.list()], type, floor, :off)
end)
Driver.set_order_button_light(:cab, floor, :off)
:ok
end
end