packages feed

sofetch-0.1.0.0: src/Fetch/Traced.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}

module Fetch.Traced
  ( TracedFetch
  , TraceConfig(..)
  , defaultTraceConfig
  , FetchStats(..)
  , runTracedFetch
  ) where

import Fetch.Class
import Fetch.Cache
import Fetch.Batched (Fetch(..), FetchConfig(..), FetchEnv(..), runLoopWith)
import Fetch.Engine (RoundStats(..))

import Control.Monad.Catch (MonadThrow(..), MonadCatch(..))
import Data.IORef
import Data.Time.Clock (NominalDiffTime, getCurrentTime, diffUTCTime)

-- | Callbacks for observing the batching process.
data TraceConfig m = TraceConfig
  { onRoundStart    :: Int -> Batches m -> m ()
    -- ^ Called before each round with round number and pending batches.
  , onRoundComplete :: Int -> RoundStats -> m ()
    -- ^ Called after each round.
  , onFetchComplete :: FetchStats -> m ()
    -- ^ Called when the entire computation finishes.
  }

-- | No-op trace config.
defaultTraceConfig :: Applicative m => TraceConfig m
defaultTraceConfig = TraceConfig
  { onRoundStart    = \_ _ -> pure ()
  , onRoundComplete = \_ _ -> pure ()
  , onFetchComplete = \_   -> pure ()
  }

-- | Aggregate stats for an entire Fetch computation.
data FetchStats = FetchStats
  { totalRounds        :: !Int
  , totalKeys          :: !Int
  , maxSourcesPerRound :: !Int
    -- ^ Peak number of distinct data sources dispatched in any single round.
  , totalTime          :: !NominalDiffTime
  } deriving (Eq, Show)

-- | A traced variant of Fetch. Same batching\/caching behavior,
-- just adds observability hooks.
--
-- This is a newtype over 'Fetch' so it shares the exact same
-- Applicative batching. The tracing happens at the runner level.
--
-- All instances are derived via @GeneralizedNewtypeDeriving@.
-- If you define your own newtype over 'Fetch', you can use the
-- same pattern:
--
-- @
-- {-\# LANGUAGE GeneralizedNewtypeDeriving, DerivingStrategies \#-}
--
-- newtype MyFetch m a = MyFetch (Fetch m a)
--   deriving newtype
--     ( Functor, Applicative, Monad
--     , MonadFail, MonadThrow, MonadCatch
--     , MonadFetch m
--     )
-- @
newtype TracedFetch m a = TracedFetch (Fetch m a)
  deriving newtype
    ( Functor, Applicative, Monad
    , MonadFail, MonadThrow, MonadCatch
    , MonadFetch m
    )

-- | Run with tracing. Fires callbacks at each round boundary.
runTracedFetch :: Monad m
               => FetchConfig m
               -> TraceConfig m
               -> TracedFetch m a
               -> m (a, FetchStats)
runTracedFetch cfg tc (TracedFetch action) = do
  cacheRef <- case configCache cfg of
    Just ref -> pure ref
    Nothing  -> configLift cfg newCacheRef
  startTime <- configLift cfg getCurrentTime
  statsRef <- configLift cfg $ newIORef (FetchStats 0 0 0 0)

  let e = FetchEnv
        { fetchCache = cacheRef
        , fetchLower = configLower cfg
        , fetchLift  = configLift cfg
        }

      withRound n batches exec = do
        onRoundStart tc n batches
        rs <- exec
        configLift cfg $ modifyIORef' statsRef $ \s -> s
          { totalRounds  = totalRounds s + 1
          , totalKeys    = totalKeys s + roundKeys rs
          , maxSourcesPerRound = max (maxSourcesPerRound s) (roundSources rs)
          }
        onRoundComplete tc n rs

  a <- runLoopWith e withRound action

  endTime <- configLift cfg getCurrentTime
  configLift cfg $ modifyIORef' statsRef $ \s ->
    s { totalTime = diffUTCTime endTime startTime }
  stats <- configLift cfg $ readIORef statsRef

  onFetchComplete tc stats
  pure (a, stats)