snap-server-0.8.0: src/Snap/Internal/Http/Server/TimeoutManager.hs
{-# LANGUAGE BangPatterns #-}
module Snap.Internal.Http.Server.TimeoutManager
( TimeoutManager
, TimeoutHandle
, initialize
, stop
, register
, tickle
, set
, modify
, cancel
) where
------------------------------------------------------------------------------
import Control.Concurrent
import Control.Exception
import Control.Monad
import Data.IORef
import Foreign.C.Types
------------------------------------------------------------------------------
data State = Deadline !CTime
| Canceled
deriving (Eq, Show)
------------------------------------------------------------------------------
instance Ord State where
compare Canceled Canceled = EQ
compare Canceled _ = LT
compare _ Canceled = GT
compare (Deadline a) (Deadline b) = compare a b
------------------------------------------------------------------------------
-- Probably breaks Num laws, but I can live with it
--
instance Num State where
--------------------------------------------------------------------------
Canceled + Canceled = Canceled
Canceled + x = x
x + Canceled = x
(Deadline a) + (Deadline b) = Deadline $! a + b
--------------------------------------------------------------------------
Canceled - Canceled = Canceled
Canceled - x = negate x
x - Canceled = x
(Deadline a) - (Deadline b) = Deadline $! a - b
--------------------------------------------------------------------------
Canceled * _ = Canceled
_ * Canceled = Canceled
(Deadline a) * (Deadline b) = Deadline $! a * b
--------------------------------------------------------------------------
negate Canceled = Canceled
negate (Deadline d) = Deadline (negate d)
--------------------------------------------------------------------------
abs Canceled = Canceled
abs (Deadline d) = Deadline (abs d)
--------------------------------------------------------------------------
signum Canceled = Canceled
signum (Deadline d) = Deadline (signum d)
--------------------------------------------------------------------------
fromInteger = Deadline . fromInteger
------------------------------------------------------------------------------
data TimeoutHandle = TimeoutHandle {
_killAction :: !(IO ())
, _state :: !(IORef State)
, _hGetTime :: !(IO CTime)
}
------------------------------------------------------------------------------
-- | Given a 'State' value and the current time, apply the given modification
-- function to the amount of time remaining.
--
smap :: CTime -> (Int -> Int) -> State -> State
smap _ _ Canceled = Canceled
smap now f (Deadline t) = Deadline t'
where
!remaining = fromEnum $ max 0 (t - now)
!newremaining = f remaining
!t' = now + toEnum newremaining
------------------------------------------------------------------------------
data TimeoutManager = TimeoutManager {
_defaultTimeout :: !Int
, _getTime :: !(IO CTime)
, _connections :: !(IORef [TimeoutHandle])
, _inactivity :: !(IORef Bool)
, _morePlease :: !(MVar ())
, _managerThread :: !(MVar ThreadId)
}
------------------------------------------------------------------------------
-- | Create a new TimeoutManager.
initialize :: Int -- ^ default timeout
-> IO CTime -- ^ function to get current time
-> IO TimeoutManager
initialize defaultTimeout getTime = do
conns <- newIORef []
inact <- newIORef False
mp <- newEmptyMVar
mthr <- newEmptyMVar
let tm = TimeoutManager defaultTimeout getTime conns inact mp mthr
thr <- forkIO $ managerThread tm
putMVar mthr thr
return tm
------------------------------------------------------------------------------
-- | Stop a TimeoutManager.
stop :: TimeoutManager -> IO ()
stop tm = readMVar (_managerThread tm) >>= killThread
------------------------------------------------------------------------------
-- | Register a new connection with the TimeoutManager.
register :: IO ()
-- ^ action to run when the timeout deadline is exceeded.
-> TimeoutManager -- ^ manager to register with.
-> IO TimeoutHandle
register killAction tm = do
now <- getTime
let !state = Deadline $ now + toEnum defaultTimeout
stateRef <- newIORef state
let !h = TimeoutHandle killAction stateRef getTime
atomicModifyIORef connections $ \x -> (h:x, ())
inact <- readIORef inactivity
when inact $ do
-- wake up manager thread
writeIORef inactivity False
_ <- tryPutMVar morePlease ()
return ()
return h
where
getTime = _getTime tm
inactivity = _inactivity tm
morePlease = _morePlease tm
connections = _connections tm
defaultTimeout = _defaultTimeout tm
------------------------------------------------------------------------------
-- | Tickle the timeout on a connection to be at least N seconds into the
-- future. If the existing timeout is set for M seconds from now, where M > N,
-- then the timeout is unaffected.
tickle :: TimeoutHandle -> Int -> IO ()
tickle th = modify th . max
{-# INLINE tickle #-}
------------------------------------------------------------------------------
-- | Set the timeout on a connection to be N seconds into the future.
set :: TimeoutHandle -> Int -> IO ()
set th = modify th . const
{-# INLINE set #-}
------------------------------------------------------------------------------
-- | Modify the timeout with the given function.
modify :: TimeoutHandle -> (Int -> Int) -> IO ()
modify th f = do
now <- getTime
state <- readIORef stateRef
let !state' = smap now f state
writeIORef stateRef state'
where
getTime = _hGetTime th
stateRef = _state th
{-# INLINE modify #-}
------------------------------------------------------------------------------
-- | Cancel a timeout.
cancel :: TimeoutHandle -> IO ()
cancel h = writeIORef (_state h) Canceled
{-# INLINE cancel #-}
------------------------------------------------------------------------------
managerThread :: TimeoutManager -> IO ()
managerThread tm = loop `finally` (readIORef connections >>= destroyAll)
where
--------------------------------------------------------------------------
connections = _connections tm
getTime = _getTime tm
inactivity = _inactivity tm
morePlease = _morePlease tm
waitABit = threadDelay 5000000
--------------------------------------------------------------------------
loop = do
waitABit
handles <- atomicModifyIORef connections (\x -> ([],x))
if null handles
then do
-- we're inactive, go to sleep until we get new threads
writeIORef inactivity True
takeMVar morePlease
else do
now <- getTime
dlist <- processHandles now handles id
atomicModifyIORef connections (\x -> (dlist x, ()))
loop
--------------------------------------------------------------------------
processHandles !now handles initDlist = go handles initDlist
where
go [] !dlist = return dlist
go (x:xs) !dlist = do
state <- readIORef $ _state x
!dlist' <- case state of
Canceled -> return dlist
Deadline t -> if t <= now
then do
_killAction x
return dlist
else return (dlist . (x:))
go xs dlist'
--------------------------------------------------------------------------
destroyAll = mapM_ diediedie
--------------------------------------------------------------------------
diediedie x = do
state <- readIORef $ _state x
case state of
Canceled -> return ()
_ -> _killAction x