Mads
Multi-Agent Distributed System
Loading...
Searching...
No Matches
fd_limit.hpp
Go to the documentation of this file.
1/*
2Internal helper: reads, plans and applies the process-wide open-file-descriptor
3limit (POSIX RLIMIT_NOFILE), backing the `[broker] max_open_files` setting.
4Not part of the installed SDK (src/detail/ is excluded from the LIB_HEADERS
5install glob in CMakeLists.txt).
6
7WHY THIS EXISTS. The broker holds two permanently open descriptors per
8connected agent -- the agent's PUB connects to the XSUB frontend and its SUB to
9the XPUB backend -- plus a third, transient one while the agent fetches its
10settings over the REQ/ROUTER settings socket. With Linux's usual soft
11RLIMIT_NOFILE of 1024 and the broker's own ~30 descriptors of overhead, that
12walls a fleet in at roughly 490 agents, and libzmq reports the wall almost
13invisibly: tcp_listener_t::accept() lists EMFILE/ENFILE among its non-fatal
14errnos, so a refused agent produces only a ZMQ_EVENT_ACCEPT_FAILED that nobody
15was listening for.
16
17The decision logic (plan_fd_limit) is deliberately pure -- it takes the
18observed limits as data and makes no syscalls -- so every branch, including the
19platforms the build is not currently running on, is unit-testable from
20tests/test_fd_limit.cpp. Mirrors the "pure evaluator" split that
21src/doctor_checks.hpp documents.
22*/
23#pragma once
24
25#include <algorithm>
26#include <cerrno>
27#include <cstdint>
28#include <cstring>
29#include <fstream>
30#include <optional>
31#include <string>
32
33#ifndef _WIN32
34#include <sys/resource.h>
35#include <sys/time.h>
36#endif
37
38#ifdef __APPLE__
39#include <sys/sysctl.h>
40#include <sys/types.h>
41#endif
42
43namespace Mads::detail {
44
48inline constexpr uint64_t FD_PER_AGENT = 2;
49
60#if defined(__linux__)
61inline constexpr uint64_t FD_BROKER_OVERHEAD = 40;
62#else
63inline constexpr uint64_t FD_BROKER_OVERHEAD = 64;
64#endif
65
69inline constexpr uint64_t FD_LOW_WATERMARK = 1024;
70
73inline constexpr uint64_t FD_ABSOLUTE_CEILING = 1048576;
74
76inline uint64_t agent_capacity(uint64_t soft) {
77 if (soft <= FD_BROKER_OVERHEAD)
78 return 0;
79 return (soft - FD_BROKER_OVERHEAD) / FD_PER_AGENT;
80}
81
85struct FdLimits {
86 bool supported = false;
87 uint64_t soft = 0;
92 uint64_t hard = 0;
93};
94
95namespace fd_limit_impl {
96
98inline std::optional<uint64_t> read_uint_file(const char *path) {
99 std::ifstream in(path);
100 if (!in)
101 return std::nullopt;
102 uint64_t value = 0;
103 if (!(in >> value))
104 return std::nullopt;
105 return value;
106}
107
113inline std::optional<uint64_t> kernel_fd_ceiling() {
114#if defined(__APPLE__)
115 int value = 0;
116 size_t size = sizeof(value);
117 if (sysctlbyname("kern.maxfilesperproc", &value, &size, nullptr, 0) == 0 &&
118 value > 0) {
119 return static_cast<uint64_t>(value);
120 }
121 return std::nullopt;
122#elif defined(__linux__)
123 return read_uint_file("/proc/sys/fs/nr_open");
124#else
125 return std::nullopt;
126#endif
127}
128
129} // namespace fd_limit_impl
130
133 FdLimits limits;
134#ifdef _WIN32
135 // Windows has no RLIMIT_NOFILE. libzmq uses SOCKET handles, which are not C
136 // runtime descriptors and are bounded by the process handle quota rather
137 // than a settable per-process table; _setmaxstdio() governs only stdio
138 // FILE* streams and would do nothing for sockets. Reporting this as
139 // unsupported is honest; silently pretending to apply a limit would not be.
140 limits.supported = false;
141#else
142 rlimit rl{};
143 if (getrlimit(RLIMIT_NOFILE, &rl) != 0)
144 return limits;
145 limits.supported = true;
146 limits.soft = static_cast<uint64_t>(rl.rlim_cur);
147 limits.hard = rl.rlim_max == RLIM_INFINITY
149 : static_cast<uint64_t>(rl.rlim_max);
150 if (const auto ceiling = fd_limit_impl::kernel_fd_ceiling())
151 limits.hard = std::min(limits.hard, *ceiling);
152 // A hard limit below the soft one is nonsensical, but clamping keeps every
153 // downstream comparison well-behaved rather than underflowing.
154 limits.hard = std::max(limits.hard, limits.soft);
155#endif
156 return limits;
157}
158
162 enum class Action {
164 Unset,
165 Invalid,
166 Unchanged,
167 Raise,
168 Lower
169 };
170
175 bool clamped = false;
176
179 uint64_t target = 0;
181 std::string message;
183 bool warn = false;
184};
185
187inline std::string describe_fd_limits(const FdLimits &limits) {
188 return std::to_string(limits.soft) + " soft / " + std::to_string(limits.hard) +
189 " hard, about " + std::to_string(agent_capacity(limits.soft)) +
190 " agents";
191}
192
194inline std::string fd_limit_hint() {
195 return "set `max_open_files` in the [broker] section of the settings file "
196 "(or LimitNOFILE= in the systemd unit) to raise it";
197}
198
202inline FdLimitPlan plan_fd_limit(std::optional<int64_t> requested,
203 const FdLimits &limits) {
204 FdLimitPlan plan;
205
206 if (!limits.supported) {
208 // Only worth saying anything when the operator actually asked for a limit
209 // and is entitled to know it did not take effect.
210 if (requested.has_value()) {
211 plan.message = "max_open_files is not applicable on this platform: it "
212 "has no per-process descriptor limit for sockets";
213 plan.warn = true;
214 }
215 return plan;
216 }
217
218 plan.target = limits.soft;
219
220 if (!requested.has_value()) {
222 plan.message = "File descriptors: " + describe_fd_limits(limits);
223 // Nagging is only useful where raising would actually buy something.
224 if (limits.soft <= FD_LOW_WATERMARK && limits.hard > limits.soft) {
225 plan.warn = true;
226 plan.message += " -- " + fd_limit_hint();
227 }
228 return plan;
229 }
230
231 if (*requested < 0) {
232 // Same posture as [broker] io_threads: a bad value in the settings file
233 // must not stop the broker from starting.
235 plan.warn = true;
236 plan.message = "Invalid [broker] max_open_files = " +
237 std::to_string(*requested) + ", ignoring it. File "
238 "descriptors: " + describe_fd_limits(limits);
239 return plan;
240 }
241
242 // 0 means "give me everything this process is permitted to have".
243 uint64_t wanted = *requested == 0 ? limits.hard
244 : static_cast<uint64_t>(*requested);
245
246 // Above the hard limit the request is capped rather than refused: only
247 // LimitNOFILE= in the unit, or a privileged `ulimit -Hn`, can lift that.
248 plan.clamped = wanted > limits.hard;
249 if (plan.clamped)
250 wanted = limits.hard;
251
252 plan.target = wanted;
253
254 if (wanted == limits.soft) {
256 plan.message = "File descriptors: " + describe_fd_limits(limits) +
257 " (max_open_files already in force)";
258 } else if (wanted > limits.soft) {
260 plan.message = "File descriptors: raising soft limit " +
261 std::to_string(limits.soft) + " -> " +
262 std::to_string(wanted) + " (hard " +
263 std::to_string(limits.hard) + "), about " +
264 std::to_string(agent_capacity(wanted)) + " agents";
265 } else {
266 // Lowering is deliberate and allowed: the soft limit moves freely below
267 // the hard one, needing no privileges. It is how a broker is capped on a
268 // shared box, and -- the reason it must not be silently ignored -- the
269 // only way to exercise the descriptor-exhaustion path without root.
270 //
271 // Always reported as a warning: it is by far the minority case, and an
272 // accidental one (128 typed for 1280) is otherwise discovered only when
273 // the fleet stops growing.
275 plan.warn = true;
276 plan.message = "File descriptors: lowering soft limit " +
277 std::to_string(limits.soft) + " -> " +
278 std::to_string(wanted) + " (hard " +
279 std::to_string(limits.hard) + "), room for about " +
280 std::to_string(agent_capacity(wanted)) + " agents";
281 // Below its own footprint the broker cannot finish starting: it runs out
282 // while creating its own sockets, long before an agent ever connects.
283 // Saying so here is much cheaper than letting it die mid-startup.
284 if (wanted <= FD_BROKER_OVERHEAD) {
285 plan.message += " -- WARNING: the broker needs about " +
286 std::to_string(FD_BROKER_OVERHEAD) +
287 " descriptors for itself and will most likely fail to"
288 " start with this few";
289 }
290 }
291
292 if (plan.clamped) {
293 plan.warn = true;
294 plan.message += ". max_open_files = " + std::to_string(*requested) +
295 " exceeds this process's hard limit of " +
296 std::to_string(limits.hard) +
297 "; raising that needs LimitNOFILE= in the systemd unit or"
298 " a privileged `ulimit -Hn`";
299 }
300 return plan;
301}
302
306 bool applied = false;
307 std::string error;
308};
309
319 FdLimitOutcome outcome;
320 outcome.limits = query_fd_limits();
321
322 const bool wants_change = plan.action == FdLimitPlan::Action::Raise ||
324 if (!wants_change || !outcome.limits.supported)
325 return outcome;
326
327#ifndef _WIN32
328 rlimit rl{};
329 if (getrlimit(RLIMIT_NOFILE, &rl) != 0) {
330 outcome.error = std::strerror(errno);
331 return outcome;
332 }
333 rl.rlim_cur = static_cast<rlim_t>(plan.target);
334 if (setrlimit(RLIMIT_NOFILE, &rl) != 0) {
335 outcome.error = std::strerror(errno);
336 return outcome;
337 }
338 outcome.applied = true;
339 outcome.limits = query_fd_limits();
340#endif
341 return outcome;
342}
343
346inline bool is_fd_exhaustion(int err) {
347#ifdef _WIN32
348 // WSAEMFILE is what Winsock reports; EMFILE covers the CRT paths.
349 return err == EMFILE || err == 10024 /* WSAEMFILE */;
350#else
351 return err == EMFILE || err == ENFILE;
352#endif
353}
354
355} // namespace Mads::detail
std::optional< uint64_t > read_uint_file(const char *path)
Reads a single unsigned integer out of a /proc or /sys file.
Definition fd_limit.hpp:98
std::optional< uint64_t > kernel_fd_ceiling()
Definition fd_limit.hpp:113
FdLimitOutcome apply_fd_limit(const FdLimitPlan &plan)
Definition fd_limit.hpp:318
FdLimitPlan plan_fd_limit(std::optional< int64_t > requested, const FdLimits &limits)
Definition fd_limit.hpp:202
constexpr uint64_t FD_PER_AGENT
Definition fd_limit.hpp:48
constexpr uint64_t FD_BROKER_OVERHEAD
Definition fd_limit.hpp:63
std::string describe_fd_limits(const FdLimits &limits)
Renders e.g. "1024 soft / 1048576 hard, about 492 agents".
Definition fd_limit.hpp:187
uint64_t agent_capacity(uint64_t soft)
How many agents a given soft limit leaves room for.
Definition fd_limit.hpp:76
FdLimits query_fd_limits()
Reads the limits currently in force, with hard clamped as described above.
Definition fd_limit.hpp:132
constexpr uint64_t FD_LOW_WATERMARK
Definition fd_limit.hpp:69
std::string fd_limit_hint()
The hint appended to every message that reports a limit worth raising.
Definition fd_limit.hpp:194
constexpr uint64_t FD_ABSOLUTE_CEILING
Definition fd_limit.hpp:73
bool is_fd_exhaustion(int err)
Definition fd_limit.hpp:346
The result of acting on a plan.
Definition fd_limit.hpp:304
FdLimits limits
the limits in force afterwards
Definition fd_limit.hpp:305
std::string error
why it failed, when it did
Definition fd_limit.hpp:307
bool applied
a setrlimit() call was made and succeeded
Definition fd_limit.hpp:306
@ Raise
raise the soft limit to target
@ Invalid
configured value makes no sense; ignored, limit untouched
@ Unchanged
the soft limit is already exactly what was asked for
@ NotSupported
no per-process descriptor limit on this platform
@ Lower
lower the soft limit to target
@ Unset
nothing configured; report the limit and leave it alone
std::string message
One line, ready to print, explaining the limit and what was done to it.
Definition fd_limit.hpp:181
uint64_t target
The soft limit that should end up in force.
Definition fd_limit.hpp:179
bool warn
True when message reports something the operator should act on.
Definition fd_limit.hpp:183