aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/notmuchworker.cpp22
-rw-r--r--tests/test_notmuchworker.cpp55
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)
{