fix(test): cap default worker count to the fd budget (#65219) - #66253
fix(test): cap default worker count to the fd budget (#65219)#66253kimdzhekhon wants to merge 1 commit into
Conversation
) 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.
tonydwb
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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-324starts a separate pytest subprocess for each file, whileThreadPoolExecutoronly controls concurrent launches atscripts/run_tests_parallel.py:941-948.RLIMIT_NOFILEis a per-process limit, so the proposedsoft_limit // 64cap 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 |
There was a problem hiding this comment.
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.
SummaryThirteen 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 consolidationKeep #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 graphflowchart 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"
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. |
What does this PR do?
scripts/run_tests_parallel.pydefaulted the per-file parallel test runner's worker count tocpu_count*2with 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 resultingOSError: [Errno 24] Too many open filesthen surfaces as unrelated test/collection failures instead of a clear infrastructure signal._default_worker_count()now capscpu_count*2against the liveRLIMIT_NOFILEsoft limit using a conservative 64-fd-per-worker allowance, falling back to the old cpu-based default on platforms withoutRLIMIT_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/-jstill override the default explicitly, unaffected by this change.Related Issue
Fixes #65219
Type of Change
Changes Made
scripts/run_tests_parallel.py: add_FD_BUDGET_PER_WORKERand_default_worker_count(), wire it into the-j/--jobsargparse default, update the module docstring'sHERMES_TEST_WORKERSdescription.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) leavescpu_count*2unaffected on a 4-core box.How to Test
Manual repro of the capped path:
What platforms you tested on
macOS (Darwin 25.5.0, arm64). The fd-budget path is POSIX-only by design (gated on
resource.RLIMIT_NOFILEavailability); Windows and any platform without it fall back to the pre-existingcpu_count*2default unchanged.Checklist
fix(test): ...)tests/test_run_tests_parallel.pypasses (7/7)ruff checkclean on changed files