2019-08-28 15:19:21 +03:00
module Control.Concurrent.Extended
( module Control.Concurrent
2020-01-16 04:56:57 +03:00
, sleep
2020-03-05 20:59:26 +03:00
, ForkableMonadIO
-- * Robust forking
, forkImmortal
2020-12-21 21:56:00 +03:00
, forkManagedT
2020-01-16 04:56:57 +03:00
-- * Deprecated
2019-08-28 15:19:21 +03:00
, threadDelay
2020-03-05 20:59:26 +03:00
, forkIO
2019-08-28 15:19:21 +03:00
) where
import Prelude
2020-03-05 20:59:26 +03:00
import Control.Exception
2020-12-21 21:56:00 +03:00
import Control.Monad.Trans.Managed ( ManagedT ( .. ) , allocate )
2020-03-05 20:59:26 +03:00
import Control.Monad.IO.Class
import Control.Monad
import Data.Aeson
import Data.Void
2019-08-28 15:19:21 +03:00
2020-03-05 20:59:26 +03:00
import qualified Control.Concurrent as Base
import qualified Control.Concurrent.Async.Lifted.Safe as LA
import qualified Control.Immortal as Immortal
import qualified Control.Monad.Trans.Control as MC
2019-08-28 15:19:21 +03:00
2020-12-21 21:56:00 +03:00
import Control.Concurrent hiding ( threadDelay , forkIO )
import Data.Time.Clock.Units ( seconds , Microseconds ( .. ) , DiffTime )
2020-03-05 20:59:26 +03:00
-- For forkImmortal. We could also have it take a cumbersome continuation if we
-- want to break this dependency. Probably best to move Hasura.Logging into a
-- separate lib with this if we do the override thing.
import Hasura.Logging
2019-08-28 15:19:21 +03:00
2020-01-16 04:56:57 +03:00
-- | Like 'Base.threadDelay', but takes a 'DiffTime' instead of an 'Int' microseconds.
--
-- NOTE: you cannot simply replace e.g. @threadDelay 1000@ with @sleep 1000@ since those literals
-- have different meanings!
sleep :: DiffTime -> IO ()
sleep = Base . threadDelay . round . Microseconds
{- # DEPRECATED threadDelay "Please use `sleep` instead (and read the docs!)" # -}
threadDelay :: Int -> IO ()
threadDelay = Base . threadDelay
2020-03-05 20:59:26 +03:00
{- # DEPRECATED forkIO
" Please use 'Control.Control.Concurrent.Async.Lifted.Safe.withAsync' \
2020-10-16 14:55:18 +03:00
\ or our 'forkImmortal' instead formore robust threading . " #-}
2020-03-05 20:59:26 +03:00
forkIO :: IO () -> IO ThreadId
forkIO = Base . forkIO
2020-12-21 21:56:00 +03:00
-- | Note: Please consider using 'forkManagedT' instead to ensure reliable
-- resource cleanup.
2020-03-05 20:59:26 +03:00
forkImmortal
:: ForkableMonadIO m
=> String
-- ^ A label describing this thread's function (see 'labelThread').
-> Logger Hasura
-> m Void
-- ^ An IO action we expect never to return normally. This will have the type
-- signature ':: m a' (see e.g. the type of 'forever').
-> m Immortal . Thread
-- ^ A handle for the forked thread. See "Control.Immortal".
forkImmortal label logger m =
2020-12-21 21:56:00 +03:00
Immortal . createWithLabel label $ \ this -> do
-- Log that the thread has started
liftIO $ unLogger logger ( ImmortalThreadRestarted label )
-- In this case, we are handling unexpected exceptions.
-- i.e This does not catch the asynchronous exception which stops the thread.
2020-03-05 20:59:26 +03:00
Immortal . onUnexpectedFinish this logAndPause ( void m )
2020-12-21 21:56:00 +03:00
where logAndPause = \ case
Right _void -> pure () -- absurd _void (i.e. unreachable)
Left e -> liftIO $ do
liftIO $ unLogger logger ( ImmortalThreadUnexpectedException label e )
-- pause before restarting some arbitrary amount of time. The idea is not to flood
-- logs or cause other cascading failures.
sleep ( seconds 1 )
2020-03-05 20:59:26 +03:00
2020-12-21 21:56:00 +03:00
-- | This function pairs a call to 'forkImmortal' with a finalizer which stops
-- the immortal thread.
--
-- Note, the thread object can leave its scope if this function is incorrectly
-- used. Generally, the result should only be used later in the same ManagedT
-- scope.
forkManagedT
:: ForkableMonadIO m
=> String
-> Logger Hasura
-> m Void
-> ManagedT m Immortal . Thread
forkManagedT label logger m = allocate
( forkImmortal label logger m )
( \ thread -> do
unLogger logger ( ImmortalThreadStopping label )
liftIO $ Immortal . stop thread )
data ImmortalThreadLog
= ImmortalThreadUnexpectedException String SomeException
-- ^ Synchronous Exception
| ImmortalThreadStopping String
-- ^ Asynchronous Exception about to be sent
| ImmortalThreadRestarted String
2020-03-05 20:59:26 +03:00
instance ToEngineLog ImmortalThreadLog Hasura where
2020-12-21 21:56:00 +03:00
toEngineLog ( ImmortalThreadStopping label ) =
( LevelInfo , ELTInternal ILTUnstructured , toJSON msg )
where msg = " Stopping immortal " <> label <> " thread "
toEngineLog ( ImmortalThreadUnexpectedException label e ) =
2020-03-05 20:59:26 +03:00
( LevelError , ELTInternal ILTUnstructured , toJSON msg )
2020-12-21 21:56:00 +03:00
where msg = " Unexpected exception in immortal thread " <> label <> " (it will be restarted): \ n "
2020-03-05 20:59:26 +03:00
<> show e
2020-12-21 21:56:00 +03:00
toEngineLog ( ImmortalThreadRestarted label ) =
( LevelInfo , ELTInternal ILTUnstructured , toJSON msg )
where msg = " Thread " <> label <> " (re)started "
2020-03-05 20:59:26 +03:00
-- TODO
-- - maybe use this everywhere, but also:
-- - consider unifying with: src-lib/Control/Monad/Stateless.hs ?
-- - nice TypeError: https://kodimensional.dev/type-errors
--
-- | Like 'MonadIO' but constrained to stacks in which forking a new thread is reasonable/safe.
-- In particular 'StateT' causes problems.
--
-- This is the constraint you can use for functions that call 'LA.async', or 'immortal'.
type ForkableMonadIO m = ( MonadIO m , MC . MonadBaseControl IO m , LA . Forall ( LA . Pure m ) )
-- TODO consider deprecating async.
-- export something with polymorphic return type, which makes "fork and forget" difficult
-- this could automatically link in one variant
-- another variant might return ThreadId that self destructs w/ finalizer (mkWeakThreadId)
-- and note: "Holding a normal ThreadId reference will prevent the delivery of BlockedIndefinitely exceptions because the reference could be used as the target of throwTo at any time, "