Skip to content

🧪 test_calc_job: fix daemon-restart flake - #7452

Open
GeigerJ2 wants to merge 2 commits into
aiidateam:mainfrom
GeigerJ2:fix/7152/restart-daemon-test-polling
Open

🧪 test_calc_job: fix daemon-restart flake#7452
GeigerJ2 wants to merge 2 commits into
aiidateam:mainfrom
GeigerJ2:fix/7152/restart-daemon-test-polling

Conversation

@GeigerJ2

@GeigerJ2 GeigerJ2 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Bug Fixes
    • Improved process wait timeout handling by using a clock unaffected by system time changes.
    • Increased reliability when waiting for jobs to complete after daemon restarts, particularly on busy systems.

`test_restart_after_daemon_reset` hand-rolled a polling loop with a
10-second budget covering the entire stop, restart, state reload, resume
and finish cycle. That bound is too tight on a contended runner.
Measured locally, the post-restart wait is ~3.2s on an idle machine but
degrades smoothly under CPU contention, reaching ~9s at moderate load,
near enough the bound to trip intermittently. The process always
completed, so the failure is a timeout on a slow job rather than a lost
or stuck one.

Replace the loop with `submit_and_await`, the fixture the test already
uses to await the `WAITING` state a few lines above. It polls
identically but allows a longer budget and, on expiry, embeds the daemon
log in the error rather than reporting a bare `current state:
ProcessState.WAITING`, which carried no diagnostic.

Measure the fixture's own timeout with `time.monotonic` instead of
`time.time`. The latter is wall-clock and can jump under NTP correction,
which is not what an elapsed-time bound wants.
`brokers/zeromq/broker.py` already uses `monotonic` for its readiness
deadline.

This addresses the ZMQ timeout symptom observed for this test in issue
separate race at task-publish time and is untouched here.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The process-awaiting fixtures now use monotonic clocks, floating-point timeouts, and explicit terminal-state handling. The daemon reset regression test replaces manual polling with submit_and_await and uses a 30-second timeout after restart.

Changes

Process waiting and restart regression

Layer / File(s) Summary
Monotonic await timeout
src/aiida/tools/pytest_fixtures/daemon.py, src/aiida/manage/tests/pytest_fixtures.py
The fixtures accept floating-point timeouts, poll the current process state, detect unexpected terminal states, and include the latest state and daemon log in timeout errors.
Restart completion wait
tests/engine/processes/calcjobs/test_calc_job.py
The restart regression test replaces manual polling with submit_and_await(..., timeout=30) after daemon restart.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RestartTest
  participant SubmitAndAwait
  participant ProcessNode
  RestartTest->>SubmitAndAwait: await restarted process with timeout=30
  SubmitAndAwait->>ProcessNode: poll process_state
  ProcessNode-->>SubmitAndAwait: return current state
  SubmitAndAwait-->>RestartTest: return on FINISHED or raise on failure/timeout
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided, so there is no meaningful summary to assess. Add a brief description of the primary changes and why they were made.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: fixing a daemon-restart flake in test_calc_job.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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.

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.33333% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.06%. Comparing base (d5bb1b5) to head (32f8958).
⚠️ Report is 29 commits behind head on main.

Files with missing lines Patch % Lines
src/aiida/manage/tests/pytest_fixtures.py 0.00% 12 Missing ⚠️
src/aiida/tools/pytest_fixtures/daemon.py 66.67% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7452      +/-   ##
==========================================
- Coverage   80.61%   80.06%   -0.54%     
==========================================
  Files         580      580              
  Lines       46722    46730       +8     
==========================================
- Hits        37658    37408     -250     
- Misses       9064     9322     +258     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

`test_restart_after_daemon_reset` hand-rolled a polling loop with a
10-second budget covering the entire stop, restart, state reload, resume
and finish cycle. That bound is too tight on a contended runner.
Measured locally, the post-restart wait is ~3.2s on an idle machine but
degrades smoothly under CPU contention, reaching ~9s at moderate load,
near enough the bound to trip intermittently. The process always
completed, so the failure is a timeout on a slow job rather than a lost
or stuck one.

Replace the loop with `submit_and_await`, the fixture the test already
uses to await the `WAITING` state a few lines above. It polls
identically but allows a longer budget and, on expiry, embeds the daemon
log in the error rather than reporting a bare `current state:
ProcessState.WAITING`, which carried no diagnostic. The fixture has
accepted a process node since well before this test was written, so the
duplicated loop was never a workaround for a missing capability.

Measure the fixture's own timeout with `time.monotonic` instead of
`time.time`. The latter is wall-clock and can jump under NTP correction,
which is not what an elapsed-time bound wants.
`brokers/zeromq/broker.py` already uses `monotonic` for its readiness
deadline.

This addresses the ZMQ timeout symptom seen for this test. The RabbitMQ
`UnroutableError` reported for it previously is a separate race at
task-publish time and is untouched here.

@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

🤖 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/aiida/tools/pytest_fixtures/daemon.py`:
- Line 120: Use the Sphinx-style :return: field consistently in both factory
docstrings: in src/aiida/tools/pytest_fixtures/daemon.py lines 120-120, rename
:returns: to :return:, and in src/aiida/manage/tests/pytest_fixtures.py lines
733-741, add the factory’s return description.
- Around line 159-162: Assign the excepted-process error text to msg before
raising RuntimeError in the daemon.py process-state wait logic, and apply the
same change in pytest_fixtures.py at the specified sibling site; preserve the
existing message content and non-excepted state 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2122316c-46ed-4601-ae98-7d5c8847b583

📥 Commits

Reviewing files that changed from the base of the PR and between 68e5e54 and 32f8958.

📒 Files selected for processing (3)
  • src/aiida/manage/tests/pytest_fixtures.py
  • src/aiida/tools/pytest_fixtures/daemon.py
  • tests/engine/processes/calcjobs/test_calc_job.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/engine/processes/calcjobs/test_calc_job.py

:raises RuntimeError: If the process fails to achieve the specified state before the timeout expires.
:raises RuntimeError: If the process terminates in a state other than the one specified, or if it fails to
achieve the specified state before the timeout expires.
:returns `~aiida.orm.nodes.process.process.ProcessNode`: The process node.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use :return: consistently in both factory docstrings.

  • src/aiida/tools/pytest_fixtures/daemon.py#L120-L120: replace :returns: with :return:.
  • src/aiida/manage/tests/pytest_fixtures.py#L733-L741: add the factory’s :return: field.

As per coding guidelines, use Sphinx-style docstrings with :param:, :return:, and :raises:.

📍 Affects 2 files
  • src/aiida/tools/pytest_fixtures/daemon.py#L120-L120 (this comment)
  • src/aiida/manage/tests/pytest_fixtures.py#L733-L741
🤖 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/aiida/tools/pytest_fixtures/daemon.py` at line 120, Use the Sphinx-style
:return: field consistently in both factory docstrings: in
src/aiida/tools/pytest_fixtures/daemon.py lines 120-120, rename :returns: to
:return:, and in src/aiida/manage/tests/pytest_fixtures.py lines 733-741, add
the factory’s return description.

Source: Coding guidelines

Comment on lines +159 to +162
if current_state is ProcessState.EXCEPTED:
raise RuntimeError(f'The process excepted: {node.exception}')
msg = f'The process terminated in state `{current_state}` while waiting for state `{state}`.'
raise RuntimeError(msg)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assign the exception message before raising in both fixtures.

  • src/aiida/tools/pytest_fixtures/daemon.py#L159-L162: assign the excepted-process message to msg before raise RuntimeError(msg).
  • src/aiida/manage/tests/pytest_fixtures.py#L766-L769: apply the same pattern.

As per coding guidelines, assign exception messages to a variable before raising.

📍 Affects 2 files
  • src/aiida/tools/pytest_fixtures/daemon.py#L159-L162 (this comment)
  • src/aiida/manage/tests/pytest_fixtures.py#L766-L769
🤖 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/aiida/tools/pytest_fixtures/daemon.py` around lines 159 - 162, Assign the
excepted-process error text to msg before raising RuntimeError in the daemon.py
process-state wait logic, and apply the same change in pytest_fixtures.py at the
specified sibling site; preserve the existing message content and non-excepted
state handling.

Source: Coding guidelines

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

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

test_calc_job.py::test_restart_after_daemon_reset failed with kiwipy.UnroutableError

2 participants