docs(arch): stream-client split + total-bytes cap for subscription forwards (FWD-15, FWD-14)
http-adapters.md: forwarding-handler step 5 documents the time/bytes split for Sub forwards (stream client without the total request timeout, total streamed-bytes cap, read timeout as the staleness guard); the HTTP Client section documents the two derived clients (request vs stream), the reqwest 0.13 per-request-override limitation that forces the derived-client design, and that only the total timeout differs between them. http-server.md: the per-endpoint dispatch deadline paragraph now covers the outbound half — unbounded time, bounded bytes on both the gateway and the forwarding side (ADR-049/021 note: unbounded time by design for subscriptions, bounded bytes per subscription). Verified: cargo doc --no-deps clean, full suite green.
This commit is contained in:
@@ -181,14 +181,30 @@ The forwarding handler is stored in the `HandlerRegistration` as a
|
|||||||
wraps it in a `ResponseEnvelope`, returns. Registered as
|
wraps it in a `ResponseEnvelope`, returns. Registered as
|
||||||
`HandlerKind::Once` — a `Handler` returning a single
|
`HandlerKind::Once` — a `Handler` returning a single
|
||||||
`ResponseEnvelope`.
|
`ResponseEnvelope`.
|
||||||
5. For a `Sub` (`text/event-stream` response): streams
|
5. For a `Sub` (`text/event-stream` response): streams
|
||||||
`call.responded` events as the SSE chunks arrive (same SSE parsing as
|
`call.responded` events as the SSE chunks arrive (same SSE parsing as
|
||||||
the TS `parseSSEFrames`), then the stream ends on SSE close (which
|
the TS `parseSSEFrames`), then the stream ends on SSE close (which
|
||||||
becomes `call.completed` on the wire). Registered as
|
becomes `call.completed` on the wire). Registered as
|
||||||
`HandlerKind::Stream` — a `StreamingHandler` returning a
|
`HandlerKind::Stream` — a `StreamingHandler` returning a
|
||||||
`BoxStream<ResponseEnvelope>` (alkcall ADR-021). Each SSE `data:`
|
`BoxStream<ResponseEnvelope>` (alkcall ADR-021). Each SSE `data:`
|
||||||
frame becomes a `ResponseEnvelope::ok()`; an HTTP error (non-2xx)
|
frame becomes a `ResponseEnvelope::ok()`; an HTTP error (non-2xx)
|
||||||
becomes a single `ResponseEnvelope::error()` and ends the stream.
|
becomes a single `ResponseEnvelope::error()` and ends the stream.
|
||||||
|
The streaming send rides the shared HTTP client's
|
||||||
|
**stream client** (see HTTP Client below): the same config minus
|
||||||
|
the total request timeout (FWD-15) — a subscription is unbounded in
|
||||||
|
*time* by contract (alkcall ADR-021 sets `deadline: None`), so the
|
||||||
|
outbound half must not die at the 30 s request deadline the
|
||||||
|
request/response half carries. Time-unbounded does not mean
|
||||||
|
memory-unbounded: the SSE parse loop enforces a **total
|
||||||
|
streamed-bytes cap** per subscription
|
||||||
|
(`HttpClientConfig.stream_total_byte_cap`, default 1 GiB),
|
||||||
|
accumulated across every chunk fed to the parser (FWD-14);
|
||||||
|
exceeding it terminates the stream with a single terminal error
|
||||||
|
envelope (`HTTP_413`), the same stream-ends semantics as the other
|
||||||
|
terminal arms. The bounded-time companion is the client's read
|
||||||
|
timeout, which stays armed on the stream client: it bounds
|
||||||
|
upstream *staleness* (no byte for 30 s → terminal), not stream
|
||||||
|
lifetime.
|
||||||
6. On HTTP error (non-2xx): maps to the declared `ErrorDefinition` by
|
6. On HTTP error (non-2xx): maps to the declared `ErrorDefinition` by
|
||||||
HTTP status code (see Error Fidelity below), returns a `CallError`.
|
HTTP status code (see Error Fidelity below), returns a `CallError`.
|
||||||
|
|
||||||
@@ -205,6 +221,29 @@ connection pooling, keep-alive, TLS, and a retry stack. The shared type is
|
|||||||
both retry and Retry-After are middleware on the stack, and middleware
|
both retry and Retry-After are middleware on the stack, and middleware
|
||||||
requires the `ClientWithMiddleware` wrapper.
|
requires the `ClientWithMiddleware` wrapper.
|
||||||
|
|
||||||
|
The shared `SharedHttpClient` exposes **two derived clients** from one
|
||||||
|
config, rebuilt-and-swapped together (the same atomicity rule as the
|
||||||
|
config itself):
|
||||||
|
|
||||||
|
- **Request client** (`SharedHttpClient::client`) — carries the total
|
||||||
|
request timeout (default 30 s, anchored to the gateway's Once-op
|
||||||
|
deadline). Every request/response forward (`from_openapi`/
|
||||||
|
`from_jsonschema` Queries and Mutations) sends through it.
|
||||||
|
- **Stream client** (`SharedHttpClient::stream_client`) — built from
|
||||||
|
the same config with the total request timeout removed and the
|
||||||
|
connect + read timeouts retained (FWD-15). Streaming (SSE)
|
||||||
|
subscription forwards send through it. reqwest 0.13's per-request
|
||||||
|
timeout override can lengthen a client-level total timeout but never
|
||||||
|
clear it (the request-scoped `None` falls back to the client
|
||||||
|
default), so a timeout-free subscription send requires the derived
|
||||||
|
client rather than a request extension. Unbounded *time* per
|
||||||
|
subscription is the contract (alkcall ADR-021); the complementary
|
||||||
|
bounds are the read timeout (staleness) and the total streamed-bytes
|
||||||
|
cap (see the forwarding-handler step 5 above).
|
||||||
|
|
||||||
|
Both clients carry the same middleware stack, redirect policy, and TLS
|
||||||
|
trust — the total request timeout is the only delta between them.
|
||||||
|
|
||||||
The middleware stack has two layers:
|
The middleware stack has two layers:
|
||||||
|
|
||||||
1. **`RetryTransientMiddleware`** (from `reqwest-retry`) — exponential
|
1. **`RetryTransientMiddleware`** (from `reqwest-retry`) — exponential
|
||||||
|
|||||||
@@ -385,7 +385,16 @@ registry invoke in `tokio::time::timeout`); a hung handler surfaces as
|
|||||||
a `TIMEOUT` error (`504`, `retryable: true`), not an indefinitely-held
|
a `TIMEOUT` error (`504`, `retryable: true`), not an indefinitely-held
|
||||||
HTTP request. Streaming invokes (`/subscribe`) are unbounded —
|
HTTP request. Streaming invokes (`/subscribe`) are unbounded —
|
||||||
subscriptions are long-lived by contract (alkcall ADR-021 sets
|
subscriptions are long-lived by contract (alkcall ADR-021 sets
|
||||||
`deadline: None` for the streaming branch).
|
`deadline: None` for the streaming branch). The same time/bytes split
|
||||||
|
governs the **outbound** half of an imported subscription:
|
||||||
|
`from_openapi`/`from_jsonschema` SSE forwards send through the shared
|
||||||
|
HTTP client's stream client, built without the total request timeout
|
||||||
|
(FWD-15) so a healthy subscription is not killed at the 30 s outbound
|
||||||
|
deadline, while a total streamed-bytes cap
|
||||||
|
(`HttpClientConfig::stream_total_byte_cap`, default 1 GiB) bounds the
|
||||||
|
bytes a single subscription may pull (FWD-14) and the client's read
|
||||||
|
timeout remains the upstream-staleness guard. Unbounded time, bounded
|
||||||
|
bytes — on both the gateway and the forwarding side.
|
||||||
|
|
||||||
### `/healthz` (raw route)
|
### `/healthz` (raw route)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user