Skip to content

Post jobs_changed to the scheduler instead of a dangling singleShot - #2629

Merged
m3nu merged 1 commit into
masterfrom
fix/announce-job-dangling-emit
Nov 15, 2026
Merged

m3nu merged 1 commit into
masterfrom
fix/announce-job-dangling-emit

Conversation

@m3nu

@m3nu m3nu commented Nov 15, 2026

Copy link
Copy Markdown
Contributor

Description

SchedulerState._announce_job queued jobs_changed with QTimer.singleShot(0, self.scheduler.jobs_changed.emit). The pending call does not keep the scheduler alive. If the scheduler is freed before the event loop delivers the call, emit runs on a freed QObject: a segfault, or AttributeError: 'VortaScheduler' does not have a signal with the signature jobs_changed() when the memory has been reused.

_announce_job now queues the signal with QMetaObject.invokeMethod(self.scheduler, 'jobs_changed', QueuedConnection). The event is posted to the scheduler itself, so Qt discards it if the scheduler is deleted first.

Related Issue

Root cause of #2595. Also the macOS failure of #2621.

Motivation and Context

The app's own scheduler lives as long as the app, so users are very unlikely to hit this. The unit tests hit it on macOS: init_db teardown drops the backup_finished_event connection that keeps each test-created VortaScheduler alive, while one of its announcements is still queued. A later GC pass frees the scheduler, and the next test's qtbot.waitSignal delivers the call. That's why the crash always showed up inside a later test's event loop and moved with GC timing (test_recording_a_skip_emits_jobs_changed in #2595, test_create_backup_records_a_running_job in #2621, 2 of 2 attempts each).

The Python traceback only showed the event loop. A native crash report from a diagnostic CI run (.ips from ~/Library/Logs/DiagnosticReports) showed the main thread in sendPostedEvents → PyQtSlotProxy::unislot → PyQtSlot::call → cfunction_call, faulting at 0x45. A standalone script reproduces it 3 of 3 times:

o = Obj()  # QObject with a reference cycle
QtCore.QTimer.singleShot(0, o.sig.emit)
del o; gc.collect()
app.exec()  # segfault

The same script with invokeMethod doesn't crash, and still delivers the signal once when the object is alive.

How Has This Been Tested?

  • New tests/unit/test_scheduler.py::test_announced_job_outlives_its_scheduler queues an announcement on a scheduler, frees the scheduler and spins the event loop. 3 of 3 passed with the fix. 3 of 3 failed without it (one segfault, two of the AttributeError above).
  • tests/unit locally on macOS, Python 3.12, Borg 1.4.3: 626 passed, 8 skipped.
  • Return consistent parent() rows and bounds-check index() in FileTreeModel #2621's commit plus this fix on the macOS CI job that had crashed twice: 580 passed, 7 skipped (run 43716244818).
  • make lint hooks pass on the changed files.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have read the CONTRIBUTING guide.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

I provide my contribution under the terms of the license of this repository and I affirm the Developer Certificate of Origin.

`QTimer.singleShot(0, scheduler.jobs_changed.emit)` does not keep the
scheduler alive. When a scheduler is freed before the event loop runs
the call, `emit` dereferences the freed QObject and segfaults, or reads
garbage. The unit tests hit this on macOS: `init_db` teardown releases
test-created schedulers with an announcement still queued, and the next
`waitSignal` delivers it (#2595, macOS job of #2621).

Queue the emit with `QMetaObject.invokeMethod` on the scheduler, so Qt
discards it together with the scheduler.
@m3nu
m3nu merged commit 88a3556 into master Nov 15, 2026
5 checks passed
@m3nu
m3nu deleted the fix/announce-job-dangling-emit branch November 15, 2026 13:09
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.

1 participant