Mailroom EMFILE resilience#1608
Conversation
payjoin-mailroom binds its listener with SystemOptions::default(), which leaves sleep_on_errors disabled. EMFILE/ENFILE (file descriptor exhaustion) is not a connection error in tokio_listener, so accept() returns it as fatal and the axum08 integration hangs the accept loop forever via std::future::pending(). A momentary descriptor spike then downs the service until it is restarted. Enable sleep_on_errors so the listener backs off and retries instead of treating exhaustion as fatal, letting it recover once descriptors free up.
A public directory and OHTTP relay holds many concurrent file descriptors: inbound connections, long-poll waits, and OHTTP bootstrap tunnels. The systemd default soft limit of 1024 is exhausted under load, making accept() fail with EMFILE. Set the descriptor limit in the example systemd unit, the NixOS module, and the docker-compose ulimits. Use soft:hard form (65536:524288): the 65536 soft limit is the enforced guardrail, well above peak, while the 524288 hard ceiling stays available to raise live (e.g. prlimit) without a redeploy.
Coverage Report for CI Build 27063085831Coverage decreased (-0.006%) to 85.37%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
|
I asked the same questions drafting this
65536 is like 2G which seems reasonable given our actual hardware in the wild, and it's just the length of the leash not somewhere we want to operate at, which is why hardware limits aren't really the issue here. The issue here is a leak where fds accumulate and are never realased. |
benalleng
left a comment
There was a problem hiding this comment.
We could still move the hard limit up if the hard limit is higher in case we come against this wall in the future, giving enough operating margin to fix a problem in prod if it were to come up again afaiu.
In my opinion I think the next lever we should pull is on the integration side with a bucket of directories but we can look into that on the payjoin-cli side.
ACK 609c5b2
|
Bucket of directories doesn't really solve this if directories never give up idle connections. We gotta give up idle connections. more directories is basically just still more buckets till failure rather than emptying buckets when we can. Let's do both. |
Symptoms
mailroom has been intermittently stopped serving requests and logging
errno 24 (EMFILE, "Too many open files").on one host the accept loop hungaccept4() failed (24: Too many open files). on payjo.in the directory's mailbox reads started 500ing because the process has been hitting the file descriptor limit. This PR is a necessary but insufficient change raising that limit, so when there's a blowup it doesn't crash so quickly.Bug
The actual bug seems to be that payjoin-mailroom accepts connections through
tokio_listenerbehindaxum::serveThe one-line fix:
set sleep_on_errors = trueNow EMFILE → sleep 1s → retry accept(). Once connections drain and fds free, accept() succeeds and the server self-heals — no restart. It briefly degrades (new connections wait/refuse during the saturated second) insteadof dying permanently.
This does two things: raise the fd limit and survive hitting it. This does NOT yet address the underlying problem. But this is the fire extinguisher we use first so that we have some time to address the core issues.
Underlying Issue
axum::serveenforces no keep-alive/idle timeout and no connection cap, so idle inbound connections pile up until they exhaust the limit ~1k idle ESTABLISHED inbound sockets, accept queue overflowing. We ship this PR first but the real fix is long-poll-safe timeout + connection caps in a follow-up PR. Multiplexing with h2/h3 between relay<->director hops is how this scales beyond that.Disclosure: co-authored by Claude; reviewed by Codex
Pull Request Checklist
Please confirm the following before requesting review:
AI
in the body of this PR.