-- SPDX-FileCopyrightText: 2026 Alexandra de Wit -- -- SPDX-License-Identifier: MIT {- | The config-derived runtime sizings of the composition root: the serve-admission capacity, the two connection-pool sizes and the file-descriptor datapoint they are computed from, and the mirror-enqueue buffer tunables. (The byte-valued bounds are partitioned separately, from the heap ceiling: "Ecluse.Composition.MemoryPlan".) Each resolution is a pure function of the validated configuration (plus, for the pools, the process file-descriptor limit read once by 'openFileSoftLimit'): an explicit config value always wins, and a computed default returns its boot-log line alongside the number so the decision's provenance lands in the standard boot log. Nothing here opens a socket or reads a clock; the composition root applies the results when it builds the managers and the admission gate. -} module Ecluse.Composition.Sizing ( -- * Connection pools and admission connectionPoolSettings, resolveServeAdmission, resolvePrivateConnections, resolvePublicConnections, openFileSoftLimit, -- * Mirror-enqueue buffering mirrorEnqueueBufferDepth, mirrorEnqueueReportInterval, ) where import Network.HTTP.Client (ManagerSettings (managerConnCount)) import System.Posix.Resource (Resource (ResourceOpenFiles), ResourceLimit (ResourceLimit, ResourceLimitInfinity, ResourceLimitUnknown), ResourceLimits (softLimit), getResourceLimit) {- | Apply an explicit per-host connection bound to an HTTP manager's settings. The public and private managers call this independently after telemetry instrumentation, so changing the pool size cannot discard the instrumented request and response hooks. -} connectionPoolSettings :: Int -> ManagerSettings -> ManagerSettings connectionPoolSettings :: Int -> ManagerSettings -> ManagerSettings connectionPoolSettings Int connections ManagerSettings settings = ManagerSettings settings{managerConnCount = connections} {- | The effective serve-admission capacity and its boot-log line: the explicit @serveMaxInFlight@ when configured, else __computed from the resolved capability count__ -- @max 8 (10 x capabilities)@. The multiplier is empirical, not modelled. The saturation model (an admitted metadata materialisation alternates upstream wait @W@ and CPU work @P@, so keeping @C@ capabilities busy wants about @C x (W + P) \/ P@ in flight) suggested ~4 per capability at a round-trip @W\/P@ of 2-3, but the load bench's measured dose-response kept climbing well past that and levelled only near 10 per capability: a slot is held across every upstream leg plus GC pauses and scheduling delay, so the effective @W\/P@ is nearer 9-10. The floor keeps a tiny pod admitting a useful burst should the multiplier ever drop below it. The capability count must be the __post-runtime-posture__ one (see "Ecluse.Rts"), so callers resolve this after 'Ecluse.Runtime.applyRuntimePosture' has run. The returned line carries the decision's provenance for the standard boot log, alongside the runtime posture lines. This bounds only __metadata materialisation__ (whole packument requests and a tarball miss's public-metadata gate). The __private__ connection pool is __not__ sized from it -- see 'resolvePrivateConnections': a trusted tarball hit __streams outside admission__, so demand on the private pool is the inbound hit concurrency, not the admission capacity, and tying the two would undersize that pool under a private-hit fan-out (http-client opens throwaway connections beyond the pool, paying a TLS handshake per overflow request). -} resolveServeAdmission :: Maybe Int -> Int -> (Int, Text) resolveServeAdmission :: Maybe Int -> Int -> (Int, Text) resolveServeAdmission Maybe Int explicit Int capabilities = case Maybe Int explicit of Just Int n -> (Int n, Text "runtime: serve admission " Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Int -> Text forall b a. (Show a, IsString b) => a -> b show Int n Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text " (from config)") Maybe Int Nothing -> let computed :: Int computed = 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) in (Int computed, Text "runtime: serve admission " Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Int -> Text forall b a. (Show a, IsString b) => a -> b show Int computed Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> 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 computed-admission constants: empirically ~10 per capability (see -- 'resolveServeAdmission'), and a floor so a tiny pod still admits a useful -- burst if the multiplier ever drops below it. serveAdmissionPerCapability :: Int serveAdmissionPerCapability :: Int serveAdmissionPerCapability = Int 10 serveAdmissionFloor :: Int serveAdmissionFloor :: Int serveAdmissionFloor = Int 8 {- | The effective private-upstream connection-pool size and its boot-log line: the explicit @privateConnectionsPerHost@ when configured, else __computed from the process file-descriptor limit__ -- @clamp 64 4096 (nofile \/ 4)@. The private pool caches idle connections to the trusted upstream for __reuse across concurrent private-hit tarball streams__. Those streams are __IO-bound__ and, unlike metadata materialisation, stream __outside serve admission__, so their concurrency (and thus the pool's real demand) is the inbound hit fan-out, not the CPU-saturation model 'resolveServeAdmission' uses -- which is exactly why this is computed from a different datapoint and is __not__ tied to @serveMaxInFlight@ (see issue #634's incomplete inference: the private pool also serves the un-admitted streaming path). Each pooled connection is one file descriptor, so the file-descriptor limit is the pool's real physical ceiling. The default takes a __quarter of the soft @RLIMIT_NOFILE@__ as the reuse cache, floored at 'privateConnectionsFloor' so a small-limit host still reuses connections across an install fan-out, and capped at 'privateConnectionsCap' so an enormous-limit host does not retain an absurd idle cache to a single upstream. A larger pool never opens more sockets than the concurrency already demands (http-client opens a connection per in-flight request regardless); it only decides how many to __retain for reuse__ rather than re-handshake, so sizing up is safe. An operator who knows their fan-out can override it outright. The returned line carries the decision's provenance for the standard boot log. -} resolvePrivateConnections :: Maybe Int -> Int -> (Int, Text) resolvePrivateConnections :: Maybe Int -> Int -> (Int, Text) resolvePrivateConnections Maybe Int explicit Int fdLimit = case Maybe Int explicit of Just Int n -> (Int n, Text "runtime: private connection pool " Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Int -> Text forall b a. (Show a, IsString b) => a -> b show Int n Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text " (from config)") Maybe Int Nothing -> let computed :: Int computed = Int -> Int clampPrivateConnections (Int fdLimit Int -> Int -> Int forall a. Integral a => a -> a -> a `div` Int privateConnectionsFdShare) in (Int computed, Text "runtime: private connection pool " Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Int -> Text forall b a. (Show a, IsString b) => a -> b show Int computed Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> 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 Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text ")") -- Clamp a computed private-pool size into the sane band: a floor so a small -- file-descriptor limit still reuses a useful number of connections, and a cap so an -- enormous limit does not retain an absurd idle cache to one upstream. clampPrivateConnections :: Int -> Int clampPrivateConnections :: Int -> Int clampPrivateConnections = Int -> Int -> Int forall a. Ord a => a -> a -> a max Int privateConnectionsFloor (Int -> Int) -> (Int -> Int) -> Int -> Int forall b c a. (b -> c) -> (a -> b) -> a -> c . Int -> Int -> Int forall a. Ord a => a -> a -> a min Int privateConnectionsCap -- The private pool takes a quarter of the file-descriptor budget as its reuse cache -- (each pooled connection is one descriptor); the other three quarters stay for the -- listener, the public pool, telemetry, the worker, and the runtime. privateConnectionsFdShare :: Int = Int 4 privateConnectionsFloor :: Int privateConnectionsFloor :: Int privateConnectionsFloor = Int 64 privateConnectionsCap :: Int privateConnectionsCap :: Int privateConnectionsCap = Int 4096 {- | The effective public-upstream connection-pool size and its boot-log line: the explicit @publicConnectionsPerHost@ when configured, else __computed from the process file-descriptor limit__ -- @clamp 32 1024 (nofile \/ 8)@. The public pool's metadata demand is small by construction (same-key misses are single-flight-coalesced and bounded by admission), but the pool is __not__ metadata-only: the onboarding fail-over's artifact streams and the mirror worker's back-fill fetches ride the same manager, and neither coalesces. During a cold fleet's onboarding burst the concurrent public streams track the inbound fan-out, and 'Network.HTTP.Client.managerConnCount' is a keep-alive __retention__ cap, not a concurrency cap: overflow opens throwaway connections, each paying a TLS handshake to the public origin per request. So the pool is sized like the private one, from the file-descriptor budget, at __half the private share__ (an eighth of @nofile@, from the three quarters the private sizing reserves for everything else): the public leg is transient by the traffic model -- the worker retires it artifact by artifact -- so it earns retention for the burst, not the steady state. Sizing up is safe for the same reason as the private pool: it never opens more sockets than the concurrency already demands, only retains more for reuse. The returned line carries the decision's provenance for the standard boot log. -} resolvePublicConnections :: Maybe Int -> Int -> (Int, Text) resolvePublicConnections :: Maybe Int -> Int -> (Int, Text) resolvePublicConnections Maybe Int explicit Int fdLimit = case Maybe Int explicit of Just Int n -> (Int n, Text "runtime: public connection pool " Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Int -> Text forall b a. (Show a, IsString b) => a -> b show Int n Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text " (from config)") Maybe Int Nothing -> let computed :: Int computed = Int -> Int clampPublicConnections (Int fdLimit Int -> Int -> Int forall a. Integral a => a -> a -> a `div` Int publicConnectionsFdShare) in (Int computed, Text "runtime: public connection pool " Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Int -> Text forall b a. (Show a, IsString b) => a -> b show Int computed Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> 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 Text -> Text -> Text forall a. Semigroup a => a -> a -> a <> Text ")") -- Clamp a computed public-pool size into its sane band, for the same reasons as -- 'clampPrivateConnections': a floor so a small limit still reuses connections -- across an onboarding burst, a cap so an enormous limit does not retain an absurd -- idle cache to one public origin. clampPublicConnections :: Int -> Int clampPublicConnections :: Int -> Int clampPublicConnections = Int -> Int -> Int forall a. Ord a => a -> a -> a max Int publicConnectionsFloor (Int -> Int) -> (Int -> Int) -> Int -> Int forall b c a. (b -> c) -> (a -> b) -> a -> c . Int -> Int -> Int forall a. Ord a => a -> a -> a min Int publicConnectionsCap -- The public pool takes an eighth of the file-descriptor budget: half the private -- share, because the public leg is the transient onboarding ramp rather than the -- steady-state workhorse, drawn from the reserve the private sizing leaves. publicConnectionsFdShare :: Int = Int 8 publicConnectionsFloor :: Int publicConnectionsFloor :: Int publicConnectionsFloor = Int 32 publicConnectionsCap :: Int publicConnectionsCap :: Int publicConnectionsCap = Int 1024 {- | The depth of the producer-side hand-off buffer the composition root wraps in front of the mirror queue ('Ecluse.Core.Queue.newEnqueueBuffer'). Sized to absorb a cold @npm ci@'s enqueue burst (a lockfile fan-out enqueues one job per public-served tarball) while bounding memory; a job dropped at the cap is re-enqueued on the next demand for its artifact, so overflow costs a deferred mirror, never correctness. -} mirrorEnqueueBufferDepth :: Int mirrorEnqueueBufferDepth :: Int mirrorEnqueueBufferDepth = Int 1024 {- | How many enqueue-buffer drops or delivery failures pass between warning-log reports at the composition root (the first is always reported, then every multiple of this). The buffer's callbacks fire per event so the failure counter stays exact, while a sustained flood logs one line per this many events rather than one per job. -} mirrorEnqueueReportInterval :: Int mirrorEnqueueReportInterval :: Int mirrorEnqueueReportInterval = Int 100 {- | The process soft file-descriptor limit (@RLIMIT_NOFILE@), the datapoint 'resolvePrivateConnections' sizes the private pool against. An __infinite__ or __unknown__ limit falls back to @privateConnectionsCap x privateConnectionsFdShare@, so the computed pool lands at the 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