packages feed

quic-0.2.3: Network/QUIC/Connection/Timeout.hs

module Network.QUIC.Connection.Timeout (
    timeout,
    fire,
    cfire,
    delay,
) where

import Control.Concurrent
import Control.Exception
import Data.Unique (Unique, newUnique)
import GHC.Conc.Sync
import Network.QUIC.Event

import Network.QUIC.Connection.Types
import Network.QUIC.Connector
import Network.QUIC.Imports
import Network.QUIC.Types

newtype Timeout = Timeout Unique deriving (Eq)

instance Show Timeout where
    show _ = "<<timeout>>"

instance Exception Timeout where
    toException = asyncExceptionToException
    fromException = asyncExceptionFromException

-- 'SomeException') within the computation will break the timeout behavior.
timeout :: Microseconds -> String -> IO a -> IO (Maybe a)
timeout (Microseconds n) label f
    | n < 0 = fmap Just f
    | n == 0 = return Nothing
    | otherwise = do
        -- In the threaded RTS, we use the Timer Manager to delay the
        -- (fairly expensive) 'forkIO' call until the timeout has expired.
        --
        -- An additional thread is required for the actual delivery of
        -- the Timeout exception because killThread (or another throwTo)
        -- is the only way to reliably interrupt a throwTo in flight.
        pid <- myThreadId
        ex <- fmap Timeout newUnique
        tm <- getSystemTimerManager
        -- 'lock' synchronizes the timeout handler and the main thread:
        --  * the main thread can disable the handler by writing to 'lock';
        --  * the handler communicates the spawned thread's id through 'lock'.
        -- These two cases are mutually exclusive.
        lock <- newEmptyMVar
        let handleTimeout = do
                v <- isEmptyMVar lock
                when v $ void $ forkIOWithUnmask $ \unmask -> unmask $ do
                    tid <- myThreadId
                    labelThread tid $ "timeout:" ++ label
                    v2 <- tryPutMVar lock =<< myThreadId
                    when v2 $ throwTo pid ex
            cleanupTimeout key = uninterruptibleMask_ $ do
                v <- tryPutMVar lock undefined
                if v
                    then unregisterTimeout tm key
                    else takeMVar lock >>= killThread
        handleJust
            (\e -> if e == ex then Just () else Nothing)
            (\_ -> return Nothing)
            ( bracket
                (registerTimeout tm n handleTimeout)
                cleanupTimeout
                (\_ -> fmap Just f)
            )

fire :: Connection -> Microseconds -> TimeoutCallback -> IO ()
fire conn (Microseconds microseconds) action = do
    timmgr <- getSystemTimerManager
    void $ registerTimeout timmgr microseconds action'
  where
    action' = do
        alive <- getAlive conn
        when alive action `catch` ignore

cfire :: Connection -> Microseconds -> TimeoutCallback -> IO (IO ())
cfire conn (Microseconds microseconds) action = do
    timmgr <- getSystemTimerManager
    key <- registerTimeout timmgr microseconds action'
    let cancel = unregisterTimeout timmgr key
    return cancel
  where
    action' = do
        alive <- getAlive conn
        when alive action `catch` ignore

delay :: Microseconds -> IO ()
delay (Microseconds microseconds) = threadDelay microseconds