Repository navigation
Conversation
Signed-off-by: Subgurim <[email protected]>
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
|
Thank you for the contribution and for framing this as an OS containment boundary rather than claiming it fixes the allocator or indexing growth itself. This is routed as a high-priority Windows stability change. Job Object memory limits affect worker termination and descendant behavior, so we need more time to review the native semantics and the regression probe carefully. The contribution queue is quite full, but this has a clear operational safety claim and will receive a proper review. No changes are requested from you at this triage stage. Thank you for including measured v0.10.8 evidence and a Windows-specific test path. |
DeusData
left a comment
There was a problem hiding this comment.
Welcome, and thank you for this — it is a well-aimed PR at a real gap. The #832 incident numbers in your description (a 3 GiB budget beside ~40 GiB workers) are exactly the case a Job Object limit exists for, and I would like to land it. I built it on macOS and ran it on a native Windows machine; the plumbing is right, but the value it hands to the OS is not safe yet. Details below, with what I saw and what I would ask you to change.
What is right
JOB_OBJECT_LIMIT_JOB_MEMORY+JobMemoryLimitis the correctly paired flag/field, and job-wide is the right scope — descendants are covered (src/foundation/subprocess.c:664-665).- Ordering is airtight: the limit is set on the job before
CreateProcessW, the worker is created suspended, assigned to the job, and only then resumed (:661-673,:741,:758,:766). No worker instruction runs outside the cap. - It extends the existing job rather than creating a second one, so the nested-job case (Windows 8+, CI runners already inside a job) is no new risk.
0means no limit, units are bytes end-to-end (application.c:2926-2928), and the field is documented honestly as Windows-only insubprocess.h.- The test is a genuine reproduction. I reverted only the
subprocess.cproduction hunk on the Windows box and re-ran:subprocess_windows_job_object_enforces_memory_limitthen fails for the right reason (outcome == 0, expected CBM_PROC_EXIT_NONZERO— the 512 MiBVirtualAllocsucceeds and the probe exits 0). It asserts the child's observable outcome, not merely that an API returned success.scripts/check-no-test-skips.shis clean and the test is wired intoSUITE(subprocess), which the runner really executes.
The blocker — proven on real Windows
build/c/test-runner subprocess index_supervisor on Windows (ARM64) with the PR as-is:
index_supervisor_async_jobs_are_isolated_cancellable_and_terminal_cached
outcome=crash exit_code=-1073741571 (0xC00000FD STATUS_STACK_OVERFLOW)
[worker-dump] worker-a log: (empty)
FAIL tests/test_index_supervisor.c:510: ASSERT(first_ready)
With the subprocess.c hunk reverted the same test passes and the totals stay 22/1/18 — the single failure just moves from the supervisor suite to your new test, which is the signature of a clean revert-check. index_supervisor runs in every CI shard, so the Windows leg will go red on this once the queue drains (it is congested today; the test-msan and test-windows-guards reds you will see on other PRs are ours, not yours).
Cause: the same number is now used twice, with no floor. src/mcp/index_supervisor.c:669-674 passes memory_budget_bytes to the worker as its internal soft budget (--memory-budget → main.c:1066 → mem.c:257-265), and :698 now passes the identical value as a hard OS commit cap. Those supervisor tests start workers with token budgets of 4096 and 8192 bytes (tests/test_index_supervisor.c:390,392 — they only assert the value reaches the child argv). Under the PR that becomes a 4 KiB / 8 KiB commit limit, and with the binary's 8 MiB stack reserve (-Wl,--stack,8388608) the worker dies before main() — hence the empty log. Anyone setting a small CBM_MEM_BUDGET_MB gets the same undiagnosable worker_failed outcome=crash.
The deeper mismatch: the budget is soft, the Job limit is hard, and they measure different things. cbm_mem_over_budget() compares RSS (mem.c:511-515); a Job Object limit is on commit charge, which is ≥ RSS. And the pipeline is designed to overshoot the budget: when backpressure naps are futile the worker latches bp_futile and proceeds with a documented soft overshoot (pass_parallel.c:760-784), and over-budget during dump only triggers opportunistic freeing (graph_buffer.c:1732). With the cap at exactly the budget, VirtualAlloc is denied mid-pipeline at whatever site is unlucky — there is no global OOM handler — so "slow, over budget, completes" becomes "index fails". The graceful path is bypassed, not assisted.
Changes I would ask for
- A floor. Do not set
JOB_OBJECT_LIMIT_JOB_MEMORYbelow a sane minimum (a few hundred MiB is a reasonable start); below it, keep the job with kill-on-close only. This alone clears the supervisor RED. Keep the arithmetic in the supervisor (policy) and leavesubprocess.cas pure mechanism — the split you already have. - Headroom above the soft budget —
JobMemoryLimit = budget × factor(1.25–1.5× is the range I would start from) so backpressure and dump-time freeing get to act before the OS denies commit. Put the reason in a comment next to the flag: RSS (mem.c:511) vs commit, and the deliberate overshoot path. One design point I want to settle on our side before you finalise this item: whether the OS cap should be a deliberately looser backstop (my expectation) or sit exactly at the budget with the internal budget lowered instead. I will confirm here shortly; items 1, 3 and 4 do not depend on it. - Make the failure diagnosable. When a worker fails, read
PeakJobMemoryUsed(JobObjectExtendedLimitInformation) orJobObjectLimitViolationInformationand log anindex.supervisor.worker_over_budget-style line with peak and limit. Today the operator gets an empty worker log and a bareexit_code=-1073741571. - (nit) Give the test probe more distance from the process baseline — e.g. a 1 GiB limit with a 2 GiB allocation. The Windows test-runner is ~393 MB against the current 512 MiB limit, so on a host where startup commit exceeded the limit the child would die with a non-73 status and the test would fail for the wrong reason.
Two small things you can ignore or fold in as you like: the probe uses GetModuleFileNameA while production self-discovery is pinned to the W variant (win_utf8.h:174) — existing tests do the same, but a non-ASCII build path would break the new one; and char self_path[32768] is a hefty stack buffer for a test.
Everything else — security, scope, format (the test_main.c:25 clang-format hit is byte-identical on main, not yours) — is clean. Once the floor and headroom are in, I expect this to be a quick merge.
|
Settled on our side: the OS cap is a looser backstop, not a hard cap at the budget. The internal budget stays advisory with its documented overshoot path; the Job Object limit sits above it to catch runaways like the #832 case (13× over), and the floor keeps small or token budgets from being fatal at startup. So item 2 is as written: |
Signed-off-by: Subgurim <[email protected]>
|
Hi @experticket-admin — checking in on this one, and we'd still very much like to land it. The mechanism is right: the Job Object memory limit, the set-before-CreateProcessW / suspended-assign-resume ordering, the job-wide scope covering descendants, and the reproduction test (which fails for the right reason when the production hunk is reverted) are all solid. The one thing still blocking a merge is the point from the review above: the memory-limit value handed to the OS isn't safe yet. Once that's addressed, this should be ready — and the red Windows CI leg looks like our known flaky lane rather than anything in your change, so don't worry about that one; we can re-run it. No rush at all — whenever you have a moment to push the value fix, drop a note here and we'll take another pass and get it merged. Thanks again for tackling a real gap (#832). |
…l sin techo Signed-off-by: Subgurim <[email protected]>
Signed-off-by: Subgurim <[email protected]>
Signed-off-by: Subgurim <[email protected]>
Signed-off-by: Subgurim <[email protected]>
|
Updated to Addressed the review requests:
I also corrected the native enforcement test based on a real Windows observation. An independent API-only probe gave:
Thus Validation
An external text-only review found no blocking issue in the revised change. The final commits after that review clarify/format a comment only. This remains a per-worker-tree commit backstop. It neither guarantees aggregate host memory (already-running daemon workers are not retroactively resized), nor guarantees cooperative checks will always run before a denied allocation. No experimental binary has been installed on the affected PC. |
|
Merged as The mechanism was right from the first revision: What was missing was the value, and
The comment earns its place too. Saying plainly that Windows charges job-wide commit, not RSS, and that 1.5× is a backstop for descendants rather than exact enforcement of the accounting budget, is the kind of honesty that stops the next reader trusting this more than it deserves. Same for the diagnostics: reporting This completes the Windows half of the memory work — the cross-platform side (an over-budget index attempt failing whole with a named error, keeping the serving index) landed in #2112, and your Job Object cap is the OS-enforced backstop underneath it for the worker and everything it spawns. It ships in v0.10.9. Thanks for your patience through the review round — the delay was ours. |
What does this PR do?
Adds a Windows OS-enforced commit backstop for an index worker and its descendants, using
JOB_OBJECT_LIMIT_JOB_MEMORYon the existing Job Object. Related: #832; complements the cooperative over-budget abort in #2112.Updated after maintainer review:
KILL_ON_JOB_CLOSEremain enabled.Scope and limitations
Windows commit is not RSS. The headroom allows room for cooperative recovery/abort, but does not guarantee that it will run before an allocation is denied. This is a per-job backstop, not a global host-memory cap: existing active jobs are not retroactively resized when the daemon starts another job. Zero-budget legacy callers remain uncapped. Other subprocess callers default to no memory cap; this does not impose a general limit on Git or search tools.
This does not fix indexing/allocator growth, guarantee that a repository will fit, or establish the cause of a whole-PC freeze. Automatic reindexing is unchanged.
Regression coverage
VirtualAllocmust succeed without a cap and return the probe's allocation-denied exit code with a 1 GiB cap. The probe does not touch the allocated pages and both process trees must be quiescent.Why the test does not assert
peak <= cap: an independent Windows API probe showed a denied 2 GiB reservation reflected inPeakJobMemoryUsedeven with a 1 GiB cap. The uncapped/capped allocation outcomes prove denial; the peak counter alone does not.Local verification (2026-09-11)
subprocess16 passed, 18 platform skips, four consecutive direct executions. Earlier cold/MSYS-launched runs had startup/quiet-timeout failures and are not counted as successful runs.index_supervisor: policy and terminal-memory-log tests pass, but the suite aborts later in an existing progress-capture test with0xC0000374atfclose(tmpfile()). A small program with no subprocess/supervisor code reproduces the same failure when linked with unchangedmain's Windows mimalloc wrappers; without those wrappers it exits normally. This does not certify the whole Windows suite or establish that CI's different Clang/UCRT toolchain reproduces it.scripts/lint.sh --ciwith scoped inputs passes (clang-format 20.1.8, cppcheck 2.13; CI uses cppcheck 2.20). No-skips policy, no-window contract, DCO andgit diff --checkpass. This is not a claim that repository-wide lint/tests passed.scripts/test.sh --suites "subprocess index_supervisor": 41 passed, 2 platform skips, exit 0. The progress-capture test that aborts in the local Windows/MSVCRT build passes here. The isolated Linux image neededzlib1g-devafter themainupdate; no host dependency change was made.The native local build requires two command-line warning-severity exceptions for pre-existing GCC 16 warnings in unrelated tests (
free-nonheap-object,stringop-overflow). No suppressions were added to the repository. No test build has been installed on the affected PC.Checklist