body-drain: add -role server|client|both and -header-timeout

Lets each side run out of fds in its own process; document ulimit -Hn
(Go raises soft limit to hard) and Linux hang vs macOS RST difference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbnJj9ZqEx9nUvzkjdwfMq
This commit is contained in:
2026-08-28 23:55:05 +02:00
co-authored by Claude Fable 5
parent 8dd8381b8c
commit 57349285b4
2 changed files with 197 additions and 56 deletions
+38 -1
View File
@@ -6,9 +6,46 @@ properly drain+close response bodies so connections are reused").
```
go run ./99-experiments/body-drain -mode all
go run ./99-experiments/body-drain -mode leak -n 200
ulimit -n 64 && go run ./99-experiments/body-drain -mode leak -n 200 # watch it die
```
## Two processes: watch each side run out of fds on its own
In one process the client and server share one fd limit and each leaked
request costs *two* fds (client socket + accepted server socket). The server
usually loses first — `accept` fails with `too many open files`, its accept
loop retries with backoff forever, and the client silently hangs waiting for
headers on a connection nobody will ever accept. Realistic, but confusing. So
split them:
```
# terminal 1
go run ./99-experiments/body-drain -role server
# terminal 2 — client is the one out of fds
ulimit -Hn 64; ulimit -Sn 64
go run ./99-experiments/body-drain -role client -mode leak -n 200
# request 60 failed: dial tcp 127.0.0.1:8099: socket: too many open files
```
Now swap: limit the *server* terminal instead. It logs
`http: Accept error: accept tcp ...: too many open files; retrying in 5ms`,
while the client (unlimited) either hangs (Linux — pass `-header-timeout 3s`
to turn that into `timeout awaiting response headers`) or gets
`read: connection reset by peer` (macOS).
Which error string you see tells you which side is exhausted:
| error | who ran out |
|---|---|
| `dial tcp ...: socket: too many open files` | the client (this process) |
| `http: Accept error: accept ...: too many open files` | the server |
| `timeout awaiting response headers` / `connection reset by peer` | the client is fine; the *server* can't accept |
Note on `ulimit`: since Go 1.19 the runtime raises the soft `RLIMIT_NOFILE`
to the hard limit at startup, so lowering only the soft limit does nothing.
bash's plain `ulimit -n 64` sets both; zsh's sets only the soft one, hence
the explicit `-Hn`.
## What it does
A local `httptest.Server` returns a 1 MiB body and counts accepted TCP