packages feed

shibuya-core-0.8.0.0: src/Shibuya/Internal/Runner/Master.hs

-- | __Internal module.__ Exposed for the test suite and benchmarks only.
-- No PVP guarantees: anything here may change or disappear in any release.
-- Application authors should import "Shibuya" instead.
--
-- Master process - central coordinator for queue processors.
-- Provides supervision, metrics collection, and control API.
--
-- Architecture:
-- - Master is an NQE Process that handles control messages
-- - Holds a Supervisor for managing child processors
-- - Maintains TVar MetricsMap for O(1) metrics access
-- - Processors register their metrics TVars with the Master
module Shibuya.Internal.Runner.Master
  ( -- * Master Handle
    Master (..),
    MasterState (..),

    -- * Control Messages
    MasterMessage (..),

    -- * Starting the Master
    startMaster,
    stopMaster,

    -- * Introspection
    getAllMetrics,
    getAllMetricsIO,
    getProcessorMetrics,
    getProcessorMetricsIO,

    -- * Processor Management
    registerProcessor,
    unregisterProcessor,
  )
where

import Control.Concurrent.NQE.Process
  ( Inbox,
    Listen,
    Process (..),
    newInbox,
    receive,
  )
import Control.Concurrent.NQE.Supervisor (Strategy (..), Supervisor)
import Control.Concurrent.NQE.Supervisor qualified as Supervisor
import Control.Concurrent.STM
  ( TVar,
    atomically,
    modifyTVar',
    newTVarIO,
    readTVar,
  )
import Control.Monad (forever)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Effectful (Eff, IOE, liftIO, (:>))
import Shibuya.Core.Metrics
  ( MetricsHandle,
    MetricsMap,
    ProcessorId,
    ProcessorMetrics,
    sampleMetrics,
  )
import Shibuya.Prelude
import UnliftIO (Async, async, cancel, link)

-- | Messages for the master process.
data MasterMessage
  = -- | Register a processor's metrics handle
    RegisterProcessor !ProcessorId !MetricsHandle !(Listen ())
  | -- | Unregister a processor
    UnregisterProcessor !ProcessorId !(Listen ())
  | -- | Shutdown all processors
    Shutdown !(Listen ())

-- | Master state held in TVars.
data MasterState = MasterState
  { -- | Map of processor IDs to their metrics handles
    metrics :: !(TVar (Map ProcessorId MetricsHandle)),
    -- | The supervisor managing child processors
    supervisor :: !Supervisor,
    -- | Whether child failures should be linked into the spawning thread.
    -- Derived from the supervision strategy: True for KillAll/IgnoreGraceful
    -- (failure must reach the application), False for IgnoreAll/Notify.
    propagateFailures :: !Bool
  }
  deriving (Generic)

-- | Master handle - provides access to the master process.
data Master = Master
  { -- | The async handle for the master
    handle :: !(Async ()),
    -- | Direct access to master state
    state :: !MasterState,
    -- | Inbox for sending messages
    inbox :: !(Inbox MasterMessage)
  }
  deriving (Generic)

-- | Start the master process.
-- Returns a handle for interacting with the master.
-- The caller is responsible for calling stopMaster when done.
startMaster :: (IOE :> es) => Strategy -> Eff es Master
startMaster strategy = liftIO $ do
  -- Create supervisor
  sup <- Supervisor.supervisor strategy

  metricsMapVar <- newTVarIO Map.empty
  let propagate = case strategy of
        KillAll -> True
        IgnoreGraceful -> True
        IgnoreAll -> False
        Notify _ -> False
      masterState = MasterState metricsMapVar sup propagate

  masterInbox <- newInbox

  -- Start master loop
  masterHandle <- async $ masterLoop masterState masterInbox
  link masterHandle

  pure
    Master
      { handle = masterHandle,
        state = masterState,
        inbox = masterInbox
      }

-- | Stop the master and all child processors.
-- Cancels the supervisor first (which cancels all children via NQE's stopAll),
-- then cancels the master message loop.
stopMaster :: (IOE :> es) => Master -> Eff es ()
stopMaster master = liftIO $ do
  -- Cancel the supervisor first - this triggers NQE's stopAll which cancels all children
  cancel (getProcessAsync master.state.supervisor)
  -- Then cancel the master message loop
  cancel master.handle

-- | The master process main loop.
masterLoop :: MasterState -> Inbox MasterMessage -> IO ()
masterLoop state inbox = forever $ do
  msg <- receive inbox
  handleMessage state msg

-- | Handle a single master message.
handleMessage :: MasterState -> MasterMessage -> IO ()
handleMessage state msg = case msg of
  RegisterProcessor pid metricsHandle respond -> do
    atomically $ do
      modifyTVar' state.metrics $ Map.insert pid metricsHandle
    atomically $ respond ()
  UnregisterProcessor pid respond -> do
    atomically $ do
      modifyTVar' state.metrics $ Map.delete pid
    atomically $ respond ()
  Shutdown respond -> do
    -- Clear all processors
    atomically $ modifyTVar' state.metrics (const Map.empty)
    atomically $ respond ()

-- | Get metrics for all processors.
getAllMetrics :: (IOE :> es) => Master -> Eff es MetricsMap
getAllMetrics = liftIO . getAllMetricsIO

-- | Get metrics for all processors (IO version for web servers).
getAllMetricsIO :: Master -> IO MetricsMap
getAllMetricsIO master = do
  handlesMap <- atomically $ readTVar master.state.metrics
  traverse sampleMetrics handlesMap

-- | Get metrics for a specific processor.
getProcessorMetrics :: (IOE :> es) => Master -> ProcessorId -> Eff es (Maybe ProcessorMetrics)
getProcessorMetrics master = liftIO . getProcessorMetricsIO master

-- | Get metrics for a specific processor (IO version for web servers).
getProcessorMetricsIO :: Master -> ProcessorId -> IO (Maybe ProcessorMetrics)
getProcessorMetricsIO master pid = do
  handlesMap <- atomically $ readTVar master.state.metrics
  traverse sampleMetrics (Map.lookup pid handlesMap)

-- | Register a processor with the master.
-- The processor should call this with its metrics handle.
registerProcessor :: (IOE :> es) => Master -> ProcessorId -> MetricsHandle -> Eff es ()
registerProcessor master pid metricsHandle =
  liftIO $ atomically $ modifyTVar' master.state.metrics $ Map.insert pid metricsHandle

-- | Unregister a processor from the master.
unregisterProcessor :: (IOE :> es) => Master -> ProcessorId -> Eff es ()
unregisterProcessor master pid =
  liftIO $ atomically $ modifyTVar' master.state.metrics $ Map.delete pid