Skip to content

[RES-01-B] Connection bounds and HTTP/2, HTTP/3, WebSocket and gRPC lifetime accounting #284

Description

@victornife

Note

Sub-issue of #141. Authority:
ADR 0017
(#116).

Depends on: #283 (141-A) only. Independent of 141-C and 141-D — these three may proceed in parallel.

Scope

Physical connection bounds, plus admission integration and lifetime accounting for every remaining
in-tree protocol.

Public configuration

Stateless, so settable at pool or location level (location wins) — transports are already built per
location by newProxyTransport(loc, policy).

max_connections_per_backend = 0   # 0 = unlimited; HTTP/1.1 effective

The name is a one-way door and was decided deliberately. max_connections was rejected: Go can only
enforce per host per transport, and [rate_limit] max_conns already denotes an inbound per-listener
cap. Two similar names pointing in opposite directions is the trap ADR 0016 avoided by keeping tls and
backend_tls distinct.

Deliverables

  1. max_connections_per_backendhttp.Transport.MaxConnsPerHost — the only lever Go offers that
    bounds sockets without defeating pooling and that honours the request context while queueing for a dial.
    • Not applicable to native gRPC or transcoding, where one HTTP/2 connection carries all streams;
      validation warns when set on such a route.
    • Health-check clients are exempt, so a saturated pool can still observe recovery.
    • Idle connections count toward the limit until IdleConnTimeout.
  2. Native gRPC and transcoding admissioninternal/handler/grpcproxy.go and internal/transcode
    have their own handler entry points and each needs the Admit call from 141-A.
  3. Lifetime accounting verified across protocols, using the existing release discipline rather than
    new mechanisms.

Accounting matrix (normative)

Client protocol A_p / A_b Backend C_b Which limit binds
HTTP/1.1, no keep-alive +1 per request +1 per request both, in lockstep
HTTP/1.1 keep-alive +1 per request conns reused; idle conns count until IdleConnTimeout either; C_b can bind while A_b is low
HTTP/2 +1 per stream typically 1 request limit; connection limit rarely binds
HTTP/3 +1 per stream unaffected — the backend leg is still HTTP/1.1 or h2 request limit only
WebSocket / 101 +1 until releaseRWBody.Close() +1 dedicated, hijacked, never pooled both
Server-sent events +1 for the response lifetime as HTTP/1.1 request limit
gRPC unary +1 per call shared h2 conn request limit
gRPC server/client/bidi streaming +1 for the whole stream lifetime shared request limit
gRPC transcoding, unary and streaming +1 per call/stream shared request limit

HTTP/3 requires no protocol-specific code. Jul's HTTP/3 support is inbound only — internal/server/http3.go
runs a quic-go http3.Server, and there is no HTTP/3 backend transport. HTTP/3 requests are served by the
same handler tree via acquireGen() and upstream.WithSnapshot, so admission applies unchanged.
h3Conn.Close(ctx) does GOAWAY and drain bounded by shutdown_timeout, so a long-lived h3 stream holds
its slot and its handlerGen reference exactly as an h2 stream does. WebSocket over HTTP/3 is not
applicable
— h3 uses extended CONNECT, which Jul does not implement.

The operator-facing sentence for the docs: max_active_requests counts requests and streams;
max_connections_per_backend counts sockets. Under HTTP/2, HTTP/3 and gRPC one socket carries many
streams, so the request limit normally binds first; under HTTP/1.1 and WebSocket the two move together.

Acceptance criteria

  • MaxConnsPerHost wired from the resolved policy; warns on gRPC and transcoding routes; health-check
    clients exempt.
  • Native gRPC and transcoding acquire and release admission on every terminal path, including
    half-open and cancelled streams.
  • Every row of the accounting matrix is asserted by a test.
  • A long-lived stream holds its slot for its full lifetime and releases exactly once.
  • Lifecycle registry entry for max_connections_per_backend, docs for the accounting matrix.

Required tests

  • Per-protocol accounting: HTTP/1.1 keep-alive, HTTP/2 multi-stream, HTTP/3, WebSocket lifetime, SSE,
    gRPC unary and all three streaming shapes, transcoding.
  • Client disconnect and panic release the slot on each protocol.
  • A MaxConnsPerHost integration test gating the documented semantics, covering interaction with
    MaxIdleConnsPerHost: 32 under keep-alive churn. The stdlib interaction is asserted from documentation
    today, not measured — if it degrades pooling, the field's meaning changes and the name is already fixed.

Non-goals for this slice

FastCGI/uWSGI (141-C); L4 (141-D); cross-protocol soak and BenchmarkAdmit_* (141-E).

Completion evidence

- PR:
- MaxConnsPerHost integration test:
- Per-protocol accounting matrix tests:
- Docs (accounting matrix + protocol applicability):

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions