Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions src/content/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,26 +205,25 @@ 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:

| 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
Expand Down Expand Up @@ -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.
37 changes: 27 additions & 10 deletions src/content/docs/sse.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 10 additions & 7 deletions src/content/docs/streaming.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
1 change: 1 addition & 0 deletions src/content/docs/websocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading