-
Notifications
You must be signed in to change notification settings - Fork 36
Open
Description
The stm package has the rather nice registerDelay
API.
registerDelay :: Int -> IO (TVar Bool)
Set the value of returned TVar to True after a given number of microseconds. The caveats associated with threadDelay also apply.
This is nice, but we could provide a more extensive timer API, based on the underlying GHC timer API. The really nice thing about registerDelay
is that being based on STM it is composable. We just need a bit more for use cases like network protocols where you need to be able to push back a timeout. Cancelling is also useful. The GHC timer API can do all these things.
I would like to suggest and get feedback on the following API and implementation. If we go for it, it might be best to add to a new module Control.Concurrent.STM.Timer
. I can make a PR based on feedback.
API:
data TimerState = TimerPending | TimerFired | TimerCancelled
data Timer
type Microseconds = Int
-- | Create a new timer which will fire at the given time duration in
-- the future.
--
-- The timer will start in the 'TimerPending' state and either
-- fire at or after the given time leaving it in the 'TimerFired' state,
-- or it may be cancelled with 'cancelTimer', leaving it in the
-- 'TimerCancelled' state.
--
-- Timers /cannot/ be reset to the pending state once fired or cancelled
-- (as this would be very racy). You should create a new timer if you need
-- this functionality.
--
newTimer :: Microseconds -> IO Timer
-- | Read the current state of a timer. This does not block, but returns
-- the current state. It is your responsibility to use 'retry' to wait.
--
-- Alternatively you may wish to use the convenience utility 'awaitTimer'
-- to wait for just the fired or cancelled outcomes.
--
-- You should consider the cancelled state if you plan to use 'cancelTimer'.
--
readTimer :: Timer -> STM TimerState
-- Adjust when this timer will fire, to the given duration into the future.
--
-- It is safe to race this concurrently against the timer firing. It will
-- have no effect if the timer fires first.
--
-- The new time can be before or after the original expiry time, though
-- arguably it is an application design flaw to move timers sooner.
--
updateTimer :: Timer -> Microseconds -> STM ()
-- | Cancel a timer (unless it has already fired), putting it into the
-- 'TimerCancelled' state. Code reading and acting on the timer state
-- need to handle such cancellation appropriately.
--
-- It is safe to race this concurrently against the timer firing. It will
-- have no effect if the timer fires first.
--
cancelTimer :: Timer -> m ()
And implementation in terms of the GHC timeout manager (which is what registerDelay
uses)
data Timer = Timer !(STM.TVar TimerState) !GHC.TimerKey
readTimer (Timer var _key) = STM.readTVar var
newTimer = \usec -> do
var <- STM.newTVarIO TimerPending
mgr <- GHC.getSystemTimerManager
key <- GHC.registerTimeout mgr usec (STM.atomically (timerAction var))
return (Timer var key)
where
timerAction var = do
x <- STM.readTVar var
case x of
TimerPending -> STM.writeTVar var TimerFired
TimerFired -> error "MonadTimer(IO): invariant violation"
TimerCancelled -> return ()
-- In GHC's TimerManager this has no effect if the timer already fired.
-- It is safe to race against the timer firing.
updateTimer (Timer _var key) usec = do
mgr <- GHC.getSystemTimerManager
GHC.updateTimer mgr key usec
cancelTimer (Timer var key) = do
STM.atomically $ do
x <- STM.readTVar var
case x of
TimerPending -> STM.writeTVar var TimerCancelled
TimerFired -> return ()
TimerCancelled -> return ()
mgr <- GHC.getSystemTimerManager
GHC.unregisterTimeout mgr key
Plus one handy derived utility
-- | Returns @True@ when the timer is fired, or @False@ if it is cancelled.
awaitTimer :: Timer -> STM Bool
awaitTimer t = do
s <- readTimer t
case s of
TimerPending -> retry
TimerFired -> return True
TimerCancelled -> return False
theobat
Metadata
Metadata
Assignees
Labels
No labels