diff --git a/src/content/docs/configuration.md b/src/content/docs/configuration.md index 02979b2..f89fc41 100644 --- a/src/content/docs/configuration.md +++ b/src/content/docs/configuration.md @@ -205,7 +205,7 @@ inside their H1 header read loop (`celeris/config.go` (`Config.ReadHeaderTimeout An SSE stream is a **detached** stream (the middleware calls [`Context.Detach`](/docs/streaming#detach)), and a detached stream is not subject to -`ReadTimeout` or `WriteTimeout` on any engine +`ReadTimeout` or `WriteTimeout` as a whole on any engine ([celeris#855](https://github.com/goceleris/celeris/issues/855)). You do not need `WriteTimeout: -1` for SSE, and a server-wide `WriteTimeout` of 30s or 60s does not end a stream: @@ -213,18 +213,17 @@ end a stream: | Engine | What a detached stream (SSE) sees | | ------ | --------------------------------- | | `std` | `net/http` arms one write deadline for the whole response, and `Detach` clears it for that request. Before celeris#855 it stayed armed and cut the stream at `WriteTimeout`, over HTTP/1.1 (the next `Send` failed) and over h2c (the stream was reset with `INTERNAL_ERROR`). `ReadTimeout` never cut a stream on Go 1.27: `net/http` has stopped it by the time the handler runs. In its place each write of the detached stream (a `Write` or `Flush`) is bounded by `WriteTimeout`: one that has not finished within it fails. For an SSE event, which is small, that means the client has stopped reading; a stream that is idle between events is not cut, and `-1` sets no such bound. | -| `epoll`, `io_uring`, `adaptive` | Neither timeout is applied to a detached connection. | +| `epoll`, `io_uring`, `adaptive` | `ReadTimeout` and `IdleTimeout` are not applied to a detached connection. `WriteTimeout` is, as "no progress for `WriteTimeout`": a detached connection with bytes queued for its peer, which takes none of them for `WriteTimeout`, is closed (the stream's context ends and `OnDisconnect` runs), whatever the middleware's idle deadline or the last read of the connection says. A peer that reads, however slowly (there is no minimum rate), and a connection with nothing queued are not closed; `-1` sets no such bound. The close comes between about 3/4 and 5/4 of `WriteTimeout` after the peer last took a byte (its progress is sampled every quarter of `WriteTimeout`; a send completion restarts the clock at once), plus the engine's sweep interval (up to about 0.1 s on `epoll`, up to 2 s on `io_uring`; a `WriteTimeout` shorter than that can double) ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). | What bounds a detached stream is what its owner sets: the handler returning, the -client closing the connection, a shutdown. On `std` a client that stays connected but -stops reading ends its stream too: a write that has not finished within -`WriteTimeout` fails (the handler, or the queue's drain goroutine, is parked in it, so -returning or `ClientPolicyClose` cannot end the stream sooner), and when +client closing the connection, a shutdown. A client that stays connected but stops +reading ends its stream too, on HTTP/1.1 on every engine (an h2c stream is not covered yet). On `std` a write that has not finished +within `WriteTimeout` fails (the handler, or the queue's drain goroutine, is parked in +it, so returning or `ClientPolicyClose` cannot end the stream sooner), and when `ShutdownTimeout` runs out the writes of the streams still running fail. On the native -engines `Send` was measured not to wait for a client that reads nothing, so the handler -returning ends the stream, and a shutdown does; neither `WriteTimeout` nor -`ClientPolicyClose` ends the connection of such a client yet (known gap, celeris#884 and -celeris#885). +engines `Send` does not wait for a client that reads nothing, so there it is the engine +that closes the connection once the client has taken none of the bytes queued for it for +`WriteTimeout`. A response that is **not** detached is still under `WriteTimeout`: a slow large download, or a chunked body written with `StreamWriter` without `Detach`. On `std` the @@ -666,5 +665,6 @@ So Celeris can tell "unset" (infer from `Protocol`) apart from an explicit - [Routing](/docs/routing) — per-route `Async` / `Sync` / `UsesDriver` overrides of `AsyncHandlers`. - [Streaming](/docs/streaming) and [SSE](/docs/sse) — long-lived responses: a detached - stream such as SSE is not subject to `ReadTimeout` or `WriteTimeout`; other - long-lived responses are. + stream such as SSE is not subject to `ReadTimeout` or `WriteTimeout` as a whole (a + client that reads nothing of it is closed after `WriteTimeout`); other long-lived + responses are. diff --git a/src/content/docs/sse.md b/src/content/docs/sse.md index 735316a..45f5266 100644 --- a/src/content/docs/sse.md +++ b/src/content/docs/sse.md @@ -564,18 +564,35 @@ why the same `sse.Config` runs unchanged on all engines. Source: ### Server timeouts An SSE stream is detached, so the server's `ReadTimeout` and `WriteTimeout` do not end -it on any engine: leave them at their defaults and do not set `WriteTimeout: -1` for +it as a whole on any engine: leave them at their defaults and do not set `WriteTimeout: -1` for SSE. On `std` the `Detach` clears the write deadline `net/http` armed for the request (before [celeris#855](https://github.com/goceleris/celeris/issues/855) it cut the stream -at `WriteTimeout`; `ReadTimeout` never did); the native engines apply neither to a -detached connection. On `std` a write of the stream that has not finished within -`WriteTimeout` fails (no bound at `-1`), so a client that stays connected and reads nothing -ends its stream after one `WriteTimeout` (the handler is parked in the write until -then), and a shutdown whose budget runs out fails the writes of the streams still -running. On the native engines `Send` does not wait for such a client; the handler -returning or a shutdown ends the stream, `WriteTimeout` and `ClientPolicyClose` do not -yet (celeris#884, celeris#885). A stream is ended by its handler returning, the client -closing the connection, or a shutdown. See [Configuration](/docs/configuration#sse-needs-no--1-other-long-lived-responses-do). +at `WriteTimeout`; `ReadTimeout` never did). A client that stays connected and stops +reading is dealt with per engine: + +- **`std`**: a write of the stream that has not finished within `WriteTimeout` fails (no + bound at `-1`), so the client's stream ends after one `WriteTimeout` (the handler is + parked in the write until then), and a shutdown whose budget runs out fails the writes + of the streams still running. +- **`epoll`, `io_uring`, `adaptive`**: `Send` does not wait for such a client (the write + only queues the event in the engine), so a write error is not what ends its stream, and + `ClientPolicyClose` ends it only earlier, when the queue overflows (a drain goroutine + that lags behind the producer). `WriteTimeout` is the bound when it does not, as "no + progress for `WriteTimeout`": a client + that takes none of the bytes the engine has queued for it for `WriteTimeout` is + disconnected, its context is cancelled and `OnDisconnect` runs + ([celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). A client + that keeps reading, however slowly (there is no minimum rate), is not disconnected, and neither is one with nothing + queued for it; `-1` sets no such bound. The disconnect comes between about 3/4 + and 5/4 of `WriteTimeout` after the client last took a byte (its progress is sampled every + quarter of `WriteTimeout`; a send completion restarts the clock at once), plus the + engine's sweep interval (up to about 0.1 s on `epoll`, up to 2 s on `io_uring`; a + `WriteTimeout` shorter than that can double). The handler returning or a shutdown ends the + stream sooner. + +A stream is ended by its handler returning, the client closing the connection, a shutdown, +or a client that stops reading (HTTP/1.1 on every engine; an h2c stream is not covered yet). See +[Configuration](/docs/configuration#sse-needs-no--1-other-long-lived-responses-do). ## Common pitfalls diff --git a/src/content/docs/streaming.md b/src/content/docs/streaming.md index 17700bc..2c2e97f 100644 --- a/src/content/docs/streaming.md +++ b/src/content/docs/streaming.md @@ -188,13 +188,16 @@ returns a `done func()`. Source: `celeris/context_response.go` (`Context.Detach` After `Detach`, the framework will not recycle the `Context` when the handler returns; the returned `done` is what releases it. -A detached stream is also exempt from the server's `ReadTimeout` and `WriteTimeout` on -every engine ([celeris#855](https://github.com/goceleris/celeris/issues/855)): `epoll`, -`io_uring` and `adaptive` apply neither to a detached connection, and on `std` `Detach` -clears the write deadline `net/http` armed for the request and bounds each write of the -stream by `WriteTimeout` instead (a write that has not finished within `WriteTimeout` -fails, so a client that reads nothing ends the stream; a shutdown whose budget runs out -fails the writes). An undetached +A detached stream is also exempt from the server's `ReadTimeout` and `WriteTimeout` as a +whole on every engine ([celeris#855](https://github.com/goceleris/celeris/issues/855)): +`epoll`, `io_uring` and `adaptive` apply `ReadTimeout` and `IdleTimeout` to a detached +connection not at all, and `WriteTimeout` as "no progress for `WriteTimeout`" (a connection +with bytes queued for a peer that takes none of them for `WriteTimeout` is closed; +[celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39), and on +`std` `Detach` clears the write deadline `net/http` armed for the request and bounds each +write of the stream by `WriteTimeout` instead (a write that has not finished within +`WriteTimeout` fails, so a client that reads nothing ends the stream; a shutdown whose +budget runs out fails the writes). An undetached `StreamWriter` is still a response with a `WriteTimeout`. See [Configuration](/docs/configuration#sse-needs-no--1-other-long-lived-responses-do). diff --git a/src/content/docs/websocket.md b/src/content/docs/websocket.md index b96fed9..9d4241b 100644 --- a/src/content/docs/websocket.md +++ b/src/content/docs/websocket.md @@ -710,6 +710,7 @@ worth knowing (`celeris/middleware/websocket/doc.go`): | `IdleTimeout` | Enforced via the engine's idle sweep. | Enforced via `net.Conn.SetReadDeadline`. | | Backpressure | Engine-integrated (TCP receive window); see [Backpressure](#backpressure-native-engines). | Handled by the kernel TCP stack. | | `WriteBufferPool` | Ignored (engine pools its own write buffer). | Consulted. | +| Peer that reads nothing | The server's `WriteTimeout` closes the connection once the peer has taken none of the bytes queued for it for `WriteTimeout` ("no progress", whatever the peer sends meanwhile; `-1` sets no such bound; [celeris#888](https://github.com/goceleris/celeris/issues/888) items 38 and 39). | Not changed by celeris#888. | The practical takeaways: