fdlimit

package
v0.8.51 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package fdlimit raises the daemon's open-file limit at startup.

Why this exists: dejimad is fd-hungry in a way that isn't obvious from its small memory footprint. Every island egress CONNECT tunnel costs TWO descriptors (the hijacked client conn and the dialed target conn) and lives as long as the agent's HTTPS session does — and a single Claude Code agent holds many concurrent connections to the LLM API. Add the API/TCP/SSH listeners and the pipes of every `docker exec` child and the working set is easily hundreds.

Meanwhile launchd hands a LaunchAgent whatever `launchctl limit maxfiles` says, which on stock macOS is a soft limit of 256. A daemon started from a shell inherits the shell's much larger limit, so this only bites the service-managed install — which is exactly the one operators run unattended. Exhaustion does not look like exhaustion: accept(2) starts failing with EMFILE, net/http treats that as retryable and keeps looping, so clients complete the TCP handshake into a backlog nobody drains and hang until they time out. Once the backlog fills the kernel refuses outright. "Some connections hang, some are refused, a restart fixes it for an hour" is the signature.

Raising the limit in-process fixes already-installed daemons without waiting for a reinstall; the service templates also set it so a fresh install starts correct. Both are cheap, and they are independent.

Index

Constants

View Source
const Target uint64 = 16384

Target is the soft limit we want. It is far above any plausible working set (a busy host runs in the low hundreds) and far below the per-process ceiling on both macOS and Linux, so Raise normally reaches it exactly.

Variables

This section is empty.

Functions

func Guard

func Guard(ln net.Listener, warn func(msg string, args ...any)) net.Listener

Guard wraps ln so that descriptor exhaustion is reported instead of endured.

This is the diagnosability half of the fix. net/http classifies an EMFILE from accept(2) as temporary: it sleeps briefly and loops, forever, logging nothing the operator would recognize. Connections meanwhile complete their handshake into a backlog nobody drains, so clients hang until they time out and then — once the backlog fills — get refused. Nothing in that picture says "out of file descriptors", which is how it gets misread as a deadlock.

warn is called on the first exhausted accept and at most once a minute after, with the current soft limit for context.

Types

type Result

type Result struct {
	Was    uint64 // soft limit before
	Now    uint64 // soft limit after
	Max    uint64 // hard limit (RLIM_INFINITY reported as ^uint64(0))
	Raised bool
}

Result reports what Raise did, for logging. Raised is false when the limit already met Target and nothing was changed.

func Raise

func Raise() (Result, error)

Raise lifts RLIMIT_NOFILE's soft limit toward Target, never above the hard limit. It is best-effort by design: a daemon that cannot raise its limit should still start and serve, just with less headroom, so callers log the error rather than treating it as fatal.

The retry loop is for macOS. Darwin's hard limit is frequently reported as RLIM_INFINITY, but the kernel silently caps a process at kern.maxfilesperproc and fails the setrlimit outright rather than clamping. Rather than read the sysctl, halve the request until one sticks — this lands on the real ceiling in a handful of iterations on any system.

func (Result) String

func (r Result) String() string

String renders the result for a log line.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL