summaryrefslogtreecommitdiffstats
path: root/src/mainwindow.h
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-06 19:12:17 +0200
committerDanilo M. <danix@danix.xyz>2026-08-06 19:12:17 +0200
commitddb4ac26b7fb553c9349401dda4510636c67cc9b (patch)
tree30a5840ad8f1a5081b945b1ac98d5769c9e55532 /src/mainwindow.h
parent46c82a09903730133b06e28140d29f16b68813f4 (diff)
downloadqtmaildir-ddb4ac26b7fb553c9349401dda4510636c67cc9b.tar.gz
qtmaildir-ddb4ac26b7fb553c9349401dda4510636c67cc9b.zip
fix(sync): hold tag edits made during a background sync
A tag edit sent while another process holds notmuch's write lock does not fail: the read-write open blocks and then succeeds. Measured against Slackware's notmuch, 9.158s against a 12s hold, status SUCCESS. Since the worker is a single thread, that blocked open holds up every read queued behind it, so the message pane freezes on whichever thread was selected first and replays the queue when the lock releases. The window now defers instead. While SyncMonitor reports a sync running, a tag change is held rather than sent, and flushed when the sync ends. The optimistic update stands in the meantime, so the row keeps its tag and the edit still counts toward the unsynced indicator, which is what the quit prompt reads. The original diagnosis was that the open fails and the edit is discarded, and a retry was built on it. That was wrong: the error branch in notmuchworker.cpp is unreachable through lock contention. The premise was taken from a plausible-looking error path without provoking the condition, and measurement disproved it. The backlog entry records this rather than quietly correcting it. Verified by hand against a real blocking open, which the tests cannot reach: they drive the deferral through the meta-object and never take a lock. Both locks held for 100s with a tag edit made during the hold. Row kept the tag, status did not expire, indicator rose, window stayed responsive, held edit sent itself on release. The 2s SyncMonitor polling window is knowingly left open: a sync starting between polls is invisible for up to 2s and an edit there still blocks. SyncMonitor::lockHeldIn() would close it at the cost of one file read per tag action, and is recorded as the option to revisit. Also fixes revertPendingTagChange() clearing the entire undo stack after any rejected write, found while working on this. Backlog: item 37 done, and item 46 added for a test that fails only under the offscreen platform, where an 800x800 screen clamps a restored 940px window. Pre-existing and unrelated; the suite is green otherwise.
Diffstat (limited to 'src/mainwindow.h')
-rw-r--r--src/mainwindow.h38
1 files changed, 38 insertions, 0 deletions
diff --git a/src/mainwindow.h b/src/mainwindow.h
index cf9aca6..22c8187 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -68,6 +68,15 @@ public:
/// load is discarded rather than painted, so no thread can reappear.
QString currentThreadId() const { return m_currentThreadId; }
+ /// True while an edit is held back because a sync holds the write lock.
+ /// Exposed for tests: the deferral is otherwise only observable by watching
+ /// the worker, which test_mainwindow has no database to drive.
+ bool hasEditAwaitingSend() const { return !m_heldEdits.isEmpty(); }
+
+ /// Whether the undo stack still holds anything. Exposed so a test can show
+ /// that a rejected write did not take unrelated history down with it.
+ bool canUndo() const { return m_undoStack.canUndo(); }
+
/// The cid: namespace prefix for the nth message of a thread.
///
/// MainWindow is the only producer of this value in the application. It
@@ -231,6 +240,35 @@ private:
/// Undoes the optimistic model update for a write the worker rejected.
void revertPendingTagChange();
+ /// Whether a write sent now would block the worker on notmuch's write lock.
+ ///
+ /// True only for a sync KNOWN to be running. `SyncMonitor::State::Unknown`
+ /// deliberately does not count: it means `/proc/locks` could not be read,
+ /// and holding every edit on a platform that cannot observe the lock at all
+ /// would strand them permanently.
+ bool aSyncHoldsTheWriteLock() const;
+
+ /// Sends every edit held while the lock was busy, oldest first.
+ void flushHeldEdits();
+
+ /// A tag change not yet sent to the worker, because a sync held the write
+ /// lock when the user made it.
+ ///
+ /// Held rather than sent because the read-write open BLOCKS: measured
+ /// 9.158s against a 12s lock hold, returning SUCCESS, not an error. Sending
+ /// into that freezes the worker thread, so every later query and thread
+ /// load queues behind it. The rows show the change meanwhile, which is
+ /// honest: it is what the user asked for and it is going to be applied.
+ struct HeldEdit {
+ QStringList threadIds;
+ TagChange change;
+ };
+
+ /// FIFO, because a sync lasts ~35s and the user can keep tagging through
+ /// it. Order matters: two edits touching one thread must reach the database
+ /// in the order they were made, or the later one does not win.
+ QVector<HeldEdit> m_heldEdits;
+
friend class ThreadTagCommand;
Config m_config;