Skip to content

fix: consolidate QThreadPool usage to eliminate macOS 'Task policy set failed' errors - #175

Merged
boscorat merged 2 commits into
masterfrom
fix/consolidate-threadpool
Aug 29, 2026
Merged

fix: consolidate QThreadPool usage to eliminate macOS 'Task policy set failed' errors#175
boscorat merged 2 commits into
masterfrom
fix/consolidate-threadpool

Conversation

@boscorat

Copy link
Copy Markdown
Owner

Problem

Running uv run openstan produces repeated macOS kernel log messages:

Task policy set failed: 4 ((os/kern) invalid argument)

These are benign — the app functions correctly — but indicate conflicting thread QoS management in the XNU kernel.

Root Cause

Two competing QThreadPool instances were creating threads independently:

  1. Private pool (Stan.__init__) — used by StatementQueuePresenter, ExportDataPresenter, AdvancedExportPresenter, RunReportsPresenter
  2. Global pool (QThreadPool.globalInstance()) — used by StatementResultPresenter and AnonymisePresenter

When both pools create threads simultaneously, macOS kernel QoS tracking conflicts. Additionally, UpdateChecker used a raw threading.Thread (not Qt-managed) to emit Qt signals, which can fail QoS assignment during thread teardown.

Fix

  • Consolidate to a single pool: StatementResultPresenter and AnonymisePresenter now accept an injected threadpool parameter (with fallback to a private pool) instead of using QThreadPool.globalInstance()
  • Convert UpdateChecker: Replaced raw threading.Thread with QRunnable + the global pool for proper Qt thread management
  • Threadpool flow: Stan creates one private QThreadPool and passes it to all presenters via constructor injection

Files Changed

  • src/openstan/main.py — pass threadpool to StatementResultPresenter
  • src/openstan/presenters/statement_result_presenter.py — accept/use injected threadpool
  • src/openstan/presenters/anonymise_presenter.py — accept/use injected threadpool
  • src/openstan/presenters/admin_presenter.py — pass threadpool to AnonymisePresenter
  • src/openstan/views/debug_info_dialog.py — pass threadpool through to AnonymisePresenter
  • src/openstan/updater.py — convert from threading.Thread to QRunnable

Verification

  • uv run ruff check . — all checks passed
  • uv run ruff format --check . — 48 files already formatted
  • uv run pyrefly check — 0 errors
  • uv run pytest tests/ -v — 146 passed

…rrors

Replace two competing QThreadPool instances (private + global) with a
single shared pool. StatementResultPresenter and AnonymisePresenter now
accept an injected threadpool instead of using
QThreadPool.globalInstance(). Convert UpdateChecker from raw
threading.Thread to QRunnable for proper Qt thread management.
@github-actions

Copy link
Copy Markdown

Documentation reminder

The following view file(s) were modified in this PR:

src/openstan/views/debug_info_dialog.py

Please review and update the relevant pages in docs/screens/ before merging.

If the UI has visually changed, capture a new screenshot for the affected screen(s) and commit it to docs/assets/screenshots/. See docs/assets/screenshots/SCREENSHOTS.md for the filename, required app state, and capture instructions for each screen.

Copilot AI left a comment

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.

🟡 Changes recommended

UpdateChecker still uses QThreadPool.globalInstance() which can reintroduce a second concurrent pool and undermine the PR’s goal of consolidating threadpool usage to avoid macOS QoS warnings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR aims to eliminate benign but noisy macOS kernel QoS warnings by standardizing background work onto a consistent Qt-managed threading approach, primarily via QThreadPool injection across presenters and dialogs.

Changes:

  • Inject a shared QThreadPool into StatementResultPresenter and AnonymisePresenter, and route worker startup through that pool instead of QThreadPool.globalInstance().
  • Thread the injected pool through UI entry points (DebugInfoDialog, AdminPresenter) so spawned tools (e.g., Anonymise) use the same pool.
  • Refactor UpdateChecker off threading.Thread onto QRunnable + QThreadPool.
File summaries
File Description
src/openstan/main.py Passes the app’s shared threadpool into StatementResultPresenter.
src/openstan/presenters/statement_result_presenter.py Adds threadpool injection and starts workers on the injected pool; forwards pool to DebugInfoDialog.
src/openstan/presenters/anonymise_presenter.py Adds threadpool injection and starts anonymisation workers on the injected pool.
src/openstan/presenters/admin_presenter.py Ensures the Anonymise tool uses Stan.threadpool.
src/openstan/views/debug_info_dialog.py Accepts a threadpool and passes it through to AnonymisePresenter.
src/openstan/updater.py Replaces threading.Thread with QRunnable run via a QThreadPool.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/openstan/updater.py Outdated
Address Copilot review comment — UpdateChecker now accepts an injected
threadpool instead of using QThreadPool.globalInstance(), completing the
single-pool consolidation.
@boscorat
boscorat merged commit 28214ad into master Aug 29, 2026
2 checks passed
boscorat added a commit that referenced this pull request Aug 30, 2026
…t failed' errors (#175)

* fix: consolidate QThreadPool usage to eliminate dual-pool macOS QoS errors

Replace two competing QThreadPool instances (private + global) with a
single shared pool. StatementResultPresenter and AnonymisePresenter now
accept an injected threadpool instead of using
QThreadPool.globalInstance(). Convert UpdateChecker from raw
threading.Thread to QRunnable for proper Qt thread management.

* fix: inject threadpool into UpdateChecker for consistency

Address Copilot review comment — UpdateChecker now accepts an injected
threadpool instead of using QThreadPool.globalInstance(), completing the
single-pool consolidation.

---------

Co-authored-by: Jason Farrar <stillwaters.technology@gmail.com>
@boscorat
boscorat deleted the fix/consolidate-threadpool branch August 30, 2026 07:40
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