buildbox-2.2.1.2: BuildBox/Data/Schedule.hs
{-# LANGUAGE ScopedTypeVariables, PatternGuards #-}
{-# OPTIONS -fno-warn-orphans #-}
-- | A schedule of commands that should be run at a certain time.
module BuildBox.Data.Schedule
(
-- * Time Periods
second, minute, hour, day
-- * When
, When (..)
, WhenModifier (..)
-- * Events
, EventName
, Event (..)
, earliestEventToStartAt
, eventCouldStartAt
-- * Schedules
, Schedule (..)
, makeSchedule
, lookupEventOfSchedule
, lookupCommandOfSchedule
, adjustEventOfSchedule
, eventsOfSchedule)
where
import Data.Time
import Data.List
import Data.Function
import Data.Maybe
import Control.Monad
import Data.Map (Map)
import qualified Data.Map as Map
instance Read NominalDiffTime where
readsPrec n str
= let [(secs :: Double, rest)] = readsPrec n str
in case rest of
's' : rest' -> [(fromRational $ toRational secs, rest')]
_ -> []
second, minute, hour, day :: NominalDiffTime
second = 1
minute = 60
hour = 60 * minute
day = 24 * hour
-- When -------------------------------------------------------------------------------------------
-- | When to invoke some event.
data When
-- | Just keep doing it.
= Always
-- | Don't do it, ever.
| Never
-- | Do it some time after we last started it.
| Every NominalDiffTime
-- | Do it some time after it last finished.
| After NominalDiffTime
-- | Do it each day at this time. The ''days'' are UTC days, not local ones.
| Daily TimeOfDay
deriving (Read, Show, Eq)
-- | Modifier to when.
data WhenModifier
-- | If the event hasn't been invoked before then do it immediately
-- when we start the cron process.
= Immediate
-- | Wait until after this time before doing it first.
| WaitUntil UTCTime
deriving (Read, Show, Eq)
-- Event ------------------------------------------------------------------------------------------
type EventName = String
-- | Records when an event should start, and when it last ran.
data Event
= Event
{ -- | A unique name for this event.
-- Used when writing the schedule to a file.
eventName :: EventName
-- | When to run the command.
, eventWhen :: When
-- | Modifier to the previous.
, eventWhenModifier :: Maybe WhenModifier
-- | When the event was last started, if any.
, eventLastStarted :: Maybe UTCTime
-- | When the event last finished, if any.
, eventLastEnded :: Maybe UTCTime }
deriving (Read, Show, Eq)
-- | Given the current time and a list of events, determine which one should be started now.
-- If several events are avaliable then take the one with the earliest start time.
earliestEventToStartAt :: UTCTime -> [Event] -> Maybe Event
earliestEventToStartAt curTime events
= let eventsStartable = filter (eventCouldStartAt curTime) events
eventsSorted = sortBy (compare `on` eventLastStarted) eventsStartable
in listToMaybe eventsSorted
-- | Given the current time, decide whether an event could be started.
-- If the `WhenModifier` is `Immediate` this always returns true.
-- The `SkipFirst` modifier is ignored, as this is handled separately.
eventCouldStartAt :: UTCTime -> Event -> Bool
eventCouldStartAt curTime event
-- If the event has never started or ended, and is marked as immediate,
-- then start it right away.
| Nothing <- eventLastStarted event
, Nothing <- eventLastEnded event
, Just Immediate <- eventWhenModifier event
= True
-- If the current end time is before the start time, then the most
-- recent iteration is still running, so don't start it again.
| Just lastStarted <- eventLastStarted event
, Just lastEnded <- eventLastEnded event
, lastEnded < lastStarted
= False
-- Keep waiting if there's a seconday wait modifier.
| Just (WaitUntil waitTime) <- eventWhenModifier event
, curTime < waitTime
= False
-- Otherwise we have to look at the real schedule.
| otherwise
= case eventWhen event of
Always -> True
Never -> False
Every diffTime
-> maybe True
(\lastTime -> (curTime `diffUTCTime` lastTime ) > diffTime)
(eventLastStarted event)
After diffTime
-> maybe True
(\lastTime -> (curTime `diffUTCTime` lastTime ) > diffTime)
(eventLastEnded event)
Daily timeOfDay
-- If it's been less than a day since we last started it, then don't do it yet.
| Just lastStarted <- eventLastStarted event
, (curTime `diffUTCTime` lastStarted) < day
-> False
| otherwise
-> let -- If we were going to run it today, this is when it would be.
startTimeToday
= curTime
{ utctDayTime = timeOfDayToTime timeOfDay }
-- If it's after that time then quit fooling around..
in curTime > startTimeToday
-- Schedule ---------------------------------------------------------------------------------------
-- | Map of event names to their details and build commands.
data Schedule cmd
= Schedule (Map EventName (Event, cmd))
-- | Get the list of events in a schedule, ignoring the build commands.
eventsOfSchedule :: Schedule cmd -> [Event]
eventsOfSchedule (Schedule sched)
= map fst $ Map.elems sched
-- | A nice way to produce a schedule.
makeSchedule :: [(EventName, When, Maybe WhenModifier, cmd)] -> Schedule cmd
makeSchedule tuples
= let makeSched (name, whn, mMod, cmd)
= (name, (Event name whn mMod Nothing Nothing, cmd))
in Schedule $ Map.fromList $ map makeSched tuples
-- | Given an event name, lookup the associated event from a schedule.
lookupEventOfSchedule :: EventName -> Schedule cmd -> Maybe Event
lookupEventOfSchedule name (Schedule sched)
= liftM fst $ Map.lookup name sched
-- | Given an event name, lookup the associated build command from a schedule.
lookupCommandOfSchedule :: EventName -> Schedule cmd -> Maybe cmd
lookupCommandOfSchedule name (Schedule sched)
= liftM snd $ Map.lookup name sched
-- | Given a new version of an event, update any matching event in the schedule.
-- If the event not already there then return the original schedule.
adjustEventOfSchedule :: Event -> Schedule cmd -> Schedule cmd
adjustEventOfSchedule event (Schedule sched)
= Schedule
$ Map.adjust
(\(_, build) -> (event, build))
(eventName event)
sched