fix(daemon): raise proxy daemon file-descriptor limit at startup#439
Conversation
The proxy daemon is the single shared egress point for every run. It inherited the host's default RLIMIT_NOFILE soft limit (typically 256 on macOS), so a burst of concurrent connections through the one proxy — e.g. `bun install` opens up to 64 parallel connections — could exhaust its file descriptors and stall every run's traffic until the burst cleared. This surfaced as all Claude sessions inside moat freezing at once on macOS during a package install, then recovering gracefully. Raise RLIMIT_NOFILE toward 65536 (capped at the hard limit) at daemon startup, before the proxy accepts connections. Cross-platform via x/sys/unix with a macOS step-down fallback (macOS rejects a soft limit above kern.maxfilesperproc even when the hard limit is higher). Best-effort: a failure logs a warning and the daemon still runs.
📦 Previous Review (superseded by newer review)Claude finished @dpup's task in 3m 57s —— View job Reviewing this PR
SummarySolid, well-scoped fix. The root-cause analysis (single shared proxy, Verified locally:
Notes (non-blocking)
No blocking issues found. This looks ready to merge. |
📦 Previous Review (superseded by newer review)Claude finished @dpup's task in 3m 48s —— View job Reviewing this PR
ReviewVerified: The fix is well-targeted: raising One logic edge case worth a look, not blocking:
for t := target / 2; t > oldSoft; t /= 2 {starts at Everything else — the macOS step-down rationale, the hard-limit cap, the idempotency test ( |
…mits Address review feedback on #439. The step-down fallback started at target/2, so if the process already had a soft limit above half the target and the full target was rejected, the loop body never ran and no intermediate value was probed (leaving the limit unraised even when a value between oldSoft and target would have been accepted). Replace it with a binary search over (oldSoft, target] for the highest value the system accepts, and treat a limit that cannot be raised further as success rather than a spurious error — avoiding a misleading "could not raise" warning at startup when the existing limit is already adequate.
|
Claude finished @dpup's task in 4m 0s —— View job Reviewing this PR
SummaryThe latest commit (508bf35) replaces the halving fallback with a proper binary search and fully resolves the edge case flagged in both prior reviews ( Verified locally:
No blocking issues. This looks ready to merge. |
Problem
Running multiple Claude sessions inside moat on macOS, all sessions would freeze simultaneously during a package install, then free up and recover gracefully a bit later.
That signature — every session stalling at once and then recovering cleanly (no crash, no OOM) — points at transient saturation of a shared dependency. The one thing every run shares is the single credential-injecting proxy daemon: all container egress is forced through it.
Root cause
The daemon inherited the host's default
RLIMIT_NOFILEsoft limit (typically 256 on macOS) — nothing raised it. A package install bursts a lot of concurrent connections through that one proxy:bun installopens up to 64 parallel connections (Bun's default since v1.1.33), each consuming file descriptors on both the client and upstream sides, and several runs can install at once. That easily exceeds 256, exhausting the proxy's FDs. While exhausted it can't accept/dial for any run, so every session'sclaude.aitraffic stalls — until the install finishes, the connections close, FDs free, and everyone recovers.Fix
Raise the daemon's
RLIMIT_NOFILEsoft limit toward 65536 (capped at the hard limit) at startup, before the proxy accepts connections.golang.org/x/sys/unix(the daemon runs on the host — Linux or macOS).kern.maxfilesperproceven when the hard limit is nominally higher, so the target steps down until one sticks (a typical Mac goes 256 → ~8192).setrlimitfailure logs a warning and the daemon still runs with the inherited limit.from/toso the raise is visible with--verbose.Testing
go test -race ./internal/daemon/— newfilelimit_test.goasserts the limit is raised, never lowered, respects the hard cap, and is idempotent.go build ./...,go vet,golangci-lint run— clean.Notes
This is the primary remediation from a short investigation into Bun-in-Docker stability. Related findings that did not warrant a change: Bun's install concurrency is already capped at 64 (moat pins Bun ≥ 1.1.33) with no container-wide knob to lower it further; keep-alive/cert-cache tuning lives in the external
gatekeeperproxy, not moat.🤖 Generated with Claude Code