haskell / stm

Software Transactional Memory
Other
97 stars 34 forks source link

Extended Timer API. Feedback requested on a proposal. #19

Open dcoutts opened 5 years ago

dcoutts commented 5 years ago

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.

https://hackage.haskell.org/package/stm-2.5.0.0/docs/Control-Concurrent-STM-TVar.html#v:registerDelay

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
dcoutts commented 5 years ago

"timer" or "timeout"? Names names names.

simonmar commented 5 years ago

In general I like it. However I think updateTimer needs to be IO, not STM, right? There's no way to perform a transaction involving updateTimer because the underlying API is IO. Similarly cancelTimer. Does that make the API less useful? You can't do a test-and-update or a test-and-cancel in the same transaction.

mitchellwrosen commented 5 years ago

Would it be possible for updateTimer and cancelTimer to return whether or not they worked?