packages feed

atelier-core-0.1.0.0: src/Atelier/Effects/Timeout.hs

-- | An effect for bounding a computation by a wall-clock deadline.
--
-- 'timeout' runs an action, returning 'Nothing' if it does not finish within
-- the given 'TimeUnit' duration; 'timeout_' is the variant that discards the
-- result. When the deadline passes the action is cancelled with an asynchronous
-- exception.
--
-- @
-- result <- timeout (5 :: Second) fetchRemote
-- case result of
--     Just value -> use value
--     Nothing    -> logWarn \"fetch timed out\"
-- @
module Atelier.Effects.Timeout
    ( Timeout
    , timeout
    , timeout_
    , runTimeout
    ) where

import Effectful (Effect, IOE, Limit (..), Persistence (..), UnliftStrategy (..))
import Effectful.Dispatch.Dynamic (interpret, localUnliftIO)
import Effectful.TH (makeEffect)

import System.Timeout qualified as IO

import Atelier.Time (TimeUnit (..))


-- | Effect for bounding a computation by a wall-clock deadline.
data Timeout :: Effect where
    -- | Run an action, returning 'Just' its result if it completes within the
    -- duration, or 'Nothing' if the deadline passes first.
    Timeout :: (TimeUnit t) => t -> m a -> Timeout m (Maybe a)


makeEffect ''Timeout


-- | Like 'timeout', but for times where you do not need the result. If you
-- need to distinguish whether the action ran to completion, or whether it
-- timed out, you should use 'timeout'.
timeout_ :: (TimeUnit t, Timeout :> es) => t -> Eff es a -> Eff es ()
timeout_ t m = const () <$> timeout t m


-- | Interpret 'Timeout' using 'System.Timeout.timeout'.
runTimeout :: (HasCallStack, IOE :> es) => Eff (Timeout : es) a -> Eff es a
runTimeout = interpret \env -> \case
    Timeout delay action ->
        localUnliftIO env (ConcUnlift Persistent (Limited 1)) \unlift ->
            IO.timeout (fromIntegral $ toMicroseconds delay) $ unlift action