Skip to content

fix: use time.monotonic() in ResourceGovernor to prevent Python 3.12 CI hang#336

Merged
bashandbone merged 2 commits into
mainfrom
copilot/fix-failing-tests-3-12
Apr 12, 2026
Merged

fix: use time.monotonic() in ResourceGovernor to prevent Python 3.12 CI hang#336
bashandbone merged 2 commits into
mainfrom
copilot/fix-failing-tests-3-12

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 12, 2026

Python 3.12 CI consistently hangs at test_full_pipeline_index_then_search (~19% progress) until the runner is killed. Root cause: three test fixtures globally mock time.time(), which the ResourceGovernor uses for timeout enforcement. The mock returns barely-incrementing values so timeouts never fire. Combined with timeout_func_only = true (pytest-timeout skips fixture setup), any slow operation during indexing runs forever. Python 3.13/3.14 happen to be fast enough to complete; 3.12 is not.

Changes

  • ResourceGovernor: time.time()time.monotonic() for all elapsed-time calculations. monotonic() is the correct clock for interval measurement and is unaffected by time.time mocks.

  • Integration fixtures (configured_providers, real_providers, indexed_test_project): Remove the global time.time mock entirely. It was added speculatively ("for deterministic behavior if needed") but defeated timeout enforcement, circuit breakers, and any other time.time-based safety mechanism across the entire indexing pipeline.

  • Governance unit test: Mock target updated from time.time to time.monotonic.

Summary by Sourcery

Switch ResourceGovernor to use a monotonic clock for timeout tracking and remove brittle global time mocking in integration tests to prevent hangs and make timeout behavior reliable.

Bug Fixes:

  • Prevent CI hangs by basing ResourceGovernor timeout enforcement on time.monotonic() instead of time.time(), avoiding interference from mocked wall-clock time.
  • Eliminate global time.time mocking in integration fixtures that could disable time-based safeguards throughout the indexing pipeline.

Enhancements:

  • Improve robustness of chunking governance by consistently using a monotonic clock for elapsed-time measurement and observability metadata.

Tests:

  • Adjust governance unit test to mock time.monotonic() instead of time.time() and simplify integration fixtures by dropping unnecessary time mocking.

….time mocks from fixtures

Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 12, 2026 19:49
@bashandbone bashandbone marked this pull request as ready for review April 12, 2026 19:49
Copilot AI review requested due to automatic review settings April 12, 2026 19:49
@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.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 12, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Switch ResourceGovernor timeout accounting to use time.monotonic() and remove global time.time mocking from integration fixtures so timeouts and safety mechanisms work reliably in CI, plus update the corresponding unit test.

Sequence diagram for ResourceGovernor timeout enforcement with time.monotonic

sequenceDiagram
    actor Pytest
    participant IntegrationTest
    participant ResourceGovernor
    participant time_time
    participant time_monotonic

    Pytest->>IntegrationTest: run test_full_pipeline_index_then_search
    IntegrationTest->>ResourceGovernor: enter context
    activate ResourceGovernor
    ResourceGovernor->>time_monotonic: monotonic()
    time_monotonic-->>ResourceGovernor: start_time
    ResourceGovernor-->ResourceGovernor: _start_time = start_time

    loop chunking loop
        IntegrationTest->>ResourceGovernor: process next chunk
        ResourceGovernor->>ResourceGovernor: _chunk_count++
        ResourceGovernor->>ResourceGovernor: check_timeout()
        ResourceGovernor->>time_monotonic: monotonic()
        time_monotonic-->>ResourceGovernor: now
        ResourceGovernor-->ResourceGovernor: elapsed = now - _start_time
        ResourceGovernor->>ResourceGovernor: check_chunk_limit()
        Note over time_time,Pytest: time.time may be globally mocked<br/>but time.monotonic is not affected
        alt elapsed > settings.chunk_timeout_seconds or _chunk_count > settings.max_chunks_per_file
            ResourceGovernor-->>IntegrationTest: raise ResourceLimitViolation
            deactivate ResourceGovernor
        end
    end

    Pytest-->>IntegrationTest: test completes without hanging
Loading

Updated class diagram for ResourceGovernor timeout accounting

classDiagram
    class ResourceGovernorSettings {
        float chunk_timeout_seconds
        int max_chunks_per_file
    }

    class ResourceGovernor {
        - float _start_time
        - int _chunk_count
        + ResourceGovernor(ResourceGovernorSettings settings)
        + __enter__() ResourceGovernor
        + __exit__(exception_type, exception_value, traceback) bool
        + check_timeout() void
        + check_chunk_limit() void
    }

    ResourceGovernor --> ResourceGovernorSettings : uses

    class time {
        + monotonic() float
    }

    ResourceGovernor ..> time : uses_monotonic_for_elapsed_time
Loading

File-Level Changes

Change Details Files
Use a monotonic clock for ResourceGovernor timeout and limit enforcement.
  • Initialize ResourceGovernor start time with time.monotonic() instead of time.time() in enter
  • Compute elapsed time for timeout checks using time.monotonic()
  • Populate chunk limit violation context with elapsed_seconds derived from time.monotonic() when available
src/codeweaver/engine/chunker/governance.py
Remove global time.time mocking from integration fixtures to avoid interfering with timeout behavior.
  • Simplify configured_providers fixture to just apply container overrides and clear them after yielding without patching time.time
  • Simplify real_providers fixture to just apply container overrides and clear them after yielding without patching time.time
  • Resolve and run IndexingService in indexed_test_project fixture without patching time.time around indexing and project path configuration
tests/integration/conftest.py
Align governance unit test with the switch to time.monotonic().
  • Change ResourceGovernor timeout test to drive mock_time.monotonic side effects instead of mock_time.time
tests/unit/engine/chunker/test_governance.py

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

@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 found 1 issue, and left some high level feedback:

  • Since ResourceGovernor now depends on time.monotonic, consider injecting a clock function (defaulting to time.monotonic) to make timeout behavior easier to unit test and to decouple it from the global time module.
  • You’ve removed global time.time mocking from a few fixtures; it may be worth briefly scanning other integration/conftest helpers for any remaining time-based mocks that could similarly interfere with timeout/circuit-breaker behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Since `ResourceGovernor` now depends on `time.monotonic`, consider injecting a clock function (defaulting to `time.monotonic`) to make timeout behavior easier to unit test and to decouple it from the global `time` module.
- You’ve removed global `time.time` mocking from a few fixtures; it may be worth briefly scanning other integration/conftest helpers for any remaining time-based mocks that could similarly interfere with timeout/circuit-breaker behavior.

## Individual Comments

### Comment 1
<location path="src/codeweaver/engine/chunker/governance.py" line_range="159-161" />
<code_context>
                 actual_value=float(self._chunk_count),
                 extra_context={
-                    "elapsed_seconds": time.time() - self._start_time if self._start_time else 0
+                    "elapsed_seconds": time.monotonic() - self._start_time if self._start_time else 0
                 },
             )
</code_context>
<issue_to_address>
**suggestion:** Use an explicit None check for `_start_time` instead of relying on float truthiness.

Relying on `_start_time`’s truthiness is slightly brittle for a float and could behave unexpectedly if it’s ever `0.0` or another falsy value. Prefer an explicit `None` check instead:

```python
"elapsed_seconds": (
    time.monotonic() - self._start_time
    if self._start_time is not None
    else 0
)
```

```suggestion
                extra_context={
                    "elapsed_seconds": (
                        time.monotonic() - self._start_time
                        if self._start_time is not None
                        else 0
                    )
                },
```
</issue_to_address>

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.

Comment thread src/codeweaver/engine/chunker/governance.py
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

Updates chunking governance and integration test fixtures to prevent Python 3.12 CI hangs caused by global time.time() mocking interfering with timeout enforcement.

Changes:

  • Switch ResourceGovernor elapsed-time calculations from time.time() to time.monotonic().
  • Remove global time.time() patching from integration fixtures to avoid disabling timeouts/circuit breakers.
  • Update the governance unit test to mock time.monotonic() instead of time.time().

Reviewed changes

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

File Description
src/codeweaver/engine/chunker/governance.py Use time.monotonic() for timeout/elapsed calculations in ResourceGovernor.
tests/integration/conftest.py Remove global time.time() mocking from provider/indexing-related fixtures.
tests/unit/engine/chunker/test_governance.py Update unit test mock to align with time.monotonic() usage.

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

actual_value=float(self._chunk_count),
extra_context={
"elapsed_seconds": time.time() - self._start_time if self._start_time else 0
"elapsed_seconds": time.monotonic() - self._start_time if self._start_time else 0
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

elapsed_seconds uses a truthiness check on self._start_time. If _start_time is 0.0 (possible in tests when time.monotonic() is mocked, and theoretically possible depending on the clock’s epoch), this will log 0 instead of the real elapsed time. Use an explicit is not None check (or guard earlier) so 0.0 is treated as a valid start time.

Suggested change
"elapsed_seconds": time.monotonic() - self._start_time if self._start_time else 0
"elapsed_seconds": (
time.monotonic() - self._start_time
if self._start_time is not None
else 0
)

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

Code Review - PR #336

Summary

This PR addresses a critical CI hang issue in Python 3.12 tests by switching from time.time() to time.monotonic() for elapsed time measurements in the ResourceGovernor and removing harmful global time mocks from integration test fixtures. The changes are well-reasoned and correctly implemented.

✅ Strengths

  1. Root Cause Analysis: The PR description provides excellent diagnosis - the global time.time() mocks in test fixtures were returning barely-incrementing values that prevented ResourceGovernor timeout enforcement from working, causing infinite hangs when combined with timeout_func_only = true in pytest-timeout.

  2. Correct Clock Choice: Using time.monotonic() instead of time.time() for elapsed time measurement is the proper fix:

    • monotonic() is designed specifically for measuring intervals and cannot go backwards
    • Immune to system clock adjustments (NTP, DST, manual changes)
    • This is a best practice in Python timing code
  3. Comprehensive Fix: All three uses of time.time() in ResourceGovernor were updated:

    • governance.py:86 - Setting _start_time in __enter__
    • governance.py:118 - Calculating elapsed in check_timeout()
    • governance.py:160 - Computing elapsed_seconds for logging in check_chunk_limit()
  4. Test Consistency: The unit test mock was correctly updated from mock_time.time.side_effect to mock_time.monotonic.side_effect in test_governance.py:45

  5. Fixture Cleanup: Removing the speculative global time.time() mocks from three integration fixtures (configured_providers, real_providers, indexed_test_project) eliminates a source of flakiness and correctly allows timeout enforcement to work as designed.

🔍 Observations

  1. Other time.time() Usage: I searched the codebase and found time.time() is still used in several other places, but these appear to be for different purposes (circuit breaker timestamps, wall-clock timestamps, performance benchmarking) where time.time() may be appropriate. The ResourceGovernor change is isolated and correct.

  2. Semantic Chunker Test: There's a test comment in test_semantic_errors.py:208 that mentions "Mock time.time() to simulate elapsed time exceeding timeout". This test uses monkeypatch.setattr(time, "time", mock_time) but this appears to be testing a different code path (SemanticChunker) and may need updating if SemanticChunker also uses ResourceGovernor internally. However, I don't see this causing immediate issues.

🎯 Recommendations

Optional Enhancement (not blocking):
Consider updating the docstring comment in test_semantic_errors.py:208 if that test path also now uses monotonic() through ResourceGovernor, to keep documentation accurate.

Security & Performance

  • ✅ No security concerns
  • ✅ Performance impact is negligible (time.monotonic() is as fast as time.time())
  • ✅ Actually improves robustness by ensuring timeouts work correctly

Conclusion

This is a high-quality fix that addresses a real production issue with the correct solution. The use of time.monotonic() for interval measurement is a best practice, and removing the harmful global time mocks prevents test-induced production code failures.

Recommendation: Approve

The changes are correct, well-tested, and follow Python best practices for time measurement. The CI tests should now complete reliably across all Python versions.

@github-actions
Copy link
Copy Markdown
Contributor

PR Review

I've reviewed this PR and the changes look excellent overall. The fix correctly addresses the root cause of Python 3.12 CI hangs by switching from time.time() to time.monotonic() for elapsed time measurement.

Summary of Changes

Correct approach: Using time.monotonic() instead of time.time() for timeout enforcement

  • time.monotonic() is the proper clock for measuring elapsed time intervals
  • Immune to system clock adjustments, NTP corrections, and test mocks of time.time()
  • This is exactly what Python's documentation recommends for this use case

Root cause fix: Removed harmful global time.time mocks from integration test fixtures

  • These mocks were causing timeouts to never fire, leading to infinite hangs
  • The mocks provided minimal value ("for deterministic behavior if needed") while breaking timeout enforcement throughout the entire indexing pipeline

Test update: Correctly updated the unit test to mock time.monotonic instead of time.time

Code Quality

The implementation is clean and follows best practices:

  • Proper use of monotonic clock for interval timing
  • Minimal, focused changes that don't introduce new complexity
  • Good test coverage for the timeout mechanism

Addressing the Review Comment

Regarding the comment on governance.py:160 about the truthiness check on self._start_time:

The suggested change is technically correct but has zero practical impact:

# Current (potentially problematic if _start_time is 0.0)
"elapsed_seconds": time.monotonic() - self._start_time if self._start_time else 0

# Suggested (explicitly checks for None)
"elapsed_seconds": (
    time.monotonic() - self._start_time
    if self._start_time is not None
    else 0
)

Why this edge case is theoretical only:

  1. time.monotonic() returns seconds since an arbitrary reference point that is implementation-defined but typically:

    • On Linux: seconds since boot
    • On Windows: seconds since system start
    • Never starts at 0.0 in practice for running systems
  2. The only scenario where _start_time = 0.0 could occur:

    • Tests that explicitly mock time.monotonic.return_value = 0.0
    • System boot at the exact instant __enter__ is called (virtually impossible)
  3. Even if _start_time = 0.0 occurred, the current code would log 0 for elapsed seconds, which is exactly what the suggested code would also log - the only difference is the suggested version uses explicit is not None which is more defensive.

My recommendation: The suggested change improves code clarity and defensive programming, making the intent explicit (None means inactive, any float including 0.0 is valid). While not fixing a real bug, it's a good practice improvement worth including.

Overall Assessment

This PR demonstrates excellent debugging work - identifying that global time mocks were defeating timeout enforcement and fixing it at the root cause level. The switch to time.monotonic() is the correct solution.

Status: Awaiting CI test results (Python 3.12/3.13/3.14 tests are currently running). Once tests pass, I recommend approval with optional consideration of the truthiness check improvement suggested in the review comment.

The core fix is sound and will resolve the Python 3.12 CI hangs. 🎯

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.

Additional Notes on Review Comment (governance.py:160)

Regarding the truthiness check suggestion on elapsed_seconds:

The suggestion is correct and improves defensive programming. While time.monotonic() = 0.0 is extremely unlikely in production (would require system boot at the exact instant __enter__ is called), the explicit is not None check is better practice:

# Current (works but theoretically fragile)
"elapsed_seconds": time.monotonic() - self._start_time if self._start_time else 0

# Better (explicit None check)  
"elapsed_seconds": (
    time.monotonic() - self._start_time
    if self._start_time is not None
    else 0
)

Benefits of the suggested change:

  1. Makes intent explicit: None = inactive, any float (including 0.0) = valid timestamp
  2. Handles edge case in tests where time.monotonic might be mocked to return 0.0
  3. More Pythonic - explicit is not None is clearer than truthy check

Recommendation: Apply this suggestion as a defensive programming improvement, even though it's not fixing a practical bug in the current code.


Overall, this PR is excellent work identifying and fixing the root cause of Python 3.12 CI hangs. The core solution (switching to time.monotonic() and removing harmful time mocks) is exactly right. Once the CI tests pass, this should be merged. 🚀

Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 12, 2026 19:57
@bashandbone bashandbone merged commit c168e9b into main Apr 12, 2026
12 checks passed
@bashandbone bashandbone deleted the copilot/fix-failing-tests-3-12 branch April 12, 2026 19:57
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

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.


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

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