packages feed

control-timeout (empty) → 0.1

raw patch · 5 files changed

+294/−0 lines, 5 filesdep +basedep +containersdep +stmbuild-type:Customsetup-changed

Dependencies added: base, containers, stm, time

Files

+ Control/Timeout.hs view
@@ -0,0 +1,197 @@+-- |+-- This module handles timeouts by using a (single) thread sitting in threadDelay+-- and the STM. One can request an IO action be performed after some number of+-- seconds and later cancel that request if need be.+--+-- The number of threads used is constant.+module Control.Timeout (+     addTimeout+   , cancelTimeout+   , TimeoutTag) where++import Control.Concurrent+import Control.Concurrent.STM+import Control.Monad++import System.IO.Unsafe++import qualified Data.Map as Map+import Data.Time.Clock.POSIX++-- | This is set, atomically, to true when the manager thread is started.+--   This thread isn't started unless someone actually creates a timeout+managerThreadStarted :: TVar Bool+managerThreadStarted = unsafePerformIO $ newTVarIO False++-- | When a timeout thread times out, it compares the first element of this+--  tuple against the value that it was created with. If they don't match then+--  it's no longer the current timeout thread and it exits. Otherwise, it sets+--  the second element to true and exits.+signal :: TVar (Int, Bool)+signal = unsafePerformIO $ newTVarIO (0, False)++-- | This is a map of all the timeouts. It maps the absolute time+--   that the timeout expires at to a list of tagged actions+--   to perform at that time. For a given value in the map, the Ints of+--   every element in the list must be unique.+timeouts :: TVar (Map.Map POSIXTime [(Int, IO ())])+timeouts = unsafePerformIO $ newTVarIO Map.empty++-- Here's how everything works. The addTimeout and cancelTimeout functions+-- alter the above globals using the STM. The first call to addTimeout will+-- start a manager thread which watches @signal@ and @timeouts@ for changes:+-- either the timeoutThread has completed or someone has added/removed the+-- least element of timeouts.+--+-- In the first case, @timeouts@ is updated by removing all the expired+-- timeouts and their actions are performed. In both cases, the time to the+-- next timeout is calculated and a timeoutThread is forked to sleep for that+-- length of time before signaling via @signal@.+--+-- Timeouts are identified by their absolute time value and the unique tag+-- number for their action at that time. When creating a timeout, that pair+-- is wrapped in the (opaque) TimeoutTag type and returned. When canceling a+-- timeout, the list of actions for the given absolute time is filtered to+-- remove the indicated action. Because of this, the tags for a given absolute+-- time must be unique. This is achieved by giving the first element a tag of 0+-- and giving subsequent elements a tag one greater than the current max.+--+-- Each timeoutThread is given a tag (of a different type to the tags in the+-- last paragraph) so that @signal@ is never set by an old timeoutThread+-- which hasn't died yet.++-- | Get the list of expired timers from a map of timeouts+expiredTimers :: POSIXTime  -- ^ the current time+              -> Map.Map POSIXTime a  -- ^ the timeouts map+              -> ([a], Map.Map POSIXTime a)  -- ^ the list of actions and a new map+expiredTimers curtime m = do+  unfoldrWithValue f m where+  f m =+    case Map.minViewWithKey m of+         Nothing -> Nothing+         Just ((time, action), m') ->+           if time <= curtime+              then Just (action, m')+              else Nothing++-- | Run the actions for all expired timers in the @timeouts@ global. Update+--   that global with a new Map, less the expired timeouts.+runExpiredTimers :: (Monad m)+                 => POSIXTime  -- ^ the current time+                 -> TVar (Map.Map POSIXTime [(Int, m ())])  -- ^ the timeouts map+                 -> STM (m ())+runExpiredTimers currentTime tm = do+  m <- readTVar tm+  let (actions, m') = expiredTimers currentTime m+  when (length actions > 0) $ writeTVar tm m'+  return $ (do sequence $ map (sequence . map snd) actions; return ())++-- | A version of unfoldr which returns the final value as well. Note that+--   the resulting list comes off in reverse order+unfoldrWithValue :: (b -> Maybe (a, b)) -> b -> ([a], b)+unfoldrWithValue f i = inner f i [] where+  inner f i acc =+    case f i of+       Nothing -> (acc, i)+       Just (v, i') -> inner f i' $ v : acc++-- | This is a thread which waits for the given number of milliseconds+--   and tries to set the snd element of the global @signal@ to true, iff+--   the first element of that global is equal to its tag number.+timeoutThread :: Int  -- ^ the id of this timeout, see @signal@+              -> POSIXTime  -- ^ the time to wakeup+              -> IO ()+timeoutThread id targetTime = do+  currentTime <- getPOSIXTime+  let deltausecs = truncate $ fromRational $ toRational ((currentTime - targetTime) * 1000000)+  when (deltausecs > 0) $ threadDelay deltausecs+  atomically (do+    (id', _) <- readTVar signal+    when (id' == id) $ writeTVar signal (id, True))++-- | This is an opaque type of timeouts. A value of this type is returned+--   when creating a timeout and can be used to cancel the same timeout.+newtype TimeoutTag = TimeoutTag (POSIXTime, Int)++-- | Add an action to be performed at some point in the future. The action will+--   occur inside a thread which is dedicated to performing them so it should+--   run quickly and certainly should not block on IO etc.+addTimeout :: Float  -- ^ the number of seconds in the future to perform the action+           -> (IO ())  -- ^ the action to perform+           -> IO TimeoutTag+addTimeout delta action = do+  currentTime <- getPOSIXTime+  let future = currentTime + (fromRational $ toRational delta)+  tag <- atomically (do+    m <- readTVar timeouts+    case Map.lookup future m of+         Nothing -> do writeTVar timeouts $ Map.insert future [(0, action)] m+                       return $ TimeoutTag (future, 0)+         Just xs -> do let magic = 1 + (maximum $ map fst xs)+                       writeTVar timeouts $ Map.insert future ((magic, action) : xs) m+                       return $ TimeoutTag (future, magic))+  -- If the manager thread isn't running, start it now.+  startp <- atomically (do+    started <- readTVar managerThreadStarted+    when (not started) $ writeTVar managerThreadStarted True+    return $ not started)+  when startp $ forkIO (timeoutManagerThread timeouts signal 0 Nothing) >> return ()++  return tag++-- | Remove a timeout. This function never fails, but will return False if the+--   given timeout couldn't be found. This may be because cancelTimeout has+--   already been called with this tag, or because the timeout has already+--   fired. Note that, since timeouts are IO actions, they don't run atomically.+--   Thus it's possible that this call returns False and that the timeout is+--   currently in the process of running.+--+--   Note that one should never call cancelTimeout twice with the same tag since+--   it's possible that the tag will be reused and thus the second call could+--   cancel a different timeout.+cancelTimeout :: TimeoutTag  -- ^ the tag returned by addTimeout+              -> STM Bool  -- ^ returns False if the timeout didn't exist+cancelTimeout (TimeoutTag (future, n)) = do+  m <- readTVar timeouts+  case Map.lookup future m of+       Nothing -> return False+       Just xs -> do+         let xs' = filter (\(t, _) -> t /= n) xs+             m' = Map.insert future xs' m+         writeTVar timeouts m'+         return $ length xs' /= length xs++timeoutManagerThread :: TVar (Map.Map POSIXTime [(Int, IO ())])+                     -> TVar (Int, Bool)+                     -> POSIXTime  -- ^ the current minimum time+                     -> Maybe ThreadId  -- ^ the id of the current timeoutThread+                     -> IO ()+timeoutManagerThread tm signal currentMin mthid = do+  -- the event is either a signal from the timeoutThread (True) or that a+  -- timeout has been added to the timeouts map (False)+  (event, currentTag) <- atomically (do+    s <- readTVar signal+    let currentTag = fst s+    if snd s+       then return (True, currentTag)+       else (do+         m <- readTVar tm+         if (Map.null m && currentMin /= 0) ||+             (not (Map.null m) && fst (Map.findMin m) /= currentMin)+            then return (False, currentTag)+            else retry))++  currentTime <- getPOSIXTime+  when event $ atomically (runExpiredTimers currentTime tm) >>= id+  minTimeout <- atomically (do+    m <- readTVar timeouts+    if Map.null m+       then return 0+       else return $ fst $ Map.findMin m)+  let nextTag = currentTag + 1+  atomically $ writeTVar signal (nextTag, False)+  case mthid of+       Nothing -> return ()+       Just x -> killThread x+  tid <- forkIO $ timeoutThread nextTag minTimeout+  timeoutManagerThread tm signal minTimeout $ Just tid
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) Adam Langley++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++1. Redistributions of source code must retain the above copyright+   notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+   notice, this list of conditions and the following disclaimer in the+   documentation and/or other materials provided with the distribution.++3. Neither the name of the author nor the names of his contributors+   may be used to endorse or promote products derived from this software+   without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ Setup.lhs view
@@ -0,0 +1,3 @@+#!/usr/bin/env runhaskell+> import Distribution.Simple+> main = defaultMain
+ control-timeout.cabal view
@@ -0,0 +1,14 @@+name:            control-timeout+version:         0.1+license:         BSD3+license-file:    LICENSE+author:          Adam Langley <agl@imperialviolet.org>+description:     This package provides functions for running timeouts+synopsis:        Timeout handling+category:        Control+build-depends:   base, containers, time, stm+stability:       provisional+tested-with:     GHC == 6.8.1+exposed-modules: Control.Timeout+ghc-options:     -O2+extra-source-files: test/TimeoutTest.hs
+ test/TimeoutTest.hs view
@@ -0,0 +1,50 @@+module Main where++import Control.Concurrent+import Control.Concurrent.STM++import Data.Time.Clock.POSIX+import Control.Timeout++import Text.Printf (printf)++singleTimeout secs = do+  a <- atomically $ newTVar 0+  startTime <- getPOSIXTime+  addTimeout secs (getPOSIXTime >>= atomically . writeTVar a)+  endTime <- atomically (do+    t <- readTVar a+    if t == 0+       then retry+       else return t)+  printf "%dms sleep took: %s\n" ((truncate $ secs * 1000) :: Int) (show (endTime - startTime))++multiTimeout :: Int -> Float -> IO ()+multiTimeout n secs = do+  a <- atomically $ newTVar n+  startTime <- getPOSIXTime+  mapM_ (const $ addTimeout secs (atomically $ readTVar a >>= writeTVar a . ((flip (-)) 1))) [1..n]+  atomically (do+    n' <- readTVar a+    if n' == 0+       then return ()+       else retry)+  endTime <- getPOSIXTime+  printf "%d timeouts for %d ms took %s\n" n ((truncate $ secs * 1000) :: Int) (show $ endTime - startTime)++setAndCancelTimeout :: Int -> Float -> IO ()+setAndCancelTimeout n secs = do+  a <- atomically $ newTVar (0 :: Int)+  startTime <- getPOSIXTime+  tags <- mapM (const $ addTimeout secs (atomically $ readTVar a >>= writeTVar a . ((+) 1))) [1..n]+  mapM (atomically . cancelTimeout) tags+  endTime <- getPOSIXTime+  expired <- atomically $ readTVar a++  printf "Setting and killing %d timeouts for %d ms took %s and %d expired\n" n ((truncate $ secs * 1000) :: Int) (show $ endTime - startTime) expired++main = do+  sequence [singleTimeout 0, singleTimeout 0.1, singleTimeout 0.5, singleTimeout 1.0]+  sequence [multiTimeout 10 0.1, multiTimeout 100 0.1, multiTimeout 1000 0.1, multiTimeout 10000 0.1, multiTimeout 100000 0.1]+  sequence [setAndCancelTimeout 10 0.1, setAndCancelTimeout 100 0.1, setAndCancelTimeout 1000 0.1, setAndCancelTimeout 10000 0.1, setAndCancelTimeout 100000 0.1]+  return ()