Skip to content

fix: prevent Python 3.12 CI hang by excluding real_providers tests and hardening timeouts#337

Merged
bashandbone merged 3 commits intomainfrom
copilot/fix-python-3-12-ci-failures
Apr 16, 2026
Merged

fix: prevent Python 3.12 CI hang by excluding real_providers tests and hardening timeouts#337
bashandbone merged 3 commits intomainfrom
copilot/fix-python-3-12-ci-failures

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

Summary

Fixes Python 3.12 CI test failures caused by real_providers tests running in standard CI (they should only run in nightly builds) and hardens timeout handling as defense-in-depth.

Root Cause Analysis

The primary cause was that real_providers tests (e.g. test_full_pipeline_index_then_search) were not excluded from CI test markers. These tests use the indexed_test_project fixture which performs full model loading and embedding generation through native code (ONNX runtime, tokenizers). On Python 3.12, this operation hangs — the CI runner consistently receives a "shutdown signal" after ~5 minutes.

Three compounding issues prevented detection:

  1. real_providers tests not excluded from CI markers — heavy tests requiring full model loading/indexing were running on every push instead of only in nightly builds
  2. timeout_func_only = true in pyproject.toml meant pytest-timeout only covered the test body, NOT fixture setup/teardown — so the fixture hang ran unchecked
  3. timeout_method = "thread" (the default) can't interrupt native C code (ONNX inference, model loading) — even if the timeout fired, it couldn't kill the stuck operation
  4. No job-level timeout in the CI workflow — the GHA default is 6 hours

Changes

.github/workflows/ci.yml

  • Add not real_providers to CI test markers — these heavy tests should only run in nightly builds, not on every push

pyproject.toml

  • Set timeout_func_only = false so pytest timeout covers the entire test lifecycle (setup + call + teardown)

tests/integration/conftest.py

  • Wrap index_project() in asyncio.wait_for(timeout=300) with a clear error message, providing a Python-level timeout for the fixture's async indexing operation

.github/workflows/_reusable-test.yml

  • Add timeout-minutes: 20 as a job-level safety net
  • Use signal timeout method (--timeout-method=signal) conditionally on Linux/macOS only — signal can interrupt native code via SIGALRM but is not supported on Windows (the reusable workflow is also used by weekly Windows tests)

Context

  • Python 3.12 tests previously passed but took 12 minutes (vs 9.5min for 3.13, 4.5min for 3.14)
  • The real_providers tests are designed for nightly runs (nightly-tests.yml) where model downloads are pre-warmed and longer timeouts are expected
  • The timeout hardening changes provide defense-in-depth for any future fixture hangs

Copilot AI added 2 commits April 13, 2026 02:10
- Set timeout_func_only=false in pyproject.toml so pytest timeout covers
  fixture setup/teardown (previously only test body was covered)
- Add asyncio.wait_for(timeout=300) in indexed_test_project fixture to
  catch hangs during model loading or embedding generation
- Add timeout-minutes: 20 to CI test job as safety net
- Use signal timeout method in CI for all tests (can interrupt native code)"

Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/f35eacec-3469-4488-98b4-b20056964e04
Copilot AI requested review from Copilot and removed request for Copilot April 13, 2026 02:14
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 13, 2026

Reviewer's Guide

Improves CI robustness against Python 3.12 hangs by expanding pytest timeouts to cover fixtures, adding an explicit async timeout around project indexing in an integration fixture, and tightening CI workflow behavior with a job-level timeout and signal-based pytest timeouts.

Sequence diagram for fixture indexing with asyncio timeout

sequenceDiagram
    participant Pytest
    participant IndexedTestProjectFixture as indexed_test_project_fixture
    participant Asyncio as asyncio
    participant IndexProject as index_project
    participant NativeCode as native_model_loading_and_indexing

    Pytest->>IndexedTestProjectFixture: request indexed_test_project
    IndexedTestProjectFixture->>Asyncio: wait_for(index_project, timeout=300)
    Asyncio->>IndexProject: start indexing
    IndexProject->>NativeCode: load_models_and_generate_embeddings

    alt indexing completes before 300s
        NativeCode-->>IndexProject: embeddings_generated
        IndexProject-->>Asyncio: indexing_result
        Asyncio-->>IndexedTestProjectFixture: result
        IndexedTestProjectFixture-->>Pytest: provide indexed_test_project
        Pytest->>Pytest: execute test body
    else indexing exceeds 300s
        Asyncio-->>IndexedTestProjectFixture: TimeoutError
        IndexedTestProjectFixture-->>Pytest: raise fixture_timeout_error
        Pytest->>Pytest: mark test as failed_due_to_fixture_timeout
    end
Loading

Flow diagram for pytest timeout coverage before and after change

flowchart LR
    subgraph Before
        B_Start["start pytest"] --> B_Setup["run setup\n(fixtures)"]
        B_Setup --> B_Call["run test call"]
        B_Call --> B_Teardown["run teardown"]
        B_Timeout["pytest-timeout\n(timeout-func-only true)"] --> B_Call
    end

    subgraph After
        A_Start["start pytest"] --> A_Setup["run setup\n(fixtures)"]
        A_Setup --> A_Call["run test call"]
        A_Call --> A_Teardown["run teardown"]
        A_Timeout["pytest-timeout\n(timeout-func-only false)\n(timeout-method signal)"] --> A_Setup
        A_Timeout --> A_Call
        A_Timeout --> A_Teardown
    end
Loading

File-Level Changes

Change Details Files
Add a job-level timeout and enforce signal-based pytest timeouts in CI to ensure stuck tests are terminated.
  • Set a 20-minute timeout on the reusable test job so hung runs are aborted at the workflow level.
  • Run real-provider tests with an explicit 900s timeout and the pytest signal timeout method to allow SIGALRM to interrupt blocking native code.
  • Run all other CI tests with the pytest signal timeout method so fixture and test hangs in native code are interruptible.
.github/workflows/_reusable-test.yml
Guard the indexed project integration fixture with an asyncio-level timeout and clear failure reporting.
  • Wrap the async index_project(force_reindex=True) call in asyncio.wait_for(..., timeout=300) to cap indexing time at 5 minutes.
  • Catch timeout from the indexing operation and fail the test via pytest.fail() with a message pointing to native-code hangs in model loading or embedding generation.
tests/integration/conftest.py
Apply pytest-timeout to the full test lifecycle so fixture setup/teardown is covered.
  • Change timeout_func_only from true to false so timeouts apply to setup, call, and teardown phases, including fixtures that perform model loading or indexing.
  • Document in comments that this change is specifically to catch fixture hangs like those seen in the indexed_test_project fixture on Python 3.12.
pyproject.toml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI requested a review from bashandbone April 13, 2026 02:18
@bashandbone bashandbone marked this pull request as ready for review April 13, 2026 02:21
Copilot AI review requested due to automatic review settings April 13, 2026 02:21
@github-actions
Copy link
Copy Markdown
Contributor

🤖 Hi @Copilot, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions
Copy link
Copy Markdown
Contributor

🤖 I'm sorry @Copilot, but I was unable to process your request. Please see the logs for more details.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In the indexed_test_project fixture, consider catching asyncio.TimeoutError rather than the base TimeoutError so you don't inadvertently mask other timeout exceptions raised inside index_project().
  • You might want to centralize the 300s timeout configuration (e.g., via a pytest marker or constant) rather than hard-coding it in the fixture, so timeout behavior remains consistent and easier to tune alongside the global pytest-timeout settings.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the `indexed_test_project` fixture, consider catching `asyncio.TimeoutError` rather than the base `TimeoutError` so you don't inadvertently mask other timeout exceptions raised inside `index_project()`.
- You might want to centralize the 300s timeout configuration (e.g., via a pytest marker or constant) rather than hard-coding it in the fixture, so timeout behavior remains consistent and easier to tune alongside the global pytest-timeout settings.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the test/CI timeout strategy to prevent Python 3.12 integration runs from hanging indefinitely during fixture setup (notably during project indexing and model/embedding initialization).

Changes:

  • Extend pytest-timeout coverage to include fixture setup/teardown (timeout_func_only = false).
  • Add an explicit asyncio.wait_for(..., timeout=300) guard around index_project() inside the indexed_test_project integration fixture.
  • Add a job-level GitHub Actions timeout and force CI runs to use pytest-timeout’s signal method.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
tests/integration/conftest.py Adds an asyncio-level timeout guard around integration fixture indexing to avoid indefinite hangs.
pyproject.toml Applies pytest timeout to the entire lifecycle (fixtures included) so setup/teardown hangs are caught.
.github/workflows/_reusable-test.yml Adds a job timeout and enforces --timeout-method=signal for CI test execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 211 to 215
if [[ "${TEST_MARKERS}" == *"real_providers"* ]]; then
mise run test-cov -m "${TEST_MARKERS}" --timeout=900 --timeout-method=signal
else
mise run test-cov -m "${TEST_MARKERS}"
mise run test-cov -m "${TEST_MARKERS}" --timeout-method=signal
fi
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

--timeout-method=signal is not supported on Windows (pytest-timeout only supports the signal method on Unix-like platforms). Since this workflow is reused with runner-version: windows-latest (e.g. weekly-tests), this change will cause those runs to error. Consider conditionally using signal only on Linux/macOS and falling back to thread on Windows, or gating the step/args on ${{ runner.os }}/$RUNNER_OS.

Copilot uses AI. Check for mistakes.
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.

This is absolutely correct! Windows doesn't support SIGALRM, so the signal method will fail on Windows runners. The workflow needs to conditionally use signal on Linux/macOS and fall back to thread on Windows.

I've submitted a full review with a recommended fix that checks ${{ runner.os }} to select the appropriate timeout method. Thank you for catching this!

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

This PR addresses a critical issue with Python 3.12 CI hangs through a multi-layered timeout approach. The solution is well-thought-out and the PR description provides excellent context about the root cause. However, there is a critical Windows compatibility issue that must be addressed before merging.

Critical Issue

Windows Compatibility Problem with --timeout-method=signal

The changes in .github/workflows/_reusable-test.yml now apply --timeout-method=signal to all test runs (lines 212, 214). However, the signal method is not supported on Windows because Windows doesn't support SIGALRM.

This workflow is used by weekly-tests.yml with runner-version: windows-latest (line 67), which means the weekly Windows tests will fail with an error like:

pytest: error: argument --timeout_method: invalid choice: 'signal' (choose from 'thread')

Recommended Fix

Conditionally apply the signal method based on the runner OS:

run: |
  # Use signal timeout method on Unix-like platforms so SIGALRM can interrupt
  # blocking C code (ONNX runtime inference, model loading).
  # Windows doesn't support SIGALRM, so it falls back to thread method.
  # Combined with timeout_func_only=false in pyproject.toml,
  # this ensures fixture hangs (e.g. indexed_test_project) are
  # caught and reported instead of running until job timeout.
  if [[ "${{ runner.os }}" == "Windows" ]]; then
    TIMEOUT_METHOD="thread"
  else
    TIMEOUT_METHOD="signal"
  fi
  
  if [[ "${TEST_MARKERS}" == *"real_providers"* ]]; then
    mise run test-cov -m "${TEST_MARKERS}" --timeout=900 --timeout-method="${TIMEOUT_METHOD}"
  else
    mise run test-cov -m "${TEST_MARKERS}" --timeout-method="${TIMEOUT_METHOD}"
  fi

Alternatively, you could use the PowerShell-compatible approach or simply omit the --timeout-method flag on Windows (pytest-timeout defaults to thread on Windows automatically).

Positive Aspects

  1. Excellent root cause analysis - The PR description clearly explains the three compounding issues
  2. Multi-layered defense - Combining pytest configuration changes, fixture-level timeouts, and job-level timeouts is a robust approach
  3. Clear documentation - The code comments explain the reasoning well
  4. Appropriate use of asyncio.wait_for - The fixture-level timeout in tests/integration/conftest.py is a good Python-level safeguard with a clear error message

Additional Observations

  1. pyproject.toml change (timeout_func_only=false) - This is correct and necessary. The previous setting left fixtures uncovered, which was the root cause of undetected hangs.

  2. Job-level timeout (20 minutes) - This is a sensible safety net. The comment mentions tests typically take 4.5-12 minutes, so 20 minutes provides adequate buffer.

  3. asyncio.wait_for usage - The 300-second (5-minute) timeout in the fixture is reasonable for indexing operations.

  4. Import placement - Good that the second commit fixed the asyncio import to be at the top level (line 11 in conftest.py).

Test Coverage

Please verify that the fix has been tested or will be tested on:

  • ✅ Linux (currently running)
  • ⚠️ Windows (will fail without the OS-conditional fix)
  • ⚠️ macOS (should work with signal method)

Recommendation

Request Changes - The Windows compatibility issue must be fixed before merging. Once the OS-conditional timeout method is implemented, this will be a solid fix for the Python 3.12 hang issues.

@bashandbone
Copy link
Copy Markdown
Contributor

@copilot While this is a step in the right direction, I note that 3.12 tests are still failing in CI. Also, please incorporate this feedback

…onditional

- Add `not real_providers` to CI test markers — these heavy tests with
  full model loading/indexing should only run in nightly builds, not on
  every push. This was the actual root cause of the 3.12 hang.
- Make `--timeout-method=signal` conditional on $RUNNER_OS to avoid
  breaking Windows weekly tests (signal not supported on Windows).
- Revert accidental mise.toml hk version change."

Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/90b7c085-c5bb-419a-bb95-a2a4c8abb04d
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 13, 2026

@copilot While this is a step in the right direction, I note that 3.12 tests are still failing in CI. Also, please incorporate [this feedback](#337 (comment)...

Fixed in b9e0812. The primary issue was that real_providers tests (like test_full_pipeline_index_then_search) were not excluded from CI markers — they were running on every push instead of only in nightly builds. Added not real_providers to the CI test markers in ci.yml.

Also addressed the Windows feedback: --timeout-method=signal is now conditional on $RUNNER_OS != "Windows", falling back to the default thread method from pyproject.toml on Windows.

Additionally reverted the accidental mise.toml hk version change that was still in the diff.

Copilot AI changed the title fix: prevent Python 3.12 CI hang with timeout coverage for fixtures fix: prevent Python 3.12 CI hang by excluding real_providers tests and hardening timeouts Apr 13, 2026
@bashandbone bashandbone merged commit 2e93960 into main Apr 16, 2026
15 checks passed
@bashandbone bashandbone deleted the copilot/fix-python-3-12-ci-failures branch April 16, 2026 03:33
bashandbone pushed a commit that referenced this pull request Apr 16, 2026
…d hardening timeouts (#337)

* fix: prevent Python 3.12 CI hang with timeout coverage for fixtures

- Set timeout_func_only=false in pyproject.toml so pytest timeout covers
  fixture setup/teardown (previously only test body was covered)
- Add asyncio.wait_for(timeout=300) in indexed_test_project fixture to
  catch hangs during model loading or embedding generation
- Add timeout-minutes: 20 to CI test job as safety net
- Use signal timeout method in CI for all tests (can interrupt native code)"

Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/f35eacec-3469-4488-98b4-b20056964e04

* fix: move asyncio import to top level, revert accidental mise.toml change

Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/f35eacec-3469-4488-98b4-b20056964e04

* fix: exclude real_providers from CI markers, make signal timeout OS-conditional

- Add `not real_providers` to CI test markers — these heavy tests with
  full model loading/indexing should only run in nightly builds, not on
  every push. This was the actual root cause of the 3.12 hang.
- Make `--timeout-method=signal` conditional on $RUNNER_OS to avoid
  breaking Windows weekly tests (signal not supported on Windows).
- Revert accidental mise.toml hk version change."

Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/90b7c085-c5bb-419a-bb95-a2a4c8abb04d

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants