Skip to content

Slice C: discovery deadlock, shared join deadline, shutdown hardening — v0.10.1 - #169

Merged
jamesabel merged 2 commits into
masterfrom
slice-c-hangs-and-shutdown
Aug 23, 2026
Merged

Slice C: discovery deadlock, shared join deadline, shutdown hardening — v0.10.1#169
jamesabel merged 2 commits into
masterfrom
slice-c-hangs-and-shutdown

Conversation

@jamesabel

Copy link
Copy Markdown
Owner

Summary

Slice C of the crash review: the hangs that read as crashes, and the shutdown-time native aborts. Two of the fixed defects were reproduced empirically during the review.

Fix Before After
GetTests joined before its queue was drained Run preparation deadlocked forever past ~400–500 test modules (child can't exit until its queue feeder flushes; pipe blocks at ~64 KB) — all controls disabled, only escape was closing the window. Reproduced: 500+ items never exit GetTests.collect(abort_event) drains while the child runs, honors abort (terminate + None)
PytestRunner.join(t) per-thread timeout Wedged pool of N workers froze closeEvent (GUI thread) for (N+1)×t — minutes of "Not Responding" ending in a user kill One shared deadline across the spin-up wait, every worker, and the runner thread
About tab QThread destroyed while running Closing before the About data arrived (git can take seconds) → Qt fatal abort 0xC0000409, no Python traceback. Reproduced About.shutdown() (quit + bounded 10 s wait), called from closeEvent
Prep thread outliving the window Ignored the abort flag through PUT detection, a 120 s uninterruptible wind-down join, DB reads and RESUME copying; could start a runner after the window was gone and emit on a destroyed QObject (C++ use-after-free race) Abort checked at every blocking phase (wind-down is a bounded abort-aware loop); emit guarded by shiboken6.isValid, with the prepared runner stopped instead of orphaned when nobody is left to adopt it
closeEvent re-entrancy Its processEvents() calls could deliver a second close (nested teardown: second dialog, second stop) and full _update_tick passes — including reconcile_process_count against a stopping runner _closing latch + refresh timer stopped before teardown

Tests

  • test_collect_drains_large_result_sets_without_deadlock — 5000-item discovery completes in seconds (hung forever before)
  • test_collect_aborts_wedged_discovery — pre-set abort terminates a wedged child promptly
  • test_join_timeout_is_a_shared_deadlinejoin(1.0) with 3 busy workers returns in ~1 s, not 3+
  • test_about_shutdown_stops_data_thread — thread finished, idempotent

Full suite: 501 passed. ruff clean; ty unchanged (13 pre-existing).

Behaviour changes

  • join(t) is now a true overall bound — callers that relied on the accidental per-thread grace get less total waiting (that laxness was the freeze).
  • Closing the window during run preparation aborts it promptly at the next phase boundary instead of letting it finish (and possibly start a runner) in the background.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KEZ9TpT9VwBujBq1S3v2Jd

jamesabel and others added 2 commits August 23, 2026 14:18
…red join deadline

Two hang fixes from the crash review (slice C):

- GetTests was joined before its queue was drained — the multiprocessing-documented
  deadlock: the child cannot exit until its queue's feeder thread flushes to the pipe,
  and the pipe blocks at ~64 KB (roughly 400-500 test modules) because nobody reads
  until after join(). Run preparation then hung forever with every control disabled.
  Reproduced empirically: 50 items exit instantly, 500+ never exit. collect(abort_event)
  now owns the wait: drains each poll, honors the abort event (terminate + None), and
  returns the sorted list. ControlWindow._build_runner uses it.

- PytestRunner.join(t) applied t per worker thread and then again to the runner thread,
  so a wedged pool of N workers held the caller for (N+1) x t — closeEvent on the GUI
  thread froze for minutes ("Not Responding", which users end with a kill). The timeout
  is now one shared deadline across the started_event wait, every worker, and the
  runner thread.

Regression tests: 5000-item discovery completes in seconds (deadlocked before), a
pre-set abort event terminates a wedged discovery promptly, and join(1.0) with three
busy workers returns in ~1 s, not 3+.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZ9TpT9VwBujBq1S3v2Jd
…tch — v0.10.1

- About.shutdown() quits and waits (bounded 10 s) the tab's data QThread; the main
  window's closeEvent calls it. Destroying a running QThread is a Qt fatal abort
  (0xC0000409, no Python traceback), and the About worker runs git — closing the app
  before its data arrived crashed at exit (reproduced during the review).
- closeEvent: re-entrancy latch and refresh-timer stop. Its processEvents() calls could
  deliver a second close request (nested teardown: second dialog, second runner stop)
  and full _update_tick passes — including reconcile_process_count against a stopping
  runner — nested inside the teardown.
- (With the previous commit) run preparation is now abort-aware at every blocking phase
  — PUT detection, prior-runner wind-down (bounded loop instead of an uninterruptible
  join(120)), discovery, and a last side-effect-free abort point before coverage
  cleanup / RESUME copying — and the prep thread's finished signal is emitted only
  after a shiboken6.isValid check, stopping the prepared runner instead of emitting
  into (or orphaning pytest processes behind) a destroyed window.

New tests: About.shutdown leaves the thread finished and is idempotent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZ9TpT9VwBujBq1S3v2Jd
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 57.14286% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.78%. Comparing base (9511ae8) to head (6d1e916).

Files with missing lines Patch % Lines
src/pytest_fly/gui/run_tab/control_window.py 4.54% 21 Missing ⚠️
src/pytest_fly/gui/gui_main.py 71.42% 2 Missing ⚠️
src/pytest_fly/gui/about_tab/about.py 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #169      +/-   ##
==========================================
- Coverage   85.90%   85.78%   -0.13%     
==========================================
  Files          74       74              
  Lines        6224     6261      +37     
==========================================
+ Hits         5347     5371      +24     
- Misses        877      890      +13     

☔ 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.

@jamesabel
jamesabel merged commit c569c3b into master Aug 23, 2026
3 checks passed
@jamesabel
jamesabel deleted the slice-c-hangs-and-shutdown branch August 23, 2026 21:27
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.

2 participants