Skip to content

Commit dd640f5

Browse files
committed
info-gatherer: fix 'Too many open files' from cpu*4 parallel workers
On a high-core host the info-gatherer runs cpu_count()*4 ProcessPoolExecutor workers (512 on a 128-core box). Each worker keeps a pipe open in the parent and forks a piped config-dump subprocess, so the pool exhausts the open-file limit when it spawns workers (OSError: [Errno 24] Too many open files). The binding limit is the *container's* RLIMIT_NOFILE, which defaults to ~1024 soft regardless of the host's (often generous) limit — so this bites in Docker even when a native build is fine. Two complementary fixes: - docker.sh: pass the HOST's hard nofile into the container via --ulimit nofile=<hard>:<hard>, so the container matches the host. The host hard limit never exceeds the kernel's fs.nr_open and the container shares that kernel, so the value is always a valid --ulimit. Falls back to 1048576 if the host reports 'unlimited'. - armbian_utils.py: before building the pool, raise the soft RLIMIT_NOFILE to the hard limit (best-effort) — fixes native/bare-metal runs with a low soft limit and is belt-and-suspenders inside the container. No change to the cpu*4 worker count. Signed-off-by: Igor Pecovnik <igor@armbian.com>
1 parent 84b4f93 commit dd640f5

2 files changed

Lines changed: 29 additions & 0 deletions

File tree

lib/functions/host/docker.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,29 @@ function docker_cli_build_dockerfile() {
397397

398398
function docker_cli_prepare_launch() {
399399
display_alert "Preparing" "common Docker arguments" "debug"
400+
401+
# The container otherwise inherits the Docker daemon's default open-file limit
402+
# (classically 1024 soft), independent of the host's own (possibly generous)
403+
# limit. That is too low for the parallel info-gatherer (cpu*4 workers, each
404+
# holding pipes) and it dies with "OSError: [Errno 24] Too many open files".
405+
# Pass the HOST's hard limit through so the container matches the host. The
406+
# host hard limit can never exceed the kernel's fs.nr_open, and the container
407+
# shares that kernel, so this value is always a valid --ulimit.
408+
declare _docker_nofile_hard
409+
_docker_nofile_hard="$(ulimit -H -n 2>/dev/null || true)"
410+
[[ "${_docker_nofile_hard}" == "unlimited" || -z "${_docker_nofile_hard}" ]] && _docker_nofile_hard=1048576
411+
400412
declare -g -a DOCKER_ARGS=(
401413
"--rm" # side effect - named volumes are considered not attached to anything and are removed on "docker volume prune", since container was removed.
402414

403415
"--cap-add=SYS_ADMIN" # add only required capabilities instead
404416
"--cap-add=MKNOD" # (though MKNOD should be already present)
405417
"--cap-add=SYS_PTRACE" # CAP_SYS_PTRACE is required for systemd-detect-virt in some cases @TODO: rpardini: so lets eliminate it @TODO: rpardini maybe it's dead already?
406418

419+
# Match the host's open-file limit (see above) so the parallel info-gatherer
420+
# isn't capped by the container's default 1024 and hit Errno 24.
421+
"--ulimit" "nofile=${_docker_nofile_hard}:${_docker_nofile_hard}"
422+
407423
# Pass env var ARMBIAN_RUNNING_IN_CONTAINER to indicate we're running under Docker. This is also set in the Dockerfile; make sure.
408424
"--env" "ARMBIAN_RUNNING_IN_CONTAINER=yes"
409425

lib/tools/common/armbian_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import logging
1515
import multiprocessing
1616
import os
17+
import resource
1718
import re
1819
import shlex
1920
import subprocess
@@ -542,6 +543,18 @@ def gather_json_output_from_armbian(command: str, targets: list[dict]):
542543
total = len(targets)
543544
# get the number of processor cores on this machine
544545
max_workers = multiprocessing.cpu_count() * 4 # use four times the number of cpu cores, that's the sweet spot
546+
# Each pool worker keeps a pipe open in the parent and forks a config-dump
547+
# subprocess (more pipes), so a high core count (e.g. 512 workers on a
548+
# 128-core host) blows past the default soft open-file limit and the pool
549+
# dies spawning workers ("OSError: [Errno 24] Too many open files"). Raise
550+
# the soft RLIMIT_NOFILE to the hard limit so the workers have the fds.
551+
try:
552+
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
553+
if soft < hard:
554+
resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
555+
log.info(f"Raised open-file limit {soft} -> {hard} for {max_workers} workers.")
556+
except (ValueError, OSError) as e:
557+
log.warning(f"Could not raise the open-file limit for {max_workers} workers: {e}")
545558
log.info(f"Using {max_workers} workers for parallel processing.")
546559
with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
547560
every_future = []

0 commit comments

Comments
 (0)