ecluse
Safe HaskellNone
LanguageGHC2021

Ecluse.Composition.MemoryPlan

Description

The composition root's memory plan: one solver that partitions the effective heap ceiling between named tenants whose sum is bounded by the ceiling, so the budgets compose instead of each claiming an independent share of the same bytes.

With an effective ceiling H (Ecluse.Rts, what the RTS actually runs with), the tenants are, in allocation order:

  1. The runtime reserve: GC copying headroom, stacks, buffers, and the RTS itself, taken off the top; everything else partitions the remainder (the application heap).
  2. Fixed buffers: the mirror enqueue hand-off buffer, charged whenever any mount mirrors (whatever the backend).
  3. The cache aggregate: ONE byte budget for all three metadata stores, split into their named sub-budgets at planCacheConfig (the split sums to the aggregate, so the cache can never triple its tenant).
  4. The material aggregate: response materialisation working space. The admission capacity is max 1 (min A_cpu A_mem) -- the CPU-derived capacity (Ecluse.Composition.Sizing) jointly bounded by what the material share can actually hold, one envelope (packumentOriginFanout concurrent origins, each wire+parsed at expandWireBytes) per admitted operation.
  5. The publish aggregate: the total bytes concurrently buffered publish bodies may hold (the byte-admission the publish pipeline acquires before reading a body), present only when a publication target is configured.
  6. The queue tenant: the in-memory mirror queue's depth, charged only when the memory backend was selected (the selection precedes this plan; an SQS deployment spends no heap on queued jobs).
  7. The mirror-artifact tenant: the transient publish envelope one back-fill job holds, charged whenever any mount mirrors (the same predicate as the fixed buffer). The worker buffers a fetched tarball whole and base64-encodes it into a strict publish document, so the tarball bytes, the base64 Text, and the serialised document coexist before GC at roughly mirrorArtifactEnvelopeMultiplier times the tarball; the worker's per-artifact byte cap (matMaxBytes) is this share divided back down by that multiplier, so the envelope the cap admits is what the tenant charges (issue #846).

The combined invariant -- reserve + cache + material + publish + queue + fixed buffers + mirror-artifact within the ceiling -- is enforced by construction for computed shares. Explicit overrides are re-checked by attribution, described under degradation below: a pin refuses the boot only when it is what a fitting plan cannot shed around, never on mere presence.

Graceful degradation, never a refusal

A pod too small for the tenants' normal floors sheds in a documented priority order, each step a loud boot warning naming what was given up and why: first the mirror-artifact cap shrinks, ultimately to zero (the background back-fill leg gives way before the serve hot path, so a starved pod protects its cache and admission and mirrors nothing it cannot buffer safely); then the cache aggregate shrinks below its floor, ultimately to zero (the proxy serves uncached); then admission shrinks toward one in-flight operation (and, where the nursery is the real pressure, the capability count sheds with it, automating the smaller-cores recommendation); then the publish aggregate shrinks to one maximum request; then the queue depth to its floor. One operation on one capability with no cache is the irreducible minimum and __always boots__ -- if even that exceeds the ceiling, the plan says so in its loudest warning and boots anyway (the cgroup backstop is then the guard).

Only an explicit operator override refuses the boot, and only when it is the cause. The plan re-derives the override-free minimum (every pin substituted out, every computed tenant at its floor) and refuses just when that minimum fits the ceiling while the pinned plan does not, naming the pins whose individual removal would fit (all of them when only their combination overshoots). A pin at or below the value the shed ladder would compute anyway pushes the plan past nothing and never refuses; a pod too small even without the pins boots with the loud warning like any other. An override is an operator claim, and a claim that alone breaks the ceiling is a misconfiguration to fix, not to shrink around.

With no ceiling datapoint at all, every bound falls back to the shipped values that predate the plan. An explicit config value always wins its own bound, and every decision returns a provenance line for the boot log.

Synopsis

Documentation

data MemoryPlan Source #

The resolved plan: every byte-valued bound the composition root builds with, each an explicit config value or its tenant-derived default, plus the degradation warnings the solver took and any explicit-override violations (the one refusal).

Constructors

MemoryPlan 

Fields

Instances

Instances details
Show MemoryPlan Source # 
Instance details

Defined in Ecluse.Composition.MemoryPlan

Eq MemoryPlan Source # 
Instance details

Defined in Ecluse.Composition.MemoryPlan

newtype PublishTenant Source #

The publish tenant: the aggregate byte-admission for concurrently buffered bodies.

Constructors

PublishTenant 

newtype MirrorArtifactTenant Source #

The mirror-artifact tenant: the mirror worker's per-artifact byte cap. Present only when some mount mirrors.

matMaxBytes is the tarball byte cap the worker's bounded fetch enforces (fetchArtifactBytes). The heap the combined invariant charges for it is that cap scaled by mirrorArtifactEnvelopeMultiplier -- what the buffered tarball, its base64 Text, and the serialised publish document may transiently hold together.

Constructors

MirrorArtifactTenant 

Fields

  • matMaxBytes :: Int

    The worker's per-artifact fetch byte cap (the tarball bound B).

data QueueTenantDemand Source #

Whether the memory plan owes the in-memory queue a tenant, projected from the already-made backend selection (planMirrorRuntime): only the memory backend spends heap on queued jobs, while any mirroring at all charges the fixed enqueue buffer.

Constructors

NoQueueTenant

No mount mirrors: no queue tenant, no enqueue buffer.

MirroringWithoutMemoryQueue

Mirroring rides a durable backend: the enqueue buffer alone is charged.

MemoryQueueTenant

Mirroring rides the in-memory queue: its depth is a tenant of this plan.

queueTenantDemand :: MirrorRuntimePlan -> QueueTenantDemand Source #

Project the queue-tenant demand from the resolved mirror runtime plan.

resolveMemoryPlan :: CacheSettings -> LimitsSettings -> QueueSettings -> Maybe Int -> EffectiveRuntimePlan -> QueueTenantDemand -> Bool -> (MemoryPlan, [Text]) Source #

Resolve the memory plan and its boot lines from the configuration groups, the effective runtime plan, the explicit admission override, the queue-tenant demand (the backend selection precedes this plan), and whether any mount publishes.

type OverridePins = (Maybe Int, Maybe Int, Maybe Int, Maybe Int, Maybe Int, Maybe Int) Source #

A hypothetical set of explicit overrides for the memory plan, in allocation order: the cache byte bound, the serve admission, the response byte cap, the request byte cap, the memory-queue depth, and the mirror-artifact byte cap, each pinned (Just) or substituted out (Nothing).

overrideMinShedSum :: Int -> Int -> Bool -> Bool -> Bool -> OverridePins -> Int Source #

The fully-shed minimum tenant sum for a hypothetical set of explicit pins: the pin-independent base (runtime reserve plus fixed buffers) plus each shedable tenant at its pinned value, or un-pinned at the floor the shed ladder would reach -- cache to zero, admission to one operation, response to its floor, the publish aggregate to the computed one-request floor, the queue depth to its floor, and the mirror-artifact envelope to zero (it sheds fully). memoryBacked gates the queue tenant and mirrors the mirror-artifact tenant: a durable or absent backend spends no heap on depth, and a non-mirroring deployment none on the artifact envelope, whatever the pin's value. Comparing this across pin sets attributes a residual overshoot to the pins that cause it.

overrideSubstitutions :: OverridePins -> [(Text, OverridePins)] Source #

Each explicit override present in the pin set, paired with the pin set that substitutes only it out (the value the shed ladder would reach without it) and tagged with the operator's config-key name, in the plan's allocation order. An absent override contributes no substitution.

attributeOverrideViolations :: Int -> Int -> Int -> [(Text, Int)] -> [Text] Source #

Decide the override refusal from the residual overshoots, and name the culprits. Refuse only when the override-free minimum fits the heap ceiling (freeOvershoot is zero) while the pinned plan does not (overriddenOvershoot is positive): the pins are then the cause, and a pod too small even without them is a degradation the shed ladder already warned about, never a refusal. Name the pins whose individual removal makes the plan fit (their one-out overshoot is zero); when none alone flips the verdict (the pins only overshoot in combination), name them all rather than under-blame. The message reports only the overshoot the named pins are responsible for, since the override-free minimum fits within the ceiling.

planCacheConfig :: CacheSettings -> MemoryPlan -> CacheConfig Source #

The metadata cache's tunables: the configured TTL married to the plan's cache aggregate, split into the three stores' named sub-budgets __summing exactly to the aggregate__ (the assembled share is the remainder). The shares: the full-packument store carries the decoded working set at 60%; the single-version store holds small flat entries at 15% but four times the entry count; the assembled store's encoded documents take the remaining 25%. A fully-shed (zero) aggregate yields stores that retain nothing: every value takes the oversized pass-through and the proxy serves uncached.