Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
dcoutts opened this issue Dec 1, 2018 · 3 comments
Open

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

dcoutts opened this issue Dec 1, 2018 · 3 comments

Comments

@dcoutts
Copy link
Contributor

dcoutts commented Dec 1, 2018

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
Copy link
Contributor Author

dcoutts commented Dec 1, 2018

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

@simonmar
Copy link
Member

simonmar commented Dec 5, 2018

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
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants