packages feed

eventlog-live-0.1.0.0: src-socket/GHC/Eventlog/Live/Socket.hs

{- |
Module      : GHC.Eventlog.Live.Socket
Description : Utilities for running eventlog machines with sockets.
Stability   : experimental
Portability : portable
-}
module GHC.Eventlog.Live.Socket (
  EventlogSocket (..),
  Tick (..),
  tryConnect,
  runWithEventlogSocket,
) where

import Control.Concurrent (threadDelay)
import Control.Exception qualified as E
import Control.Monad.IO.Unlift (MonadUnliftIO (..))
import Data.Machine (ProcessT, runT_, (~>))
import Data.Machine.Fanout (fanout)
import Data.Maybe (fromMaybe)
import Data.Void (Void)
import GHC.Eventlog.Live.Machines
import GHC.Eventlog.Live.Options (EventlogSocket (..))
import GHC.RTS.Events (Event)
import Network.Socket qualified as S
import System.IO (Handle)
import System.IO qualified as IO

{- |
Run an event processor with an eventlog socket.
-}
runWithEventlogSocket ::
  (MonadUnliftIO m) =>
  -- | The eventlog socket handle.
  EventlogSocket ->
  -- | The initial timeout in microseconds for exponential backoff.
  Double ->
  -- | The timeout exponent for exponential backoff.
  Double ->
  -- | The batch interval in milliseconds.
  Int ->
  -- | The number of bytes to read (defaults to 4KiB).
  Maybe Int ->
  -- | An optional file to which to stream binary eventlog data.
  Maybe FilePath ->
  -- | The event processor.
  ProcessT m (Tick Event) Void ->
  m ()
runWithEventlogSocket eventlogSocket timeoutExponent initialTimeoutMcs batchIntervalMs maybeChuckSizeBytes maybeOutputFile toEventSink = do
  -- TODO: Handle connection errors by waiting for the socket to be created.
  withEventlogSocket timeoutExponent initialTimeoutMcs eventlogSocket $ \eventlogHandle -> do
    let chuckSizeBytes = fromMaybe defaultChunkSizeBytes maybeChuckSizeBytes
    let fromSocket = sourceHandleBatch batchIntervalMs chuckSizeBytes eventlogHandle
    case maybeOutputFile of
      Nothing ->
        runT_ $
          fromSocket ~> decodeEventBatch ~> toEventSink
      Just outputFile ->
        withRunInIO $ \runInIO ->
          IO.withFile outputFile IO.WriteMode $ \outputHandle -> do
            runInIO . runT_ $
              fromSocket
                ~> fanout
                  [ fileSinkBatch outputHandle
                  , decodeEventBatch ~> toEventSink
                  ]

{- |
Run an action with a `Handle` to an `EventlogSocket`.
-}
withEventlogSocket ::
  (MonadUnliftIO m) =>
  -- | The initial timeout in microseconds for exponential backoff.
  Double ->
  -- | The timeout exponent for exponential backoff.
  Double ->
  -- | The eventlog socket.
  EventlogSocket ->
  (Handle -> m ()) ->
  m ()
withEventlogSocket initialTimeoutMcs timeoutExponent eventlogSocket action = do
  withRunInIO $ \runInIO ->
    E.bracket (connectRetry initialTimeoutMcs timeoutExponent eventlogSocket) IO.hClose $ \handle ->
      runInIO $
        action handle

{- |
Connect to an `EventlogSocket` with retries and non-randomised exponential backoff.
-}
connectRetry ::
  -- | The initial timeout in microseconds for exponential backoff.
  Double ->
  -- | The timeout exponent for exponential backoff.
  Double ->
  -- | The eventlog socket.
  EventlogSocket ->
  IO Handle
connectRetry initialTimeoutMcs timeoutExponent eventlogSocket =
  connectLoop initialTimeoutMcs
 where
  waitFor :: Double -> IO ()
  waitFor timeoutMcs = threadDelay $ round $ timeoutMcs * 1_000_000

  connectLoop :: Double -> IO Handle
  connectLoop timeoutMcs = do
    E.catch (tryConnect eventlogSocket) $ \(_e :: E.IOException) -> do
      waitFor timeoutMcs
      connectLoop (timeoutMcs * timeoutExponent)

{- |
Try to connect to an `EventlogSocket`.
-}
tryConnect :: EventlogSocket -> IO Handle
tryConnect = \case
  EventlogSocketUnix socketName ->
    E.bracketOnError (S.socket S.AF_UNIX S.Stream S.defaultProtocol) S.close $ \socket -> do
      S.connect socket (S.SockAddrUnix socketName)
      handle <- S.socketToHandle socket IO.ReadMode
      IO.hSetBuffering handle IO.NoBuffering
      pure handle