From b1db3e878a6fb468dc2c572c7080c572e3bd1ac5 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 25 Aug 2026 12:46:31 +0200 Subject: fix(worker): reopen the notmuch handle so queries see new mail A read-only notmuch handle is a Xapian snapshot taken when it is opened, so it never observes a write made by another process afterwards. The worker opened one handle and kept it for the process lifetime, which made the sync script's `notmuch new` invisible: every query after startup was answered from the index as it stood when the application launched. The symptom was mail arriving while the window was open and not appearing until a restart. It was not confined to the post-sync refresh, which is what made it hard to place: a query typed by hand also found nothing, since it hits the same handle. Tag writes were unaffected throughout, because applyTags opens its own read-write handle per call. Reopen in openReadOnly() rather than at each call site: every read path begins by asking for the handle. A reopen failure is deliberately not fatal, since the existing handle is still usable and answering from a slightly stale index beats refusing to answer. The suite could not reproduce this before: the test helper builds a fresh worker per query, so it opens a fresh handle every time. The new test holds one worker across two queries and indexes between them from a second process. Item 104. --- tests/test_notmuchworker.cpp | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) (limited to 'tests') diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp index 7574ace..e1a21cd 100644 --- a/tests/test_notmuchworker.cpp +++ b/tests/test_notmuchworker.cpp @@ -104,6 +104,8 @@ private slots: void aSplitIndexListsTheMaildirsFolders(); void twoMessagesMovedTogetherGetDistinctNames(); + void aQuerySeesMailIndexedAfterTheWorkerOpened(); + private: /// Adds one read message in `folder` and reindexes, for the move tests. /// Each of those takes its own message, because a move is destructive and @@ -186,6 +188,59 @@ void TestNotmuchWorker::initTestCase() QVERIFY2(m_fixture.index(), qPrintable(m_fixture.error())); } +void TestNotmuchWorker::aQuerySeesMailIndexedAfterTheWorkerOpened() +{ + // The defect this covers is item 104, and it is the reason mail arriving + // while the window is open was invisible until the application restarted. + // + // A read-only notmuch handle is a Xapian SNAPSHOT taken when it is opened. + // `notmuch new` runs in a separate process, so nothing it writes is visible + // to a handle already open, however long it is held and however many + // queries are run through it. The worker opens once and keeps that handle + // for the process lifetime, so every query after the first sync answered + // from a stale index: a refresh missed the mail, and so did a query the + // user typed by hand, which is what ruled out the model and the generation + // counter when this was diagnosed. + // + // ONE worker across both queries is the whole point. The runQuery() helper + // builds a fresh worker per call, which opens a fresh handle and therefore + // cannot reproduce this at all: a test written through it passes against + // the bug. + NotmuchWorker worker(m_fixture.configPath()); + + const QString query = QStringLiteral("subject:\"Arrived mid-session\""); + + { + QSignalSpy ready(&worker, &NotmuchWorker::threadsReady); + worker.runQuery(query, 1); + QVector before; + for (const QList &args : ready) + before += args.at(0).value>(); + // Establishes that the handle is open and the query is well-formed, + // rather than leaving "found nothing" to mean either. + QCOMPARE(before.size(), 0); + } + + // A second process writes to the index, exactly as the sync script's + // `notmuch new` does while the window is open. + QVERIFY(m_fixture.addMessage(QStringLiteral("inbox"), + QStringLiteral("mid@example.org"), + QStringLiteral("Arrived mid-session"), + QStringLiteral("Carol "), + QStringLiteral("Tue, 9 Jun 2026 10:00:00 +0000"), + QStringLiteral("new mail"))); + QVERIFY2(m_fixture.index(), qPrintable(m_fixture.error())); + + QSignalSpy ready(&worker, &NotmuchWorker::threadsReady); + worker.runQuery(query, 2); + QVector after; + for (const QList &args : ready) + after += args.at(0).value>(); + + QCOMPARE(after.size(), 1); + QCOMPARE(after.first().subject, QStringLiteral("Arrived mid-session")); +} + QVector TestNotmuchWorker::runQuery( const QString &query, NotmuchWorker::SortOrder sort, bool withRecipients) { -- cgit v1.2.3