log-warper-1.9.0: src/System/Wlog/Concurrent.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | This module introduces functions that allow to run action in parallel with logging.
module System.Wlog.Concurrent
( WaitingDelta (..)
, CanLogInParallel
, logWarningLongAction
, logWarningWaitOnce
, logWarningWaitLinear
, logWarningWaitInf
) where
import Universum
import Control.Concurrent.Async.Lifted (withAsyncWithUnmask)
import Control.Monad.Trans.Control (MonadBaseControl)
import Fmt ((+|), (+||), (|+), (||+))
import GHC.Real ((%))
import Time (RatioNat, Second, Time, sec, threadDelay, timeMul, (+:+))
import System.Wlog.CanLog (WithLoggerIO, logWarning)
-- | Data type to represent waiting strategy for printing warnings
-- if action take too much time.
data WaitingDelta
-- | wait s seconds and stop execution
= WaitOnce (Time Second)
-- | wait s, s * 2, s * 3 , s * 4 , ... seconds
| WaitLinear (Time Second)
-- | wait m, m * q, m * q^2, m * q^3, ... microseconds
| WaitGeometric (Time Second) RatioNat
deriving (Show)
-- | Constraint for something that can be logged in parallel with other action.
type CanLogInParallel m = (MonadBaseControl IO m, WithLoggerIO m)
-- | Run action and print warning if it takes more time than expected.
logWarningLongAction :: forall m a . CanLogInParallel m
=> (Text -> m ()) -> WaitingDelta -> Text -> m a -> m a
logWarningLongAction logFunc delta actionTag action =
-- Previous implementation was
--
-- bracket (fork $ waitAndWarn delta) killThread (const action)
--
-- but this has a subtle problem: 'killThread' can be interrupted even
-- when exceptions are masked, so it's possible that the forked thread is
-- left running, polluting the logs with misinformation.
--
-- 'withAsync' is assumed to take care of this, and indeed it does for
-- 'Production's implementation, which uses the definition from the async
-- package: 'uninterruptibleCancel' is used to kill the thread.
--
-- thinking even more about it, unmasking auxilary thread is crucial if
-- this function is going to be called under 'mask'.
withAsyncWithUnmask (\unmask -> unmask $ waitAndWarn delta) (const action)
where
printWarning :: Time Second -> m ()
printWarning t = logFunc $ "Action `"+|actionTag|+"` took more than "+||t||+""
waitAndWarn :: WaitingDelta -> m ()
waitAndWarn (WaitOnce s) = delayAndPrint s s
waitAndWarn (WaitLinear s) =
let waitLoop :: Time Second -> m ()
waitLoop acc = do
delayAndPrint s acc
waitLoop (acc +:+ s)
in waitLoop s
waitAndWarn (WaitGeometric ms k) =
let waitLoop :: Time Second -> Time Second -> m ()
waitLoop acc delayT = do
let newAcc = acc +:+ delayT
let newDelayT = k `timeMul` delayT
delayAndPrint delayT newAcc
waitLoop newAcc newDelayT
in waitLoop (sec 0) ms
delayAndPrint :: Time Second -> Time Second -> m ()
delayAndPrint delayT printT = do
threadDelay delayT
printWarning printT
{- Helper functions to avoid dealing with data type -}
-- | Specialization of 'logWarningLongAction' with 'WaitOnce'.
logWarningWaitOnce :: CanLogInParallel m => Time Second -> Text -> m a -> m a
logWarningWaitOnce = logWarningLongAction logWarning . WaitOnce
-- | Specialization of 'logWarningLongAction' with 'WaiLinear'.
logWarningWaitLinear :: CanLogInParallel m => Time Second -> Text -> m a -> m a
logWarningWaitLinear = logWarningLongAction logWarning . WaitLinear
-- | Specialization of 'logWarningLongAction' with 'WaitGeometric'
-- with parameter @1.3@. Accepts 'Second'.
logWarningWaitInf :: CanLogInParallel m => Time Second -> Text -> m a -> m a
logWarningWaitInf = logWarningLongAction logWarning
. (`WaitGeometric` (13 % 10))