fix: consolidate QThreadPool usage to eliminate macOS 'Task policy set failed' errors - #175
Conversation
…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.
|
Documentation reminder The following view file(s) were modified in this PR: Please review and update the relevant pages in If the UI has visually changed, capture a new screenshot for the affected screen(s) and commit it to |
There was a problem hiding this comment.
🟡 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
QThreadPoolintoStatementResultPresenterandAnonymisePresenter, and route worker startup through that pool instead ofQThreadPool.globalInstance(). - Thread the injected pool through UI entry points (
DebugInfoDialog,AdminPresenter) so spawned tools (e.g., Anonymise) use the same pool. - Refactor
UpdateCheckeroffthreading.ThreadontoQRunnable+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.
Address Copilot review comment — UpdateChecker now accepts an injected threadpool instead of using QThreadPool.globalInstance(), completing the single-pool consolidation.
…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>
Problem
Running
uv run openstanproduces repeated macOS kernel log messages:These are benign — the app functions correctly — but indicate conflicting thread QoS management in the XNU kernel.
Root Cause
Two competing
QThreadPoolinstances were creating threads independently:Stan.__init__) — used byStatementQueuePresenter,ExportDataPresenter,AdvancedExportPresenter,RunReportsPresenterQThreadPool.globalInstance()) — used byStatementResultPresenterandAnonymisePresenterWhen both pools create threads simultaneously, macOS kernel QoS tracking conflicts. Additionally,
UpdateCheckerused a rawthreading.Thread(not Qt-managed) to emit Qt signals, which can fail QoS assignment during thread teardown.Fix
StatementResultPresenterandAnonymisePresenternow accept an injectedthreadpoolparameter (with fallback to a private pool) instead of usingQThreadPool.globalInstance()UpdateChecker: Replaced rawthreading.ThreadwithQRunnable+ the global pool for proper Qt thread managementStancreates one privateQThreadPooland passes it to all presenters via constructor injectionFiles Changed
src/openstan/main.py— pass threadpool toStatementResultPresentersrc/openstan/presenters/statement_result_presenter.py— accept/use injected threadpoolsrc/openstan/presenters/anonymise_presenter.py— accept/use injected threadpoolsrc/openstan/presenters/admin_presenter.py— pass threadpool toAnonymisePresentersrc/openstan/views/debug_info_dialog.py— pass threadpool through toAnonymisePresentersrc/openstan/updater.py— convert fromthreading.ThreadtoQRunnableVerification
uv run ruff check .— all checks passeduv run ruff format --check .— 48 files already formatteduv run pyrefly check— 0 errorsuv run pytest tests/ -v— 146 passed