Skip to content

v26.9.1 — Off-UI-Thread I/O and Radio Backend Cleanup - #26

Merged
NF0T merged 6 commits into
mainfrom
develop
Sep 14, 2026
Merged

NF0T merged 6 commits into
mainfrom
develop

Conversation

@NF0T

@NF0T NF0T commented Sep 14, 2026

Copy link
Copy Markdown
Owner

First release of September 2026. Moves the two remaining sources of UI-thread blocking — ADIF import and Hamlib CAT I/O — onto background threads, and fixes a data-integrity gap in custom SQLite deployments.

Highlights

Closes #17, #18, #20, #22.

See ROADMAP.md's new v26.9.1 entry for full detail.

🤖 Generated with Claude Code

NF0T and others added 6 commits September 13, 2026 19:33
Runs the ADIF parse and insert loop asynchronously via QtConcurrent::run()
on a background worker with its own dedicated database connection. Progress
and cooperative cancellation are marshaled back to the UI thread via
QFutureWatcher.

Key improvements and fixes:
- Offloads ADIF parsing and insertion off the UI thread (fixes #17)
- Batches database inserts in 500-record transactions (kImportBatchSize)
  to optimize SQLite WAL write performance and yield write locks to readers
- Throttles progress updates to every 25 records (kProgressUpdateStride)
  to eliminate UI thread event-queue congestion
- Introduces setImportLock() to lock down table editing, deletions, filter bar,
  and settings without interrupting digital mode UDP listeners (WSJT-X)
- Safely manages QProgressDialog lifetime via QPointer and deleteLater()
- Guards against QFutureWatcher::result() assertion abort on canceled imports
- Passes station location parameters (grid, lat, lon) by value to avoid
  repeated QSettings disk/registry queries on worker threads
- Configures PRAGMA busy_timeout=5000 on SqliteBackend for concurrent access
- Routes File -> Exit through MainWindow::close() so in-progress guards run

Fixes #17
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
HamlibBackend's 500ms poll issued up to three blocking Hamlib calls per
cycle, and connectRadio()/disconnectRadio() also blocked; a slow or
unresponsive serial/network link could stall the whole UI for seconds.
Moves the HamlibBackend instance to its own QThread so Hamlib I/O never
runs on the UI thread, per the approved RFC in #18.

RadioBackend::connectRadio()/disconnectRadio() are now slots so
MainWindow's call sites dispatch via QMetaObject::invokeMethod(...,
Qt::QueuedConnection) instead of calling across thread affinity
directly. HamlibBackend drops its QObject parent (moveToThread()
refuses parented objects) and MainWindow owns/deletes it explicitly;
m_connected becomes atomic since it's read on the UI thread and
written on the worker thread.

Independently reviewed by Antigravity/Gemini and Claude Code, each
verifying the other's claims rather than deferring to them. Fixes that
came out of that review:
- poll() now tracks consecutive read failures and disconnects with an
  error() after 3, instead of silently retrying forever with a stale
  "Connected" indicator on a dead rig
- connectRadio() dropped its always-unused bool return
- HamlibBackend's four public slots assert same-thread-affinity, so a
  future cross-thread misuse fails loudly instead of racing silently
  on the RIG* handle
- Connect actions now disable eagerly on click and re-enable on a
  failed attempt, closing a re-entrancy window that async dispatch
  opened (a double-click could previously queue two connects)
- The destructor's blocking teardown (a known, accepted limitation —
  Hamlib's blocking C API has no cancellation hook) is now documented
  explicitly at the point it applies

Remaining architectural debt (the whole-object moveToThread() shape,
and the exit-time blocking teardown) is tracked in #22 for a proper
Worker-Object split rather than blocking this PR.

Fixes #18
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- currentBackendConfig() now resolves through Settings::resolvedSqlitePath(), honoring a user-configured custom SQLite path instead of always using the AppLocalDataLocation default (previously ignored entirely).
- setMigrationLock() now disables m_logView, m_filterBar, m_exportAdifAction, and m_settingsAction while a migration is running, mirroring the UI coverage setImportLock() already has.
- Fixed a split-brain hazard found in adversarial review: onImportAdif() now targets the database m_db was actually opened with (cached in m_activeDbConfig/m_activeDbBackendKey) instead of re-reading Settings, which could otherwise point a background import at a different database than the one currently open if Settings changed mid-session.
- currentBackendConfig() now creates the parent directory for custom SQLite paths too, not just the default location.
- onExportAdif(), onEditQso(), onDeleteSelectedQso(), and onExportSelectedQsos() now check m_importLock/m_migrationLock for consistency with the rest of the lock coverage.

Fixes #20. Independently reviewed and re-verified by Antigravity/Gemini (adversarial review + re-review approval on PR #23).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…er (#24)

Implements the worker-object pattern proposed in #22. HamlibWorker now owns
the RIG* handle, the poll timer, and every blocking Hamlib call, living on
its own QThread. HamlibBackend goes back to being a normal QObject parented
to MainWindow, so RadioBackend call sites in MainWindow no longer need
QMetaObject::invokeMethod ceremony - connectRadio()/disconnectRadio() are
plain virtual calls again, and MainWindow relies on Qt's parent-child
lifetime management instead of manually deleting the backend.

Shutdown keeps the same accepted tradeoff as PR #21: ~HamlibBackend() still
blocks the UI thread waiting for the worker's current Hamlib call to
finish, because Hamlib's blocking C API has no cancellation hook.

Hardened per adversarial review (Antigravity): HamlibWorker now has a
destructor that closes the rig defensively, the destructor's blocking
disconnect call is guarded against a worker thread that never started or
already stopped, and the shutdown-rationale comment was corrected to cite
the real constraint - Qt refusing to destroy a QThread member while it's
still running, not a RIG* use-after-free.

Closes #22.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@NF0T
NF0T merged commit 03e4403 into main Sep 14, 2026
8 checks passed
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.

[RFC] Move ADIF import off the UI thread

1 participant