You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sub-issue of #141, which is the umbrella contract. Authority: ADR 0017
(#116). Where this issue and the ADR disagree, the ADR wins.
Depends on: nothing. This is the foundation for 141-B, 141-C and 141-D, which are independent of
each other and may proceed in any order once this lands.
Scope
The resilience policy object, the admission primitive, and HTTP proxy integration. Nothing else.
Public configuration
Pool-scoped only; setting any of these in a location that targets a named upstream is a validation
error.
[upstreams.resilience]
max_active_requests = 1000# 0 = unlimited (default)max_active_per_backend = 0# 0 = unlimited (default)max_pending_requests = 0# 0 = NO QUEUE, reject immediately (default)pending_timeout = "0s"# 0 = bounded only by the request context
max_pending_requests = 0 means no queue, not unlimited — "unlimited pending" is the unbounded-memory
failure this exists to prevent, so it must be unrepresentable.
Deliverables
resilience.Policy — immutable, resolved once at load time, mirroring backendtls.Policy. Held on
the live Pool in an atomic.Pointer. Must not enter upstreamMeta. The hot path does one pointer
load and reads pre-parsed scalars: no tree traversal, no duration parsing, no allocation.
admission — in internal/upstream, a standalone type with no *Pool dependency. This is
an import-graph commitment: internal/stream, internal/handler and later internal/auth must all
reach it without importing a sibling.
atomic.Int64 CAS fast path while active < limit; no lock is taken below the limit.
Slow path under mu: reject if the FIFO is full, else append a waiter and select on the waiter
channel, the request context, and pending_timeout.
Release with direct handoff: if a waiter exists andactive-1 < limit, hand the slot to the
FIFO head without decrementing. Strict FIFO, no barging, monotonic recovery after a limit decrease.
Zero new goroutines. The parked goroutine is the inbound net/http goroutine.
Admit returns a release closure guarded by sync.Once, capturing the exact counter it
incremented. No generation tags.
proxyHandler.ServeHTTP — a new method; the type currently promotes ServeHTTP from the embedded *httputil.ReverseProxy. Admission is innermost, so a cache hit or a WAF-blocked request never
consumes a slot, and background cache revalidation correctly does.
Per-backend limit as a selection filter, not a second queue: a backend at max_active_per_backend
is not eligible in pickExcluding. Nested admission is a deadlock generator.
Counting channel (capacity is fixed at creation, and the limit is hot-reloadable); x/sync/semaphore (no resize, unbounded waiter list); sync.Cond (cannot select on ctx.Done()); a
custom lock-free ring (no contention evidence); a timing wheel (live timers are bounded by max_pending_requests).
Acceptance criteria
Exactly-once acquire/release across success, transport failure, retry, cancellation, timeout, queue
cancellation, body close, 101 upgrade and panic.
Forced generation retirement wakes and rejects parked waiters. A parked request holds a handlerGen in-flight reference so the generation cannot retire gracefully, but retirement is also
bounded by a forced grace timeout after which the transport is closed — without an explicit wakeup a
parked request could be admitted onto a closed transport. pending_timeout is validated against the
retirement grace.
Reload 1000 → 100 with 500 active: the 500 complete (admission is an entry control), new arrivals
queue or get proxy_overloaded, recovery is monotonic.
Validation errors: stateful control in a location targeting a named upstream; max_pending_requests > 0 while max_active_requests = 0.
Validation warnings: max_active_per_backend * len(servers) < max_active_requests (static lists
only — the pool limit is then unreachable while the queue sits empty); max_pending_requests > 0 && pending_timeout == 0.
Defaults reproduce current behaviour exactly.
Lifecycle registry, generated mirrors, docs and changelog for what this slice ships.
Invariants
0 <= Q_p <= max_pending_requests
0 <= A_p <= max_active_requests + delta (delta >= 0 only after a decrease, non-increasing)
sum(A_b) <= k * A_p (k = 1: a request holds pool admission during backoff
and selection, but a backend slot only during an attempt)
Required tests
Exact cap boundaries; queue full; cancel; deadline; shutdown.
-race acquire/release/cancel against concurrent policy swaps; assert active == 0 && pending == 0
at quiesce and delta non-increasing.
The handoff-versus-cancel race, run repeatedly: a waiter granted a slot as it cancels must consume
and release the grant, never leak it.
Forced-retirement waiter wakeup.
Reload 1000 → 100 asserted at every drain step.
FIFO ordering and starvation freedom.
Non-goals for this slice
Connection bounds and gRPC (141-B); FastCGI/uWSGI and Backend.Network (141-C); L4 (141-D);
cross-protocol soak and benchmarks (141-E); retry (#142); circuit (#143).
Note
Sub-issue of #141, which is the umbrella contract. Authority:
ADR 0017
(#116). Where this issue and the ADR disagree, the ADR wins.
Depends on: nothing. This is the foundation for 141-B, 141-C and 141-D, which are independent of
each other and may proceed in any order once this lands.
Scope
The resilience policy object, the admission primitive, and HTTP proxy integration. Nothing else.
Public configuration
Pool-scoped only; setting any of these in a location that targets a named upstream is a validation
error.
max_pending_requests = 0means no queue, not unlimited — "unlimited pending" is the unbounded-memoryfailure this exists to prevent, so it must be unrepresentable.
Deliverables
resilience.Policy— immutable, resolved once at load time, mirroringbackendtls.Policy. Held onthe live
Poolin anatomic.Pointer. Must not enterupstreamMeta. The hot path does one pointerload and reads pre-parsed scalars: no tree traversal, no duration parsing, no allocation.
admission— ininternal/upstream, a standalone type with no*Pooldependency. This isan import-graph commitment:
internal/stream,internal/handlerand laterinternal/authmust allreach it without importing a sibling.
atomic.Int64CAS fast path whileactive < limit; no lock is taken below the limit.mu: reject if the FIFO is full, else append a waiter andselecton the waiterchannel, the request context, and
pending_timeout.active-1 < limit, hand the slot to theFIFO head without decrementing. Strict FIFO, no barging, monotonic recovery after a limit decrease.
net/httpgoroutine.Admitreturns areleaseclosure guarded bysync.Once, capturing the exact counter itincremented. No generation tags.
proxyHandler.ServeHTTP— a new method; the type currently promotesServeHTTPfrom the embedded*httputil.ReverseProxy. Admission is innermost, so a cache hit or a WAF-blocked request neverconsumes a slot, and background cache revalidation correctly does.
max_active_per_backendis not eligible in
pickExcluding. Nested admission is a deadlock generator.[HR-00] Make lifecycle classification granular and correct proven misclassifications #89's closed-world authority an unregistered path fails closed, so this ships here, not in 141-E.
Rejected implementations (do not reintroduce)
Counting channel (capacity is fixed at creation, and the limit is hot-reloadable);
x/sync/semaphore(no resize, unbounded waiter list);sync.Cond(cannot select onctx.Done()); acustom lock-free ring (no contention evidence); a timing wheel (live timers are bounded by
max_pending_requests).Acceptance criteria
cancellation, body close, 101 upgrade and panic.
handlerGenin-flight reference so the generation cannot retire gracefully, but retirement is alsobounded by a forced grace timeout after which the transport is closed — without an explicit wakeup a
parked request could be admitted onto a closed transport.
pending_timeoutis validated against theretirement grace.
queue or get
proxy_overloaded, recovery is monotonic.max_pending_requests > 0whilemax_active_requests = 0.max_active_per_backend * len(servers) < max_active_requests(static listsonly — the pool limit is then unreachable while the queue sits empty);
max_pending_requests > 0 && pending_timeout == 0.Invariants
Required tests
-raceacquire/release/cancel against concurrent policy swaps; assertactive == 0 && pending == 0at quiesce and
deltanon-increasing.and release the grant, never leak it.
Non-goals for this slice
Connection bounds and gRPC (141-B); FastCGI/uWSGI and
Backend.Network(141-C); L4 (141-D);cross-protocol soak and benchmarks (141-E); retry (#142); circuit (#143).
Completion evidence