Skip to content

fix(test): cap default worker count to the fd budget (#65219) - #66253

Open
kimdzhekhon wants to merge 1 commit into
NousResearch:mainfrom
Library-Core:fix/test-runner-fd-budget
Open

fix(test): cap default worker count to the fd budget (#65219)#66253
kimdzhekhon wants to merge 1 commit into
NousResearch:mainfrom
Library-Core:fix/test-runner-fd-budget

Conversation

@kimdzhekhon

Copy link
Copy Markdown

What does this PR do?

scripts/run_tests_parallel.py defaulted the per-file parallel test runner's worker count to cpu_count*2 with no awareness of the process's file-descriptor budget (RLIMIT_NOFILE). On a many-core macOS box with the platform's default soft limit (256), that can pick 20 workers — each running a full pytest subprocess whose fixtures (tempdirs, asyncio event loops, aiohttp test sockets) hold several dozen fds — and exhaust descriptors mid-suite. The resulting OSError: [Errno 24] Too many open files then surfaces as unrelated test/collection failures instead of a clear infrastructure signal.

_default_worker_count() now caps cpu_count*2 against the live RLIMIT_NOFILE soft limit using a conservative 64-fd-per-worker allowance, falling back to the old cpu-based default on platforms without RLIMIT_NOFILE (e.g. Windows) or when the soft limit is generous (most Linux CI runners are unaffected).

This doesn't fix descriptor leaks within a single file's test run (a separate, per-file concern) — it only keeps the default worker count from making exhaustion more likely than the box can actually sustain. HERMES_TEST_WORKERS / -j still override the default explicitly, unaffected by this change.

Related Issue

Fixes #65219

Type of Change

  • 🐛 Bug fix

Changes Made

  • scripts/run_tests_parallel.py: add _FD_BUDGET_PER_WORKER and _default_worker_count(), wire it into the -j/--jobs argparse default, update the module docstring's HERMES_TEST_WORKERS description.
  • tests/test_run_tests_parallel.py: two regression tests — one asserting a low soft limit (256) caps the default to 4 workers on a 10-core box, one asserting a generous soft limit (65536) leaves cpu_count*2 unaffected on a 4-core box.

How to Test

scripts/run_tests.sh tests/test_run_tests_parallel.py -v

Manual repro of the capped path:

python3 -c "
import resource, sys
resource.setrlimit(resource.RLIMIT_NOFILE, (256, resource.getrlimit(resource.RLIMIT_NOFILE)[1]))
sys.path.insert(0, 'scripts')
import run_tests_parallel as r
print(r._default_worker_count())  # 4, not cpu_count*2
"

What platforms you tested on

macOS (Darwin 25.5.0, arm64). The fd-budget path is POSIX-only by design (gated on resource.RLIMIT_NOFILE availability); Windows and any platform without it fall back to the pre-existing cpu_count*2 default unchanged.

Checklist

  • Conventional Commits (fix(test): ...)
  • Focused, single-purpose change (worker-count default only)
  • Tested locally — tests/test_run_tests_parallel.py passes (7/7)
  • ruff check clean on changed files

)

The per-file parallel runner defaulted to cpu_count*2 workers with no
awareness of RLIMIT_NOFILE. On hosts with a low soft limit (e.g. macOS's
default 256), a many-core box can exhaust file descriptors mid-suite,
turning unrelated test files into misleading failures instead of
surfacing a clear infrastructure signal.

_default_worker_count() now caps cpu_count*2 against the process's
RLIMIT_NOFILE soft limit using a conservative 64-fd-per-worker allowance,
falling back to the old cpu-based default wherever RLIMIT_NOFILE isn't
available (e.g. Windows) or the soft limit is generous.
@alt-glitch alt-glitch added type/test Test coverage or test infrastructure P3 Low — cosmetic, nice to have sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation labels Jul 17, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved (LGTM)

Overview

Caps default worker count to the fd budget in test infrastructure. +92/0.

Security

  • No hardcoded secrets or credentials

Code Quality

  • Clean resource limit handling

Looks Good

  • Appropriate test infrastructure fix

Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused reproduction and tests. The current runner still defaults to cpu_count*2 at scripts/run_tests_parallel.py:657, so the reported concurrency behavior remains on main.

Problems

  • scripts/run_tests_parallel.py:301-324 starts a separate pytest subprocess for each file, while ThreadPoolExecutor only controls concurrent launches at scripts/run_tests_parallel.py:941-948. RLIMIT_NOFILE is a per-process limit, so the proposed soft_limit // 64 cap does not establish a descriptor pool shared by those workers and does not demonstrate that it prevents the reported EMFILE failure.
  • The added tests mock resource.getrlimit() and assert the formula, but do not exercise the subprocess runner or the reported failure mode.

Suggested changes

  • Identify the actual shared macOS resource limit or per-child descriptor leak first, then test the mitigation through the runner's subprocess path.

Automated hermes-sweeper review.

soft_limit, _ = resource.getrlimit(resource.RLIMIT_NOFILE)
except (ImportError, AttributeError, ValueError, OSError):
# No RLIMIT_NOFILE concept (e.g. Windows) — fall back to cpu-based.
return cpu_based

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RLIMIT_NOFILE applies to each process, but every worker here is a separate pytest subprocess. This arithmetic treats it as one budget shared by all workers, so it cannot establish that reducing -j prevents a child from hitting EMFILE. Please diagnose the actual shared resource or per-child leak before capping concurrency this way.

@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Thirteen PRs are in this test-runner complex: #54008 resolves separatorless pytest flags; #57152/#57261 and #51896 address Windows path-list or encoding failures; #60304/#61238 address interrupted-run reporting; #30643/#63873/#66253 change worker sizing; and #40648, #51900, and part of #42204 cover separate runner-adjacent concerns. For #65219 specifically, #30643 and #63873 reduce concurrency, while #66253 proposes an RLIMIT_NOFILE formula that the contributor review says does not establish or test prevention of the reported subprocess EMFILE failure.

Related pull requests

Duplicates

#42195 and the runner portion of #42204 are superseded by merged #54008. #57261 duplicates #57152, while #51896 overlaps only with their legacy-encoding half; #60304 and #61238 overlap on signal handling but are not full duplicates because #61238 additionally retains statuses, classifies reports, cleans up child trees, and exercises a signal lifecycle.

Suggested consolidation

Keep #63873 open with a salvage path as the recorded best existing #65219 mitigation: preserve its focused cpu_count*2-to-cpu_count correction and policy tests while separately adding reproduction-backed EMFILE detection or resource accounting. Author action on #66253: identify the actual shared macOS resource constraint or per-child descriptor leak and add an end-to-end subprocess reproduction before retaining an RLIMIT-based cap; this explicitly follows, rather than overrides, the blocking contributor keep_open review. Keep #60304, #61238, #42195, and #57261 closed for the reasons above; treat #42204's runner portion as superseded by #54008, and consolidate #51896 only into #57152's overlapping encoding work after #57152 adds the requested --files and HERMES_TEST_PATHS coverage.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I65219(["issue #65219 (open)"])
    P66253["PR #66253 (open)"]
    P66253 -.->|partial| I65219
    class I65219 open
    class P66253 open
    class P66253 target
    click I65219 "https://github.com/NousResearch/hermes-agent/issues/65219"
    click P66253 "https://github.com/NousResearch/hermes-agent/pull/66253"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 13 pull requests and 4 issues in this complex. Each diff was read against this issue; Assessment working set: 100 kB of PR diffs, 38 kB of issue/PR text, 9 kB of discussion (18 comments), 17 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation type/test Test coverage or test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: full macOS suite exhausts file descriptors under per-file parallel runner

5 participants