| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
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
- data WeightedAdmission
- newWeightedAdmission :: Int -> Int -> Int -> IO WeightedAdmission
- withWeightedAdmission :: MonadUnliftIO m => AdmissionObservers -> WeightedAdmission -> Int -> m a -> m (Maybe a)
- data AdmissionObservers = AdmissionObservers {}
- admissionWaitMicros :: Int
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
| |
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.