-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

{- | The config-derived runtime sizings of the composition root: serve-admission capacity,
the two connection-pool sizes and the managers built under them, and the mirror-enqueue
buffer tunables. The byte-valued bounds live in "Ecluse.Composition.MemoryPlan".

Each resolution is a pure function of the validated configuration plus the process
file-descriptor limit. An explicit config value always wins, and 'resolveSized' pairs the
result with the boot-log line naming its provenance.
-}
module Ecluse.Composition.Sizing (
    -- * A resolved bound and its boot-log line
    resolveSized,
    renderSized,

    -- * Connection pools and admission
    newPooledManager,
    connectionPoolSettings,
    resolveServeAdmission,
    resolvePrivateConnections,
    resolvePublicConnections,
    openFileSoftLimit,

    -- * Mirror-enqueue buffering
    mirrorEnqueueBufferDepth,
    mirrorEnqueueReportInterval,
) where

import Data.Ord (clamp)
import Network.HTTP.Client (Manager, ManagerSettings (managerConnCount), newManager)
import System.Posix.Resource (Resource (ResourceOpenFiles), ResourceLimit (ResourceLimit, ResourceLimitInfinity, ResourceLimitUnknown), ResourceLimits (softLimit), getResourceLimit)

{- | A resolved bound and its boot-log line: an explicit config value wins, else the
computed default. Every sizing and every memory-plan bound resolves through this.
-}
resolveSized :: (Show a) => Text -> Maybe a -> a -> Text -> (a, Text)
resolveSized :: forall a. Show a => Text -> Maybe a -> a -> Text -> (a, Text)
resolveSized Text
subject Maybe a
explicit a
computed Text
computedClause =
    (a
value, Text -> a -> Maybe a -> Text -> Text
forall a. Show a => Text -> a -> Maybe a -> Text -> Text
renderSized Text
subject a
value Maybe a
explicit Text
computedClause)
  where
    value :: a
value = a -> Maybe a -> a
forall a. a -> Maybe a -> a
fromMaybe a
computed Maybe a
explicit

{- | The boot-log line for a bound already resolved elsewhere. The explicit config value
decides the provenance clause, and the caller supplies the computed alternative.
-}
renderSized :: (Show a) => Text -> a -> Maybe a -> Text -> Text
renderSized :: forall a. Show a => Text -> a -> Maybe a -> Text -> Text
renderSized Text
subject a
value Maybe a
explicit Text
computedClause =
    Text
subject Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> a -> Text
forall b a. (Show a, IsString b) => a -> b
show a
value Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" (" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
provenance Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
")"
  where
    provenance :: Text
provenance = if Maybe a -> Bool
forall a. Maybe a -> Bool
isJust Maybe a
explicit then Text
"from config" else Text
computedClause

{- | Open an HTTP manager under an explicit per-host connection bound. Every manager the
composition root builds comes from here, so one pool bound applies whatever dials through it.
-}
newPooledManager :: Int -> ManagerSettings -> IO Manager
newPooledManager :: Int -> ManagerSettings -> IO Manager
newPooledManager Int
connections = ManagerSettings -> IO Manager
newManager (ManagerSettings -> IO Manager)
-> (ManagerSettings -> ManagerSettings)
-> ManagerSettings
-> IO Manager
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> ManagerSettings -> ManagerSettings
connectionPoolSettings Int
connections

{- | Apply an explicit per-host connection bound to an HTTP manager's settings. Callers
apply it after telemetry instrumentation, so it cannot discard the instrumented hooks.
-}
connectionPoolSettings :: Int -> ManagerSettings -> ManagerSettings
connectionPoolSettings :: Int -> ManagerSettings -> ManagerSettings
connectionPoolSettings Int
connections ManagerSettings
settings = ManagerSettings
settings{managerConnCount = connections}

{- | The serve-admission capacity and its boot-log line: explicit @serveMaxInFlight@, else
@max 8 (10 x capabilities)@ on the post-posture count. The multiplier is where the bench levelled.
-}
resolveServeAdmission :: Maybe Int -> Int -> (Int, Text)
resolveServeAdmission :: Maybe Int -> Int -> (Int, Text)
resolveServeAdmission Maybe Int
explicit Int
capabilities =
    Text -> Maybe Int -> Int -> Text -> (Int, Text)
forall a. Show a => Text -> Maybe a -> a -> Text -> (a, Text)
resolveSized
        Text
"runtime: serve admission"
        Maybe Int
explicit
        (Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
serveAdmissionFloor (Int
serveAdmissionPerCapability Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
capabilities))
        (Text
"computed from " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show Int
capabilities Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" capabilities")

-- The floor keeps a tiny pod admitting a useful burst. 'resolveServeAdmission' explains
-- the multiplier.
serveAdmissionPerCapability :: Int
serveAdmissionPerCapability :: Int
serveAdmissionPerCapability = Int
10

serveAdmissionFloor :: Int
serveAdmissionFloor :: Int
serveAdmissionFloor = Int
8

{- | The private pool size and its boot-log line: @privateConnectionsPerHost@, else
@clamp (64, 4096) (nofile \/ 4)@. 'managerConnCount' caps retention, not concurrency.
-}
resolvePrivateConnections :: Maybe Int -> Int -> (Int, Text)
resolvePrivateConnections :: Maybe Int -> Int -> (Int, Text)
resolvePrivateConnections Maybe Int
explicit Int
fdLimit =
    Text -> Maybe Int -> Int -> Text -> (Int, Text)
forall a. Show a => Text -> Maybe a -> a -> Text -> (a, Text)
resolveSized
        Text
"runtime: private connection pool"
        Maybe Int
explicit
        (Int -> Int
clampPrivateConnections (Int
fdLimit Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
privateConnectionsFdShare))
        (Int -> Text
fdLimitClause Int
fdLimit)

-- The floor keeps a small file-descriptor limit reusing a useful number of connections.
-- The cap stops an enormous limit retaining an absurd idle cache to one upstream.
clampPrivateConnections :: Int -> Int
clampPrivateConnections :: Int -> Int
clampPrivateConnections = (Int, Int) -> Int -> Int
forall a. Ord a => (a, a) -> a -> a
clamp (Int
privateConnectionsFloor, Int
privateConnectionsCap)

-- One descriptor per pooled connection. The private pool takes a quarter of the budget
-- and leaves the rest to the listener, the public pool, telemetry, the worker, and the runtime.
privateConnectionsFdShare :: Int
privateConnectionsFdShare :: Int
privateConnectionsFdShare = Int
4

privateConnectionsFloor :: Int
privateConnectionsFloor :: Int
privateConnectionsFloor = Int
64

privateConnectionsCap :: Int
privateConnectionsCap :: Int
privateConnectionsCap = Int
4096

{- | The public pool size and its boot-log line: @publicConnectionsPerHost@, else
@clamp (32, 1024) (nofile \/ 8)@. Onboarding fail-over and back-fill streams ride it too.
-}
resolvePublicConnections :: Maybe Int -> Int -> (Int, Text)
resolvePublicConnections :: Maybe Int -> Int -> (Int, Text)
resolvePublicConnections Maybe Int
explicit Int
fdLimit =
    Text -> Maybe Int -> Int -> Text -> (Int, Text)
forall a. Show a => Text -> Maybe a -> a -> Text -> (a, Text)
resolveSized
        Text
"runtime: public connection pool"
        Maybe Int
explicit
        (Int -> Int
clampPublicConnections (Int
fdLimit Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
publicConnectionsFdShare))
        (Int -> Text
fdLimitClause Int
fdLimit)

fdLimitClause :: Int -> Text
fdLimitClause :: Int -> Text
fdLimitClause Int
fdLimit = Text
"computed from file-descriptor limit " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show Int
fdLimit

-- The floor keeps a small limit reusing connections across an onboarding burst. The cap
-- and the reasoning match 'clampPrivateConnections'.
clampPublicConnections :: Int -> Int
clampPublicConnections :: Int -> Int
clampPublicConnections = (Int, Int) -> Int -> Int
forall a. Ord a => (a, a) -> a -> a
clamp (Int
publicConnectionsFloor, Int
publicConnectionsCap)

-- An eighth of the file-descriptor budget, drawn from the reserve the private sizing
-- leaves. The public leg is the transient onboarding ramp, not the steady-state load.
publicConnectionsFdShare :: Int
publicConnectionsFdShare :: Int
publicConnectionsFdShare = Int
8

publicConnectionsFloor :: Int
publicConnectionsFloor :: Int
publicConnectionsFloor = Int
32

publicConnectionsCap :: Int
publicConnectionsCap :: Int
publicConnectionsCap = Int
1024

{- | The depth of the hand-off buffer in front of the mirror queue. It absorbs a cold @npm ci@
burst, and a job dropped at the cap re-enqueues on the next demand, so overflow defers a mirror.
-}
mirrorEnqueueBufferDepth :: Int
mirrorEnqueueBufferDepth :: Int
mirrorEnqueueBufferDepth = Int
1024

{- | How many enqueue-buffer drops or delivery failures pass between warning-log reports. The
buffer's callbacks still fire per event, so the counter beside the log stays exact.
-}
mirrorEnqueueReportInterval :: Int
mirrorEnqueueReportInterval :: Int
mirrorEnqueueReportInterval = Int
100

{- | The process soft file-descriptor limit (@RLIMIT_NOFILE@). An infinite or unknown limit falls
back to a value that lands the computed pool on its cap rather than overflowing.
-}
openFileSoftLimit :: IO Int
openFileSoftLimit :: IO Int
openFileSoftLimit = do
    limits <- Resource -> IO ResourceLimits
getResourceLimit Resource
ResourceOpenFiles
    pure $ case softLimit limits of
        ResourceLimit Integer
n -> Integer -> Int
forall a. Num a => Integer -> a
fromInteger Integer
n
        ResourceLimit
ResourceLimitInfinity -> Int
privateConnectionsCap Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
privateConnectionsFdShare
        ResourceLimit
ResourceLimitUnknown -> Int
privateConnectionsCap Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
privateConnectionsFdShare