Copyright | (c) 2016, Drew Hess |
---|---|
License | BSD3 |
Maintainer | Drew Hess <src@drewhess.com> |
Stability | experimental |
Portability | non-portable |
Safe Haskell | Safe |
Language | Haskell2010 |
The mellon-core
state machine is the heart of the locking protocol.
A user of the mellon-core
package is not expected to interact
directly with the state machine, as the state machine is pure and is
not capable of setting timers or performing IO
actions on physical
access devices. In mellon-core
, those operations are the
responsibility of controllers, and controllers are what users should
interact with; see the Mellon.Controller module. However,
understanding the state machine model is useful for understanding the
behavior of a mellon-core
application.
The state machine's behavior is quite simple:
- The locked state has indefinite duration.
- The unlocked state has an expiration date (a
UTCTime
). The controller will inform the state machine when this date has passed (since the state machine cannot keep time), at which point the state machine will advise the controller to lock the device again. - The user can (via the controller) send a lock command at any time, which will immediately cancel any unlock currently in effect.
- If the user (via the controller) sends an unlock command while a previous unlock command is still in effect, then the unlock with the later expiration date "wins"; i.e., if the new expiration date is later than the current one, the unlock period is effectively extended, otherwise the device remains unlocked until the previously-specified date.
The state machine types
The state machine's inputs, i.e., commands sent to the machine by a controller, either in response to a user's command, or in response to an expired timer.
InputLockNow | Lock immediately, canceling any unlock currently in effect |
InputUnlockExpired !UTCTime | An unlock command has expired. The unlock's expiration date
is given by the specified |
InputUnlock !UTCTime | Unlock until the specified time. If no existing unlock command with a later expiration is currently in effect when this command is executed, the controller managing the state machine must schedule a lock to run at the specified time (i.e., when the unlock expires). |
The state machine's outputs, i.e., commands to be performed by a controller.
It's reasonable to wonder why the OutputUnlock
and
OutputRescheduleLock
values take a UTCTime
parameter, when the
State
they're both always associated with (StateUnlocked
) also
takes a UTCTime
parameter. Indeed, their time values will always
be the same. However, this redundancy permits an interface to the
state machine where the state is implicit (e.g., in a state monad)
and the controller only "sees" the Output
.
OutputLock | Lock the device now |
OutputUnlock !UTCTime | Unlock the device now and schedule a lock to run at the given time |
OutputRescheduleLock !UTCTime | The date for the currently scheduled lock has changed. Reschedule it for the specified date. Note that the new date is guaranteed to be later than the previously-scheduled time. |
OutputCancelLock | Cancel the currently scheduled lock and lock the device now |
The state machine's states.
StateLocked | The state machine is in the locked state |
StateUnlocked !UTCTime | The state machine is unlocked until the specified date. |
The state machine implementation
transition :: State -> Input -> (Maybe Output, State) Source #
Run one iteration of the state machine.
Note that some transitions require no action by the controller,
hence the first element of the returned pair (the Output
value)
is wrapped in Maybe
.
Properties
transition StateLocked InputLockNow == (Nothing,StateLocked)
\date -> transition StateLocked (InputUnlockExpired date) == (Nothing,StateLocked)
\date -> transition StateLocked (InputUnlock date) == (Just $ OutputUnlock date,StateUnlocked date)
\date -> transition (StateUnlocked date) InputLockNow == (Just OutputCancelLock,StateLocked)
\date -> transition (StateUnlocked date) (InputUnlockExpired date) == (Just OutputLock,StateLocked)
\(date1, date2) -> date1 /= date2 ==> transition (StateUnlocked date1) (InputUnlockExpired date2) == (Nothing,StateUnlocked date1)
\date -> transition (StateUnlocked date) (InputUnlock date) == (Nothing,StateUnlocked date)
\(date1, date2) -> date2 > date1 ==> transition (StateUnlocked date1) (InputUnlock date2) == (Just $ OutputRescheduleLock date2,StateUnlocked date2)
\(date1, date2) -> not (date2 > date1) ==> transition (StateUnlocked date1) (InputUnlock date2) == (Nothing,StateUnlocked date1)