diff options
| -rw-r--r-- | src/notmuchworker.cpp | 22 | ||||
| -rw-r--r-- | tests/test_notmuchworker.cpp | 55 |
2 files changed, 76 insertions, 1 deletions
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp index f510fd5..f3a76ea 100644 --- a/src/notmuchworker.cpp +++ b/src/notmuchworker.cpp @@ -288,8 +288,28 @@ QByteArray NotmuchWorker::configPathArg() const bool NotmuchWorker::openReadOnly() { - if (m_db) + if (m_db) { + // A read-only handle is a Xapian SNAPSHOT taken when it was opened, so + // it never observes a write made by another process afterwards. The + // sync script's `notmuch new` is exactly that, which made mail arriving + // while the window was open invisible until the application restarted: + // not only to the post-sync refresh, but to any query the user typed by + // hand, since all of them are answered from the same handle. Item 104. + // + // Reopening here rather than at each call site covers every read path, + // which all begin by asking for the handle. It is cheap and it is what + // notmuch provides the call for; a failure is deliberately NOT fatal, + // since the existing handle is still usable and serving slightly stale + // results beats refusing to answer at all. + const notmuch_status_t status = + notmuch_database_reopen(m_db, NOTMUCH_DATABASE_MODE_READ_ONLY); + if (status != NOTMUCH_STATUS_SUCCESS) { + emit errorOccurred( + QStringLiteral("Cannot refresh notmuch database: %1") + .arg(QString::fromUtf8(notmuch_status_to_string(status)))); + } return true; + } const QByteArray configPath = configPathArg(); char *error = nullptr; 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<ThreadSummary> before; + for (const QList<QVariant> &args : ready) + before += args.at(0).value<QVector<ThreadSummary>>(); + // 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 <carol@example.org>"), + 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<ThreadSummary> after; + for (const QList<QVariant> &args : ready) + after += args.at(0).value<QVector<ThreadSummary>>(); + + QCOMPARE(after.size(), 1); + QCOMPARE(after.first().subject, QStringLiteral("Arrived mid-session")); +} + QVector<ThreadSummary> TestNotmuchWorker::runQuery( const QString &query, NotmuchWorker::SortOrder sort, bool withRecipients) { |
