ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Server.Admission.Weighted

Description

The shared brief-wait admission core: a weighted door/wait/shed machine both serve admission (Ecluse.Core.Server.Admission) and byte-weighted publish admission (Ecluse.Core.Server.Admission.Bytes) are built from. The unit-slot version is this core at weight one with the room equal to the capacity.

A handle caps the aggregate weight concurrently held and retains a __bounded room of waiters__: an acquisition takes its weight immediately, waits briefly for room, or is refused. This bounds aggregate residency by construction while absorbing a burst that merely brushes the capacity, so near-capacity load degrades into short queueing delay rather than a refusal the client immediately retries. Refusal is reserved for genuine overload: a waiting room already at its bound (the deep-overflow band, refused instantly and cheaply) or a wait that outlives its budget.

Instant shedding is self-amplifying under a hammering client: each refusal is answered in microseconds, so the client comes straight back, and the refusal work itself competes for the cores the admitted work needs. Waiting in-process is a blocked green thread -- nearly free -- and every release goes to work that has already arrived. The wait budget (admissionWaitMicros) equals the shed path's Retry-After: 1 hint, so a request is never refused faster than the client would have been told to come back.

Two fairness properties, one deliberate limit:

  • A newcomer never jumps a non-empty waiting room: capacity is taken directly only when no one is waiting, so arrival order is respected between the room and the door.
  • Within the room, wake-up order is not FIFO (STM retry semantics: all waiters race, first commit wins). With the room bounded and turnover far faster than the budget, starvation is not a practical concern, and strict ticketing is complexity this surface has not earned.

Held weight is released across normal completion, failure, and asynchronous cancellation. The waits run masked: a blocked STM retry remains interruptible (a cancellation lands and aborts the transaction, taking nothing), while a committed acquire returns with exceptions still masked, so weight can never be lost between acquisition and the protected run. Release publishes the in-flight gauge decrement before returning capacity to the door, so a newly admitted request cannot make the observable gauge transiently exceed the configured bound; capacity is still returned if that observer throws.

The two instances differ only in their construction policy (the serve handle errors on a non-positive capacity, the byte handle clamps to one byte and clamps each call's weight to the capacity) and in the observer callbacks they supply; the door discipline lives here so a fix to the slot-leak-prone reasoning is made once for both.

Synopsis

Documentation

data WeightedAdmission Source #

The bounded handle's mutable state and its tuning. The available weight and the waiter count are the two TVars the door transaction races over; the room bound and wait budget are fixed at construction. The constructor is hidden so only the checked acquire/wait/release operations can mutate it.

newWeightedAdmission :: Int -> Int -> Int -> IO WeightedAdmission Source #

Allocate a bounded handle over the given capacity, a waiter-room bound, and a wait budget (microseconds). The capacity is taken verbatim; the constructor's caller (the serve or byte wrapper) owns the positive-capacity policy. The room and budget are floored at zero, so a room of zero reproduces pure acquire-or-refuse admission.

withWeightedAdmission :: MonadUnliftIO m => AdmissionObservers -> WeightedAdmission -> Int -> m a -> m (Maybe a) Source #

Run an action holding the given weight against the aggregate. Nothing means the request was shed -- the room was full, or the weight did not fit within the wait budget -- and the caller should refuse it. The weight is used as given; a per-instance clamp is the wrapper's responsibility. Held weight is released on every exit path: normal completion, a synchronous throw, and asynchronous cancellation.

The masked entry decides a Gate and dispatches: shedRecording refuses, admittedRun is the release-protected run, and queuedWait is the brief wait for room before that run. Each arm's mask and finally reasoning sits with its own code rather than woven through one expression.

Marked INLINE so each wrapper's literal AdmissionObservers is eliminated at its call site (case-of-known-constructor), leaving the same code the two hand-written twins compiled to: the extraction is allocation-neutral on the admitted hot path. The arm helpers are INLINE too, so the whole chain folds back into that saturated call and the observers record never survives to be allocated.

data AdmissionObservers Source #

The metric hooks the door/wait/release bracket calls, so the shared machine owns no telemetry vocabulary of its own and each instance records under its own signals.

Constructors

AdmissionObservers 

Fields

  • onQueued :: IO ()

    A request that had to wait cleared the wait and is now admitted. Serve admission records its queued metric here; byte admission does nothing.

  • onShed :: IO ()

    The request was shed: refused at a full door, or its wait outlived the budget. Byte admission records its shed metric here; serve admission is silent.

  • onInFlightDelta :: Int -> IO ()

    Move the in-flight gauge by the signed weight: +weight on admission, -weight on release. Both calls run under the acquire mask, so the gauge is paired on every path.

admissionWaitMicros :: Int Source #

The wait budget (microseconds) an acquisition finding the capacity busy waits before it is shed: deliberately equal to the shed path's Retry-After: 1 hint, so a refusal only ever reaches a client that has already waited one full retry interval in-process, where the wait is a blocked green thread instead of a wire round trip.