packages feed

cgroup-rts-threads-0.2.1.0: src/Control/Concurrent/CGroup.hs

-- | This module provides a container-/cgroup-aware substitute for GHC's RTS
-- @-N@ flag. See 'initRTSThreads'.
module Control.Concurrent.CGroup (
  initRTSThreads,
  initRTSThreadsWith,
  RoundQuota (..),
) where

import Control.Exception (Exception (..), SomeAsyncException (SomeAsyncException), SomeException, catch, throwIO)
import qualified Data.Ratio as Ratio
import GHC.Conc (getNumProcessors, setNumCapabilities)
import System.CGroup.Types (CPUQuota (..))
import qualified System.CGroup.V1.CPU as V1
import qualified System.CGroup.V2.CPU as V2

-- | CPU quotas can be fractions, but the number of RTS capabilities is an integer. This type
-- determines how to round the CPU quota to get to the number of capabilities.
--
-- The names correspond to the similarly named methods of 'RealFrac'.
data RoundQuota
  = CeilingQuota
  | FloorQuota
  | RoundQuota
  deriving (Show, Read, Eq, Ord)

-- | Round a quota.
roundQuota :: RoundQuota -> Ratio.Ratio Int -> Int
roundQuota roundMode =
  case roundMode of
    CeilingQuota -> ceiling
    FloorQuota -> floor
    RoundQuota -> round

-- | A container-/cgroup-aware substitute for GHC's RTS @-N@ flag.
--
-- On most platforms, this sets the number of runtime threads to match the
-- number of physical processors (see 'GHC.Conc.getNumProcessors'), which is the
-- default behavior of the GHC @-N@ flag.
--
-- When running within a cgroup on linux (most often within a container), this
-- observes the current process' cgroup cpu quota to constrain the number of
-- runtime threads.
--
-- By default, the number of capabilities is determined by rounding the CPU quota down.
--
-- See 'CPUQuota'
initRTSThreads :: IO ()
initRTSThreads = initRTSThreadsWith FloorQuota

-- | Same as 'initRTSThreads' but lets you specify the quota round mode.
initRTSThreadsWith :: RoundQuota -> IO ()
initRTSThreadsWith roundMode = do
  quota <-
    V1.getProcessCPUQuota
      `fallback` V2.getProcessEffectiveCPUQuota
      `fallback` pure NoQuota
  initRTSThreadsFromQuota roundMode quota

-- | Use a CPU quota to set the number of runtime threads.
initRTSThreadsFromQuota :: RoundQuota -> CPUQuota -> IO ()
initRTSThreadsFromQuota _ NoQuota = defaultInitRTSThreads
initRTSThreadsFromQuota roundMode (CPUQuota ratio) = do
  procs <- getNumProcessors
  setNumCapabilities $ clamp 1 procs $ roundQuota roundMode ratio

-- | Set number of runtime threads to the number of available processors. This
-- matches the behavior of GHC's RTS @-N@ flag.
defaultInitRTSThreads :: IO ()
defaultInitRTSThreads = setNumCapabilities =<< getNumProcessors

-- | Clamp a value within a range
clamp :: Int -> Int -> Int -> Int
clamp lower upper = max lower . min upper

-- | Catch non-async exceptions
safeCatch :: Exception e => IO a -> (e -> IO a) -> IO a
safeCatch act hdl = act `catch` (\e -> if isSyncException e then hdl e else throwIO e)

isSyncException :: Exception e => e -> Bool
isSyncException e =
  case fromException (toException e) of
    Just (SomeAsyncException _) -> False
    Nothing -> True

-- | Return the result of the first successful action
fallback :: IO a -> IO a -> IO a
fallback a b = a `safeCatch` (\(_ :: SomeException) -> b)