Skip to content

feat(bootstrap): parallelize RESOLVE and PREPARE_SOURCE I/O with background threads#1199

Open
dhellmann wants to merge 3 commits into
python-wheel-build:mainfrom
dhellmann:background-tasks
Open

feat(bootstrap): parallelize RESOLVE and PREPARE_SOURCE I/O with background threads#1199
dhellmann wants to merge 3 commits into
python-wheel-build:mainfrom
dhellmann:background-tasks

Conversation

@dhellmann

@dhellmann dhellmann commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Add a thread pool for processing some tasks from the bootstrap work item stack before they are popped. Keep the stack processing as it is, allowing the backgrounded phases to get the results from the future object created by the thread pool. Apply this approach to the phases for resolving requirements and preparing source.

Ensure that background callables are module-level functions (no self capture) so threads cannot accidentally access mutable Bootstrapper state (self.why, self._seen_requirements, etc.)

Add a _push_items() helper to guarantee every RESOLVE and PREPARE_SOURCE item has a bg_future set, including the initial item in bootstrap()

Set the minimum size of the thread pool to 1 to avoid special cases.

Test plan

  • All existing unit tests pass (hatch run test:test tests/test_bootstrapper.py tests/test_bootstrapper_iterative.py)
  • TestPhaseResolve tests updated to use pre-completed Future objects instead of patching resolve_versions
  • Loop tests updated to mock _resolver.resolve directly (the path background threads use)
  • ./e2e/test_bootstrap_parallel_git_url.sh passes end-to-end

🤖 Generated with Claude Code

@dhellmann dhellmann requested a review from a team as a code owner June 17, 2026 21:38
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a background-threaded I/O prefetch pipeline to the bootstrapper. BootstrapRequirementResolver gains a new get_cached_resolution() method that checks the resolver session cache under a thread-safety lock and returns cached versions or None. A PreparedSourceData dataclass is added to carry background-prepared wheel/sdist artifacts, and WorkItem gains an optional bg_future field to synchronize background results. Wheel cache lookup and source preparation logic is extracted from Bootstrapper instance methods into standalone module-level functions (_find_cached_wheel, _download_wheel_from_cache, _create_unpack_dir, _bg_resolve, _bg_prepare_source, _bg_prepare_prebuilt). The bootstrapper gains a num_bg_threads parameter, initializes a ThreadPoolExecutor, and new _push_items/_drain_background_pool helpers manage LIFO submission and exclusive-build barriers. _phase_resolve and _phase_prepare_source now block on and consume futures. A --bg-threads CLI option with CPU-count-derived default is added to both bootstrap and bootstrap_parallel commands. lint_requirements wraps Bootstrapper in a context manager. Tests are updated throughout to inject bg_future directly and mock bt._resolver.resolve instead of bt.resolve_versions. E2E tests tighten log-message matching to require exact package-version prefixes (e.g., stevedore-5.2.0 instead of stevedore).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~80 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding background thread parallelization for RESOLVE and PREPARE_SOURCE I/O operations in the bootstrap process.
Description check ✅ Passed The description clearly explains the implementation approach, key design decisions (module-level functions, bg_future field, _push_items helper), and validates the test plan with passing test results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dhellmann dhellmann marked this pull request as draft June 17, 2026 21:39
@mergify mergify Bot added the ci label Jun 17, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/fromager/commands/bootstrap.py (1)

121-149: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a Sphinx version directive for the new CLI option.

Line 121 introduces a user-facing CLI flag, but the command docstring doesn’t record this with a Sphinx version marker.

Proposed change
 def bootstrap(
@@
 ) -> None:
     """Compute and build the dependencies of a set of requirements recursively
 
     TOPLEVEL is a requirements specification, including a package name
     and optional version constraints.
 
+    .. versionchanged:: NEXT
+       Added ``--bg-threads`` to control background I/O prefetch worker count.
+
     """

As per coding guidelines, **/*.{rst,py} requires Sphinx versionadded/versionchanged directives for user-facing changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/commands/bootstrap.py` around lines 121 - 149, The new CLI
option `--bg-threads` introduced in the bootstrap function's click decorators is
missing a Sphinx version directive in the function docstring. Add a
`:versionadded::` directive to the docstring of the bootstrap function to
document when this new user-facing CLI flag was introduced, following the coding
guidelines requirement for marking changes in py and rst files.

Source: Coding guidelines

🧹 Nitpick comments (2)
src/fromager/commands/bootstrap.py (1)

531-544: ⚡ Quick win

Expose and forward num_bg_threads in bootstrap_parallel for tunable performance.

bootstrap_parallel currently forces the default background-thread count when it invokes bootstrap, so callers of the parallel command cannot tune this knob.

Proposed change
 `@click.option`(
+    "--bg-threads",
+    "num_bg_threads",
+    type=click.IntRange(min=1),
+    default=max(1, (os.cpu_count() or 2) // 2),
+    show_default=True,
+    help="Number of background threads for parallel I/O pre-fetching (min 1).",
+)
+@click.option(
     "--max-release-age",
@@
 def bootstrap_parallel(
@@
     multiple_versions: bool,
     max_release_age: int | None,
+    num_bg_threads: int,
     toplevel: list[str],
 ) -> None:
@@
     ctx.invoke(
         bootstrap,
@@
         multiple_versions=multiple_versions,
         max_release_age=max_release_age,
+        num_bg_threads=num_bg_threads,
         toplevel=toplevel,
     )

Also applies to: 562-572

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/commands/bootstrap.py` around lines 531 - 544, The
`bootstrap_parallel` function does not expose a `num_bg_threads` parameter in
its function signature, preventing callers from tuning the background thread
count when it internally invokes `bootstrap`. Add `num_bg_threads` as a
parameter to the `bootstrap_parallel` function signature, and then forward this
parameter when calling `bootstrap` within the function body to allow callers to
control this performance knob.
tests/test_bootstrapper_iterative.py (1)

260-386: ⚡ Quick win

Add one bg_future exception-path test to cover threaded failure propagation.

The updated suite validates resolved/empty futures, but not the case where item.bg_future.result() re-raises an exception from the background resolver.

Proposed change
 def _make_resolved_future(
     result: typing.Any,
 ) -> concurrent.futures.Future[typing.Any]:
@@
     future.set_result(result)
     return future
 
+def _make_failed_future(
+    err: Exception,
+) -> concurrent.futures.Future[typing.Any]:
+    """Return an already-failed Future carrying *err*."""
+    future: concurrent.futures.Future[typing.Any] = concurrent.futures.Future()
+    future.set_exception(err)
+    return future
+
@@
 class TestPhaseResolve:
+    def test_future_exception_propagates(self, tmp_context: WorkContext) -> None:
+        bt = bootstrapper.Bootstrapper(tmp_context)
+        item = _make_resolve_item()
+        item.bg_future = _make_failed_future(RuntimeError("resolver failed"))
+
+        with pytest.raises(RuntimeError, match="resolver failed"):
+            bt._phase_resolve(item)
+
     def test_single_version(self, tmp_context: WorkContext) -> None:

As per coding guidelines, tests/** asks us to verify intended behavior and flag missing edge cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_bootstrapper_iterative.py` around lines 260 - 386, Add a new test
method to the TestPhaseResolve class that covers the exception path when
item.bg_future.result() raises an exception from the background resolver. The
test should create a resolve item with a bg_future that raises an exception when
result() is called, invoke _phase_resolve on the bootstrapper, and verify that
the exception is properly propagated. This test should validate that exceptions
from the background resolver thread are correctly surfaced, covering the missing
edge case in exception handling.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fromager/bootstrapper.py`:
- Around line 350-405: The background task functions _bg_resolve,
_bg_prepare_source, and _bg_prepare_prebuilt execute outside the main-thread
context and therefore lose per-requirement log attribution. Wrap each of these
functions with req_ctxvar_context() using the appropriate requirement parameter
to preserve requirement-scoped logging context. For _bg_resolve wrap with
req_ctxvar_context(req), for _bg_prepare_source wrap with
req_ctxvar_context(req), and for _bg_prepare_prebuilt wrap with
req_ctxvar_context(req).
- Around line 200-217: The ZipFile object created when opening wheel_filename is
not wrapped in a context manager, which can lead to file descriptor leaks
especially under parallel load. Wrap the zipfile.ZipFile call with a `with`
statement to ensure the file is properly closed and the file descriptor is
released immediately after use, moving the entire extraction logic inside the
context manager block.

---

Outside diff comments:
In `@src/fromager/commands/bootstrap.py`:
- Around line 121-149: The new CLI option `--bg-threads` introduced in the
bootstrap function's click decorators is missing a Sphinx version directive in
the function docstring. Add a `:versionadded::` directive to the docstring of
the bootstrap function to document when this new user-facing CLI flag was
introduced, following the coding guidelines requirement for marking changes in
py and rst files.

---

Nitpick comments:
In `@src/fromager/commands/bootstrap.py`:
- Around line 531-544: The `bootstrap_parallel` function does not expose a
`num_bg_threads` parameter in its function signature, preventing callers from
tuning the background thread count when it internally invokes `bootstrap`. Add
`num_bg_threads` as a parameter to the `bootstrap_parallel` function signature,
and then forward this parameter when calling `bootstrap` within the function
body to allow callers to control this performance knob.

In `@tests/test_bootstrapper_iterative.py`:
- Around line 260-386: Add a new test method to the TestPhaseResolve class that
covers the exception path when item.bg_future.result() raises an exception from
the background resolver. The test should create a resolve item with a bg_future
that raises an exception when result() is called, invoke _phase_resolve on the
bootstrapper, and verify that the exception is properly propagated. This test
should validate that exceptions from the background resolver thread are
correctly surfaced, covering the missing edge case in exception handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e01796a7-cf1c-434b-9f19-3b1fa7dfc08a

📥 Commits

Reviewing files that changed from the base of the PR and between 1d2876b and 0a4bf71.

📒 Files selected for processing (5)
  • docs/proposals/background-tasks.md
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • src/fromager/commands/bootstrap.py
  • tests/test_bootstrapper_iterative.py

Comment thread src/fromager/bootstrapper.py
Comment thread src/fromager/bootstrapper.py
@mergify

mergify Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be merged.
@dhellmann please rebase your branch.

@dhellmann dhellmann force-pushed the background-tasks branch 2 times, most recently from 1df982f to 188b4a9 Compare June 18, 2026 19:02
@dhellmann

Copy link
Copy Markdown
Member Author

Claude Code Review: background-tasks branch

One commit: feat(bootstrap): parallelize RESOLVE and PREPARE_SOURCE I/O with background threads

Overview

This PR adds a ThreadPoolExecutor to Bootstrapper to pre-fetch resolution and source download/unpack work in background threads while the main thread processes previously queued items. The design extracts I/O-heavy logic into module-level functions (no Bootstrapper state) that are safe to call from background threads. A bg_future field on WorkItem carries the pre-fetched result back to the main-thread phase handlers.


Strengths

  • Good architectural discipline: Background functions are deliberately stateless (no Bootstrapper access), closures capture only immutable values, and the invariant is clearly documented in docstrings.
  • _drain_background_pool for exclusive builds: Correct barrier semantics — drains in-flight work before an exclusive build without losing already-started tasks (cancel_futures=False).
  • Fixes a pre-existing resource leak: _unpack_metadata_from_wheel now uses with zipfile.ZipFile(...) as archive: (old code used bare zipfile.ZipFile(...) without a context manager).
  • get_cached_resolution pre-check in resolver: The comment explains the race-avoidance motivation clearly.
  • Test updates are faithful: _make_resolved_future + already-completed futures cleanly replace the patch.object(bt, "resolve_versions", ...) pattern.

Issues

Critical (Must Fix)

1. server.update_wheel_mirror(ctx) called from background threads without synchronization

  • Files: bootstrapper.py:441, bootstrapper.py:323
  • Functions: _bg_prepare_prebuilt and _download_wheel_from_cache
  • Issue: Both background workers call server.update_wheel_mirror(ctx) concurrently. If two packages are resolved to prebuilt wheels simultaneously, both threads call this. Without seeing the implementation it's unclear if it's thread-safe, but update_wheel_mirror likely regenerates an on-disk index — a classic TOCTOU race.
  • Fix: Either ensure update_wheel_mirror is idempotent+thread-safe, or move the call back to the main thread (e.g., after consuming prepared from the future in _phase_prepare_source).

Important (Should Fix)

2. Pool resource leak on exception

  • File: bootstrapper.py:2064
  • Issue: _bg_pool is shut down only in finalize(). If bootstrap() raises an uncaught exception before the caller reaches finalize(), the thread pool and any in-flight futures leak. Threads will keep running until Python garbage-collects the executor.
  • Fix: Add a try/finally in bootstrap() to call self._bg_pool.shutdown(wait=False, cancel_futures=True) on exception, or make Bootstrapper a context manager.

3. _bg_resolve silently bypasses git URL handling in resolve_versions

  • File: bootstrapper.py:386-400
  • Issue: resolve_versions handles git URL packages (lines ~580-590 in the original) before delegating to _resolver.resolve. The background _bg_resolve calls _resolver.resolve directly, which raises ValueError for git URL packages. The assumption is that git URL packages are always pre-resolved by resolve_and_add_top_level, but this invariant isn't enforced or asserted anywhere.
  • Impact: If a git URL package somehow reaches _phase_resolve through a code path that bypasses pre-resolution, the error message will be confusing.
  • Fix: Add an assertion or guard in _bg_resolve that req is not a git URL requirement, with a clear error message.

4. No concurrency tests

  • File: tests/test_bootstrapper_iterative.py, tests/test_bootstrapper.py
  • Issue: All tests use already-completed futures (_make_resolved_future). There are no tests that actually run items through the thread pool, verify ordering, or exercise the cancellation path. Concurrency bugs (like the update_wheel_mirror race) won't be detected.
  • Fix: At minimum, add an integration-style test that runs a 2-package bootstrap with num_bg_threads=2 and verifies the result is correct.

Minor (Nice to Have)

5. PreparedSourceData docstring is inaccurate

  • File: bootstrapper.py:179
  • Issue: "Exactly one of (sdist_root_dir, wheel_filename) will be set" — but when a cached source wheel is found, both sdist_root_dir and cached_wheel_filename are set together (line 416-419).
  • Fix: Revise to: "For prebuilt wheels, wheel_filename/unpack_dir are set. For source (including cache-hit), sdist_root_dir is set and cached_wheel_filename may also be set."

6. Dead inline-fallback path in _phase_prepare_source

  • File: bootstrapper.py:1646-1660
  • Issue: The comment says "falling back to inline I/O otherwise" but in practice _push_items always submits a future for PREPARE_SOURCE items, so item.bg_future will never be None here. The fallback is unreachable code.
  • Fix: Either add a comment explaining it's a defensive fallback, or add an assert item.bg_future is not None consistent with _phase_resolve.

7. Default num_bg_threads computed at import time in CLI decorator

  • File: commands/bootstrap.py:1033
  • Issue: default=max(1, (os.cpu_count() or 2) // 2) in the @click.option decorator is evaluated once at module import. On systems where cpu_count() changes (containers with dynamic CPU limits), the default is stale.
  • Fix: Use default=None with a callback, or just accept the minor limitation with a comment.

Recommendations

  1. Verify server.update_wheel_mirror thread safety before this merges — it's the highest-risk issue. If it involves subprocess or filesystem-level re-indexing, it almost certainly needs a lock.
  2. Consider exposing the num_bg_threads parameter in the Bootstrapper constructor docs/changelog since it changes observable performance characteristics.
  3. The _push_items loop over reversed(items) (line 782) submits the item that will be popped first — the comment explains this but a brief note tying it to LIFO stack ordering would help future readers.

Assessment

Ready to merge: With fixes

The core parallelism design is sound — stateless background functions, captured-variable closures, and clean barrier semantics for exclusive builds. The update_wheel_mirror thread safety issue is the blocker; fixing it and adding at least one actual concurrency test would make this ready.

@dhellmann

Copy link
Copy Markdown
Member Author

Revised Claude Code Review: background-tasks branch

One commit: feat(bootstrap): parallelize RESOLVE and PREPARE_SOURCE I/O with background threads

Overview

This PR adds a ThreadPoolExecutor to Bootstrapper to pre-fetch resolution and source download/unpack work in background threads while the main thread processes previously queued items. The design extracts I/O-heavy logic into module-level functions (no Bootstrapper state) that are safe to call from background threads. A bg_future field on WorkItem carries the pre-fetched result back to the main-thread phase handlers.


Strengths

  • Good architectural discipline: Background functions are deliberately stateless (no Bootstrapper access), closures capture only immutable values, and the invariant is clearly documented in docstrings.
  • _drain_background_pool for exclusive builds: Correct barrier semantics — drains in-flight work before an exclusive build without losing already-started tasks (cancel_futures=False).
  • Fixes a pre-existing resource leak: _unpack_metadata_from_wheel now uses with zipfile.ZipFile(...) as archive: (old code used bare zipfile.ZipFile(...) without a context manager).
  • get_cached_resolution pre-check in resolver: The comment explains the race-avoidance motivation clearly.
  • Test updates are faithful: _make_resolved_future + already-completed futures cleanly replace the patch.object(bt, "resolve_versions", ...) pattern.

Issues

Critical (Must Fix)

1. server.update_wheel_mirror(ctx) called from background threads without synchronization

  • Files: bootstrapper.py:441, bootstrapper.py:323
  • Functions: _bg_prepare_prebuilt and _download_wheel_from_cache
  • Issue: Both background workers call server.update_wheel_mirror(ctx) concurrently. If two packages are resolved to prebuilt wheels simultaneously, both threads call this. Without seeing the implementation it's unclear if it's thread-safe, but update_wheel_mirror likely regenerates an on-disk index — a classic TOCTOU race.
  • Fix: Either ensure update_wheel_mirror is idempotent+thread-safe, or move the call back to the main thread (e.g., after consuming prepared from the future in _phase_prepare_source).
  • Verification: FALSE POSITIVE — no fix needed. update_wheel_mirror in server.py is decorated with @with_thread_lock() (defined in threading_utils.py), which wraps every call in a module-level threading.Lock(). All concurrent calls serialize through that lock. The race condition does not exist. (Line numbers in the finding are also slightly off: the actual call sites are lines 404 and 286, not 441 and 323.)

Important (Should Fix)

2. Pool resource leak on exception

  • File: bootstrapper.py:2064
  • Issue: _bg_pool is shut down only in finalize(). If bootstrap() raises an uncaught exception before the caller reaches finalize(), the thread pool and any in-flight futures leak. Threads will keep running until Python garbage-collects the executor.
  • Fix: Add a try/finally in bootstrap() to call self._bg_pool.shutdown(wait=False, cancel_futures=True) on exception, or make Bootstrapper a context manager.
  • Verification: CONFIRMED — fix needed, and broader than stated. bootstrap() has no try/finally. The caller in commands/bootstrap.py also has no try/finallyfinalize() is unreachable if bootstrap() raises. Additionally, lint_requirements.py creates a Bootstrapper but never calls finalize() at all, leaking the pool on every invocation regardless of exceptions.

3. _bg_resolve silently bypasses git URL handling in resolve_versions

  • File: bootstrapper.py:386-400
  • Issue: resolve_versions handles git URL packages (lines ~580-590 in the original) before delegating to _resolver.resolve. The background _bg_resolve calls _resolver.resolve directly, which raises ValueError for git URL packages. The assumption is that git URL packages are always pre-resolved by resolve_and_add_top_level, but this invariant isn't enforced or asserted anywhere.
  • Impact: If a git URL package somehow reaches _phase_resolve through a code path that bypasses pre-resolution, the error message will be confusing.
  • Fix: Add an assertion or guard in _bg_resolve that req is not a git URL requirement, with a clear error message.
  • Verification: PARTIALLY REAL — severity overstated, but assertion is worth adding. The invariant is implicitly enforced: resolve_and_add_top_level populates the resolver cache via extend_known_versions for all git URL requirements, and BootstrapRequirementResolver.resolve() checks get_cached_resolution() before the ValueError guard. The comment at that guard even explicitly documents this design. If a git URL did somehow reach _bg_resolve uncached, it would fail loudly (not silently) through the normal error-handling path. That said, adding an assert not req.url in _bg_resolve is a low-cost defensive improvement to document-by-contract.

4. No concurrency tests

  • File: tests/test_bootstrapper_iterative.py, tests/test_bootstrapper.py
  • Issue: All tests use already-completed futures (_make_resolved_future). There are no tests that actually run items through the thread pool, verify ordering, or exercise the cancellation path. Concurrency bugs (like the update_wheel_mirror race) won't be detected.
  • Fix: At minimum, add an integration-style test that runs a 2-package bootstrap with num_bg_threads=2 and verifies the result is correct.
  • Verification: CONFIRMED — fix needed. No test in either file references num_bg_threads, ThreadPoolExecutor, executor, or submit (confirmed by grep). All TestPhaseResolve tests use pre-completed futures; all loop tests mock _dispatch_phase entirely. The entire thread-pool submission path (executor.submit(), future blocking, cancellation) is untested.

Minor (Nice to Have)

5. PreparedSourceData docstring is inaccurate

  • File: bootstrapper.py:179
  • Issue: "Exactly one of (sdist_root_dir, wheel_filename) will be set" — but when a cached source wheel is found, both sdist_root_dir and cached_wheel_filename are set together (line 416-419).
  • Fix: Revise to: "For prebuilt wheels, wheel_filename/unpack_dir are set. For source (including cache-hit), sdist_root_dir is set and cached_wheel_filename may also be set."
  • Verification: CONFIRMED — and worse than stated. The class has four fields (sdist_root_dir, cached_wheel_filename, wheel_filename, unpack_dir) but the docstring only mentions two. The "exactly one" invariant is violated in two places: cached-source path sets both sdist_root_dir and cached_wheel_filename together; prebuilt path sets both wheel_filename and unpack_dir together. Three distinct cases: (1) source no cache-hit: only sdist_root_dir; (2) cached source wheel: sdist_root_dir + cached_wheel_filename; (3) prebuilt: wheel_filename + unpack_dir.

6. Dead inline-fallback path in _phase_prepare_source

  • File: bootstrapper.py:1646-1660
  • Issue: The comment says "falling back to inline I/O otherwise" but in practice _push_items always submits a future for PREPARE_SOURCE items, so item.bg_future will never be None here. The fallback is unreachable code.
  • Fix: Either add a comment explaining it's a defensive fallback, or add an assert item.bg_future is not None consistent with _phase_resolve.
  • Verification: CONFIRMED. _bg_pool is always non-None during the bootstrap loop, so _push_items always submits a future for PREPARE_SOURCE items, and _get_background_work always returns a callable for them. The else fallback branches are unreachable. _phase_resolve uses assert item.bg_future is not None; _phase_prepare_source silently falls back instead — an inconsistency that could mask future regressions. Prefer the assertion.

7. Default num_bg_threads computed at import time in CLI decorator

  • File: commands/bootstrap.py:1033
  • Issue: default=max(1, (os.cpu_count() or 2) // 2) in the @click.option decorator is evaluated once at module import. On systems where cpu_count() changes (containers with dynamic CPU limits), the default is stale.
  • Fix: Use default=None with a callback, or just accept the minor limitation with a comment.
  • Verification: TECHNICALLY ACCURATE but not worth fixing. The default is indeed computed at import time (confirmed at lines 125 and 534). However, dynamic CPU limit changes while a Python CLI process is running are not a realistic scenario — container orchestrators set CPU limits before process start. The default is already conservative (cpu_count // 2, min 1) and users can always override with --bg-threads N. Adding a callback would increase complexity for no practical benefit. Accept with a comment if anything is done at all.

Recommendations

  1. Verify server.update_wheel_mirror thread safety before this merges — it's the highest-risk issue. If it involves subprocess or filesystem-level re-indexing, it almost certainly needs a lock.
  2. Consider exposing the num_bg_threads parameter in the Bootstrapper constructor docs/changelog since it changes observable performance characteristics.
  3. The _push_items loop over reversed(items) (line 782) submits the item that will be popped first — the comment explains this but a brief note tying it to LIFO stack ordering would help future readers.

Assessment

Ready to merge: With fixes

The core parallelism design is sound — stateless background functions, captured-variable closures, and clean barrier semantics for exclusive builds.

After verification, the priority order changes:

@dhellmann

Copy link
Copy Markdown
Member Author

Fix Plan: background-tasks branch review issues

Context

The background-tasks branch adds ThreadPoolExecutor-based background I/O to Bootstrapper.
Code review identified 7 issues; verification confirmed 5 need fixing (#1 was a false positive,
#7 not worth changing). This plan addresses the 5 confirmed issues in priority order.


Issue #2 — Pool resource leak (Important)

Problem: _bg_pool is shut down only in finalize(). Two callers have leaks:

  • commands/bootstrap.py calls bootstrap() then finalize() with no try/finally — if bootstrap() raises, finalize() is never reached
  • lint_requirements.py creates Bootstrapper but never calls finalize() at all

Fix: Make Bootstrapper a context manager.

Changes

src/fromager/bootstrapper.py — add after finalize():

def __enter__(self) -> "Bootstrapper":
    return self

def __exit__(self, exc_type: type | None, exc_val: BaseException | None, exc_tb: object) -> None:
    if self._bg_pool is not None:
        self._bg_pool.shutdown(wait=False, cancel_futures=True)
        self._bg_pool = None

Note: wait=False on exception path — don't block on cleanup. Normal path still calls
finalize() (which does wait=True) before __exit__ runs, so _bg_pool is already None.

src/fromager/commands/bootstrap.py — wrap Bootstrapper creation with with:

# Before:
bt = bootstrapper.Bootstrapper(...)
...
bt.finalize()

# After:
with bootstrapper.Bootstrapper(...) as bt:
    ...
    bt.finalize()

src/fromager/commands/lint_requirements.py — wrap with with:

# Before:
bt = bootstrapper.Bootstrapper(...)
# ... (no finalize call)

# After:
with bootstrapper.Bootstrapper(...) as bt:
    # ... (no finalize call needed — __exit__ handles cleanup)

Issue #3 — Missing assertion in _bg_resolve (Minor defensive improvement)

Problem: _bg_resolve calls _resolver.resolve() directly, bypassing the git URL guard
in resolve_versions. The invariant (git URLs always pre-cached) is implicit and unenforced.

Fix: Add assertion at the top of _bg_resolve:

def _bg_resolve(...) -> list[tuple[str, Version]]:
    """Background-safe resolution: no Bootstrapper state accessed."""
    assert not req.url, (
        f"git URL requirements must be pre-resolved before background dispatch: {req}"
    )
    ...

Issue #5PreparedSourceData docstring inaccurate (Minor)

Problem: Docstring says "Exactly one of (sdist_root_dir, wheel_filename) will be set" but
there are 4 fields set in 3 distinct combinations.

Fix: Replace docstring:

@dataclasses.dataclass
class PreparedSourceData:
    """Result of background I/O pre-fetching returned to the main thread.

    Fields are set in one of three combinations depending on the result type:

    - Source (no cache hit): only ``sdist_root_dir`` is set.
    - Source (cache hit): both ``sdist_root_dir`` and ``cached_wheel_filename`` are set.
    - Prebuilt wheel: both ``wheel_filename`` and ``unpack_dir`` are set.
    """

Issue #6 — Dead fallback in _phase_prepare_source (Minor)

Problem: _phase_prepare_source uses if item.bg_future is not None: with a silent
fallback, while _phase_resolve uses assert item.bg_future is not None. The fallback is
unreachable dead code that could mask future regressions.

Fix: Replace the conditional block with an assertion matching _phase_resolve's style.
Remove the dead fallback code entirely.

# Replace:
prepared: PreparedSourceData | None = None
if item.bg_future is not None:
    prepared = item.bg_future.result()

# With:
assert item.bg_future is not None
prepared: PreparedSourceData = item.bg_future.result()

Also remove the unreachable inline-I/O fallback branches (the else blocks).


Issue #4 — No concurrency tests (Important)

Problem: All tests use pre-completed futures (_make_resolved_future). The thread pool
submission path (executor.submit, future blocking, cancellation) is completely untested.

Fix: Add one test in tests/test_bootstrapper_iterative.py that uses a real
ThreadPoolExecutor to exercise _push_itemsexecutor.submitfuture.result() for a
RESOLVE item.

Approach:

  • Create Bootstrapper(tmp_context, num_bg_threads=1)
  • Mock bt._resolver.resolve to return a fixed synthetic result (no network)
  • Create a RESOLVE WorkItem and call bt._push_items(stack, [item])
  • Assert item.bg_future is a real concurrent.futures.Future (not pre-set)
  • Call item.bg_future.result() and assert it equals the mocked return value
  • Verify the result is processed correctly through bt._phase_resolve(item)
  • Clean up with bt.finalize()

Critical Files

File Purpose
src/fromager/bootstrapper.py Add __enter__/__exit__; fix assertion (#3), docstring (#5), dead-code (#6)
src/fromager/commands/bootstrap.py Wrap Bootstrapper with with statement
src/fromager/commands/lint_requirements.py Wrap Bootstrapper with with statement
tests/test_bootstrapper_iterative.py Add real-thread-pool test

dhellmann added a commit to dhellmann/fromager that referenced this pull request Jun 20, 2026
- Make `Bootstrapper` a context manager (`__enter__`/`__exit__`) so the
  thread pool is always cleaned up, even when an exception is raised before
  `finalize()` is called.  `lint_requirements.py` never called `finalize()`
  at all, leaking the pool on every invocation.
- Wrap `Bootstrapper` with `with` in `commands/bootstrap.py` and
  `commands/lint_requirements.py`.
- Add `assert not req.url` guard in `_bg_resolve` to document-by-contract
  that git URL requirements must be pre-resolved before background dispatch.
- Fix `PreparedSourceData` docstring — "exactly one field set" was wrong;
  replace with the three actual field-combination cases.
- Replace the dead `if item.bg_future is not None:` fallback in
  `_phase_prepare_source` with `assert item.bg_future is not None`,
  consistent with `_phase_resolve`.  Remove the unreachable inline-I/O
  fallback branches and update tests to supply pre-completed futures.
- Add `TestThreadPoolSubmission` test that uses a real `ThreadPoolExecutor`
  to exercise the `_push_items` → `submit` → `future.result()` path.

Closes: python-wheel-build#1199

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Doug Hellmann <dhellmann@redhat.com>
@dhellmann dhellmann force-pushed the background-tasks branch 2 times, most recently from fec76f9 to 6d21e35 Compare June 20, 2026 17:50
@dhellmann dhellmann marked this pull request as ready for review June 20, 2026 17:50

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/test_bootstrapper.py (1)

781-784: ⚡ Quick win

Narrow the log-prefix assertion scope to target records only.

These loops assert the prefix on every captured message, so unrelated future INFO logs on fromager.bootstrapper can fail the test despite correct behavior. Filter to the helper-specific messages (or assert at least one expected prefixed message) before checking prefixes.

Suggested change
-    for msg in messages:
-        assert msg.startswith("mypkg-1.2.3: "), (
-            f"Expected 'mypkg-1.2.3: ' prefix, got: {msg!r}"
-        )
+    target_messages = [m for m in messages if "preparing source" in m]
+    assert target_messages
+    for msg in target_messages:
+        assert msg.startswith("mypkg-1.2.3: "), (
+            f"Expected 'mypkg-1.2.3: ' prefix, got: {msg!r}"
+        )
-    for msg in messages:
-        assert msg.startswith("mypkg-1.2.3: "), (
-            f"Expected 'mypkg-1.2.3: ' prefix, got: {msg!r}"
-        )
+    target_messages = [m for m in messages if "using pre-built wheel" in m]
+    assert target_messages
+    for msg in target_messages:
+        assert msg.startswith("mypkg-1.2.3: "), (
+            f"Expected 'mypkg-1.2.3: ' prefix, got: {msg!r}"
+        )

Based on learnings, this test suite should avoid brittle log-string assertions and prefer behavior-focused checks.

Also applies to: 822-824

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_bootstrapper.py` around lines 781 - 784, The assertion loops in
the test are checking the log-prefix requirement on all captured messages,
making the test brittle to unrelated future INFO logs on fromager.bootstrapper
that would fail the test despite correct behavior. Filter the messages list to
include only the helper-specific messages that are expected to have the
mypkg-1.2.3 prefix before performing the assertion, rather than asserting on
every message in the loop. Apply this filtering fix to both assertion blocks
mentioned in the comment.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/test_bootstrap_cache.sh`:
- Around line 45-46: The grep assertions in the bootstrap cache test script use
regex mode which can match unintended strings when patterns contain dotted
versions like "78.1.0" where dots are interpreted as wildcards. Switch all grep
commands checking log assertions (at lines 45-46, 57, and 108-110) from standard
grep mode to fixed-string mode by adding the -F flag to each grep -q invocation,
so that version strings with dots are matched literally rather than as regex
patterns.

In `@e2e/test_bootstrap_prerelease.sh`:
- Line 23: The grep command uses regex pattern matching which treats dots as
metacharacters that match any character, causing unintended matches in version
strings. Replace `grep -q` with `grep -Fq` on both occurrences (the line
checking for "flit_core-2.0: new toplevel dependency flit_core<2.0.1 resolves to
2.0" and the other similar check at line 44) to use fixed-string literal
matching instead, ensuring the grep assertions match only the exact expected
version strings.

In `@src/fromager/bootstrap_requirement_resolver.py`:
- Around line 121-129: The cached resolution path does not handle the case where
get_cached_resolution() returns an empty list, causing an IndexError when trying
to access cached_result[0]. Add a check after retrieving the cached_result to
verify it is not empty before attempting to access its first element. If the
cached_result is empty, return an empty list to match the behavior of the
non-cached code path that handles empty matching results.

In `@src/fromager/commands/bootstrap.py`:
- Around line 223-229: Replace the manual context variable management using
requirement_ctxvar.set() and requirement_ctxvar.reset() with the
req_ctxvar_context() context manager to ensure the context is always reset even
if bt.resolve_and_add_top_level() raises an exception. Wrap the call to
bt.resolve_and_add_top_level() within the context manager so the per-requirement
logging context does not leak when an exception occurs. Apply the same fix to
the similar code block at lines 233-236 that uses the same pattern.

In `@src/fromager/commands/lint_requirements.py`:
- Around line 62-64: The try-except block around Requirement(line) does not
properly guard against execution continuing to later code blocks that reference
the requirement variable when InvalidRequirement is raised. When
Requirement(line) fails and raises an exception, the requirement variable
remains unassigned, but the resolve-requirements logic (around lines 81-103)
attempts to use it, causing UnboundLocalError. Fix this by ensuring that after
catching the exception for Requirement(line), the code either continues to the
next iteration of the loop or uses req_ctxvar_context() for proper error logging
and skips the resolve path entirely for that line so that undefined requirement
references are never reached.

---

Nitpick comments:
In `@tests/test_bootstrapper.py`:
- Around line 781-784: The assertion loops in the test are checking the
log-prefix requirement on all captured messages, making the test brittle to
unrelated future INFO logs on fromager.bootstrapper that would fail the test
despite correct behavior. Filter the messages list to include only the
helper-specific messages that are expected to have the mypkg-1.2.3 prefix before
performing the assertion, rather than asserting on every message in the loop.
Apply this filtering fix to both assertion blocks mentioned in the comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7f916997-faa5-48df-bf6c-6e8e1c5b3399

📥 Commits

Reviewing files that changed from the base of the PR and between 0a4bf71 and 6d21e35.

📒 Files selected for processing (13)
  • e2e/test_bootstrap_build_tags.sh
  • e2e/test_bootstrap_cache.sh
  • e2e/test_bootstrap_cooldown_prebuilt.sh
  • e2e/test_bootstrap_prerelease.sh
  • pyproject.toml
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/commands/lint_requirements.py
  • tests/test_bootstrap.py
  • tests/test_bootstrap_test_mode.py
  • tests/test_bootstrapper.py
  • tests/test_bootstrapper_iterative.py
✅ Files skipped from review due to trivial changes (2)
  • e2e/test_bootstrap_build_tags.sh
  • e2e/test_bootstrap_cooldown_prebuilt.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/fromager/bootstrapper.py

Comment thread e2e/test_bootstrap_cache.sh
Comment thread e2e/test_bootstrap_prerelease.sh
Comment thread src/fromager/bootstrap_requirement_resolver.py
Comment thread src/fromager/commands/bootstrap.py
Comment thread src/fromager/commands/lint_requirements.py
@dhellmann

Copy link
Copy Markdown
Member Author

Local testing of this with a representatively large set of input packages shows performance improvements:

single-version mode multi-version mode
before PR 46 min (full job) 17 min (bootstrap only)
after PR 37 min (full job) 13 min (bootstrap only)

All tests used bootstrap-parallel. The single-version mode test ran through to the end, including building all packages after the initial bootstrap phase. The multi-version mode stopped after bootstrapping and did not complete all of the builds, but the PR changes have no effect on that stage of the work.

@mergify

mergify Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be merged.
@dhellmann please rebase your branch.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
tests/test_bootstrapper.py (1)

775-777: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce brittleness in log-prefix assertions by targeting relevant records only.

On Line 781 and Line 823, asserting every captured message starts with an exact prefix can fail due to unrelated log lines. Filter to records emitted by the function-under-test path and assert prefix presence on those records (or assert at least one matching prefixed record), rather than requiring all captured messages to match.

Suggested test hardening
-    for msg in messages:
-        assert msg.startswith("mypkg-1.2.3: "), (
-            f"Expected 'mypkg-1.2.3: ' prefix, got: {msg!r}"
-        )
+    relevant = [m for m in messages if "preparing source" in m or "wheel" in m]
+    assert relevant, "Expected at least one relevant bootstrapper log message"
+    assert all(m.split(": ", 1)[0] == "mypkg-1.2.3" for m in relevant)
-    assert len(messages) >= 1
-    for msg in messages:
-        assert msg.startswith("mypkg-1.2.3: "), (
-            f"Expected 'mypkg-1.2.3: ' prefix, got: {msg!r}"
-        )
+    relevant = [m for m in messages if "pre-built wheel" in m or "wheel" in m]
+    assert relevant, "Expected at least one relevant bootstrapper log message"
+    assert all(m.split(": ", 1)[0] == "mypkg-1.2.3" for m in relevant)

Based on learnings, in this repo’s tests avoid asserting exact log output strings because they are brittle; prefer behavior-oriented verification. As per path instructions for tests/**, verify intended behavior and flag brittle tests.

Also applies to: 781-784, 815-817, 821-824

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_bootstrapper.py` around lines 775 - 777, The test assertions at
lines 781 and 823 are brittle because they require all captured messages to
start with the exact prefix, which fails when unrelated log lines are emitted.
Instead of collecting all messages from caplog.records without filtering, filter
the caplog.records to only include records emitted by the specific
function-under-test or logger being tested (using attributes like logger name or
module), then assert that the prefix is present on those filtered records. This
way the test verifies the intended behavior without breaking on unrelated log
output. Apply the same filtering pattern to all similar assertions mentioned in
the comment where exact log output strings are being verified.

Sources: Path instructions, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_bootstrapper.py`:
- Around line 775-777: The test assertions at lines 781 and 823 are brittle
because they require all captured messages to start with the exact prefix, which
fails when unrelated log lines are emitted. Instead of collecting all messages
from caplog.records without filtering, filter the caplog.records to only include
records emitted by the specific function-under-test or logger being tested
(using attributes like logger name or module), then assert that the prefix is
present on those filtered records. This way the test verifies the intended
behavior without breaking on unrelated log output. Apply the same filtering
pattern to all similar assertions mentioned in the comment where exact log
output strings are being verified.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 74ae9544-0642-4bd4-8970-35a53d528952

📥 Commits

Reviewing files that changed from the base of the PR and between b5d104d and b828b29.

📒 Files selected for processing (14)
  • e2e/test_bootstrap_build_tags.sh
  • e2e/test_bootstrap_cache.sh
  • e2e/test_bootstrap_cooldown_prebuilt.sh
  • e2e/test_bootstrap_prerelease.sh
  • pyproject.toml
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/commands/lint_requirements.py
  • src/fromager/resolver.py
  • tests/test_bootstrap.py
  • tests/test_bootstrap_test_mode.py
  • tests/test_bootstrapper.py
  • tests/test_bootstrapper_iterative.py
✅ Files skipped from review due to trivial changes (2)
  • e2e/test_bootstrap_build_tags.sh
  • e2e/test_bootstrap_cache.sh
🚧 Files skipped from review as they are similar to previous changes (11)
  • e2e/test_bootstrap_prerelease.sh
  • pyproject.toml
  • tests/test_bootstrap.py
  • src/fromager/resolver.py
  • e2e/test_bootstrap_cooldown_prebuilt.sh
  • src/fromager/commands/lint_requirements.py
  • src/fromager/commands/bootstrap.py
  • tests/test_bootstrap_test_mode.py
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • tests/test_bootstrapper_iterative.py

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
tests/test_bootstrapper.py (1)

781-784: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add assertion that at least one message was logged.

For consistency with test_bg_prepare_prebuilt_log_prefix_includes_version (line 821), add an assertion before the loop to ensure the test fails explicitly if no messages are logged.

🔍 Proposed fix
     finally:
         logging.setLogRecordFactory(old_factory)
 
+    assert len(messages) >= 1
     for msg in messages:
         assert msg.startswith("mypkg-1.2.3: "), (
             f"Expected 'mypkg-1.2.3: ' prefix, got: {msg!r}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_bootstrapper.py` around lines 781 - 784, The test loop that checks
message prefixes does not explicitly verify that at least one message was
logged, which means the test could pass even if the messages list is empty. Add
an assertion before the for loop iterating over messages to verify that at least
one message exists in the messages collection, ensuring the test fails
explicitly if no messages are logged, consistent with the pattern used in
test_bg_prepare_prebuilt_log_prefix_includes_version.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_bootstrapper.py`:
- Around line 781-784: The test loop that checks message prefixes does not
explicitly verify that at least one message was logged, which means the test
could pass even if the messages list is empty. Add an assertion before the for
loop iterating over messages to verify that at least one message exists in the
messages collection, ensuring the test fails explicitly if no messages are
logged, consistent with the pattern used in
test_bg_prepare_prebuilt_log_prefix_includes_version.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ead43119-875f-472f-aefa-f86c6926c4b0

📥 Commits

Reviewing files that changed from the base of the PR and between b828b29 and b196f88.

📒 Files selected for processing (14)
  • e2e/test_bootstrap_build_tags.sh
  • e2e/test_bootstrap_cache.sh
  • e2e/test_bootstrap_cooldown_prebuilt.sh
  • e2e/test_bootstrap_prerelease.sh
  • pyproject.toml
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/bootstrapper.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/commands/lint_requirements.py
  • src/fromager/resolver.py
  • tests/test_bootstrap.py
  • tests/test_bootstrap_test_mode.py
  • tests/test_bootstrapper.py
  • tests/test_bootstrapper_iterative.py
✅ Files skipped from review due to trivial changes (5)
  • e2e/test_bootstrap_cooldown_prebuilt.sh
  • e2e/test_bootstrap_prerelease.sh
  • e2e/test_bootstrap_build_tags.sh
  • e2e/test_bootstrap_cache.sh
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/fromager/resolver.py
  • tests/test_bootstrap_test_mode.py
  • src/fromager/commands/bootstrap.py
  • tests/test_bootstrap.py
  • src/fromager/commands/lint_requirements.py
  • tests/test_bootstrapper_iterative.py
  • src/fromager/bootstrapper.py

dhellmann and others added 3 commits June 23, 2026 13:33
…ground threads

Submit version resolution and source download/unpack to a thread pool
as items are pushed onto the bootstrap stack, overlapping I/O with the
main thread's serial processing.

Key design decisions:
- Background callables are module-level functions (no self capture) so
  background threads cannot accidentally access mutable Bootstrapper
  state (self.why, self._seen_requirements, etc.)
- _push_items() helper guarantees every RESOLVE and PREPARE_SOURCE item
  gets a bg_future, including the initial item in bootstrap()
- _drain_background_pool() provides an exclusive-build barrier
- BootstrapRequirementResolver cache protected by threading.Lock
- --bg-threads CLI option (min 1, default: cpu_count // 2)
- finalize() uses cancel_futures=True to avoid blocking on abort paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Doug Hellmann <dhellmann@redhat.com>
When bootstrapping finishes, overwrite the stack file with an empty
list.

Signed-off-by: Doug Hellmann <dhellmann@redhat.com>
Convert pypi_simple.errors.NoSuchProjectError to
resolvelib.resolvers.ResolverException so callers only have to track
one type of exception.

Signed-off-by: Doug Hellmann <dhellmann@redhat.com>
@mergify

mergify Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be merged.
@dhellmann please rebase your branch.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant