[PATCH 1/3] lib: handle DatabaseModifiedError in _notmuch_message_create

Subject: [PATCH 1/3] lib: handle DatabaseModifiedError in _notmuch_message_create

Date: Mon, 7 Jul 2025 14:27:06 +0200

To: notmuch@notmuchmail.org

Cc:

From: Anton Khirnov


If an open database is modified sufficiently by other callers, the open
instance becomes invalid and operations on it throw
DatabaseModifiedError. Per Xapian documentation, the caller is then
supposed to reopen the database and restart the query. This exception is
currently not handled in _notmuch_message_create(), leading to the
default handler abort()ing the process.

Catch this exception in _notmuch_message_create() and return an error
instead of crashing. Since the entire query becomes invalid - including
results that have already been read by the caller - this situation
cannot be handled by libnotmuch transparently. A new public function -
notmuch_messages_status() - is added to allow the callers to check
whether the messages iterator was exhausted or terminated early due to
a runtime error. This also allows memory allocation failure to be
signalled to the caller.
---
 lib/database.cc                         |  4 ++
 lib/message.cc                          |  4 ++
 lib/messages.c                          | 31 +++++++++++----
 lib/notmuch-private.h                   |  3 ++
 lib/notmuch.h                           | 53 ++++++++++++++++++++++---
 lib/query.cc                            | 13 ++++--
 test/T641-database-modified-messages.sh | 34 +++++++++++++---
 7 files changed, 120 insertions(+), 22 deletions(-)

diff --git a/lib/database.cc b/lib/database.cc
index 737a3f30..8f687eee 100644
--- a/lib/database.cc
+++ b/lib/database.cc
@@ -313,6 +313,10 @@ notmuch_status_to_string (notmuch_status_t status)
 	return "Syntax error in query";
     case NOTMUCH_STATUS_NO_MAIL_ROOT:
 	return "No mail root found";
+    case NOTMUCH_STATUS_ITERATOR_EXHAUSTED:
+	return "Iterator exhausted";
+    case NOTMUCH_STATUS_OPERATION_INVALIDATED:
+	return "Operation invalidated due to concurrent database modification";
     default:
     case NOTMUCH_STATUS_LAST_STATUS:
 	return "Unknown error status value";
diff --git a/lib/message.cc b/lib/message.cc
index 46638f80..ea815efe 100644
--- a/lib/message.cc
+++ b/lib/message.cc
@@ -212,6 +212,10 @@ _notmuch_message_create (const void *talloc_owner,
 	if (status)
 	    *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
 	return NULL;
+    } catch (const Xapian::DatabaseModifiedError &error) {
+	if (status)
+	    *status = NOTMUCH_PRIVATE_STATUS_OPERATION_INVALIDATED;
+	return NULL;
     }
 
     return _notmuch_message_create_for_document (talloc_owner, notmuch,
diff --git a/lib/messages.c b/lib/messages.c
index eec0a162..619b1d2f 100644
--- a/lib/messages.c
+++ b/lib/messages.c
@@ -79,6 +79,7 @@ _notmuch_messages_create (notmuch_message_list_t *list)
 
     messages->is_of_list_type = true;
     messages->iterator = list->head;
+    messages->status = NOTMUCH_STATUS_SUCCESS;
 
     return messages;
 }
@@ -98,16 +99,30 @@ _notmuch_messages_create (notmuch_message_list_t *list)
  *	   carefully control object construction with placement new
  *	   anyway. *sigh*
  */
+notmuch_status_t
+notmuch_messages_status (notmuch_messages_t *messages)
+{
+    bool valid;
+
+    if (messages == NULL)
+	return NOTMUCH_STATUS_ITERATOR_EXHAUSTED;
+
+    if (messages->status != NOTMUCH_STATUS_SUCCESS)
+	return messages->status;
+
+    if (! messages->is_of_list_type)
+	valid = _notmuch_mset_messages_valid (messages);
+    else
+	valid = messages->iterator != NULL;
+
+    return valid ?
+	NOTMUCH_STATUS_SUCCESS : NOTMUCH_STATUS_ITERATOR_EXHAUSTED;
+}
+
 notmuch_bool_t
 notmuch_messages_valid (notmuch_messages_t *messages)
 {
-    if (messages == NULL)
-	return false;
-
-    if (! messages->is_of_list_type)
-	return _notmuch_mset_messages_valid (messages);
-
-    return (messages->iterator != NULL);
+    return notmuch_messages_status (messages) == NOTMUCH_STATUS_SUCCESS;
 }
 
 bool
@@ -142,7 +157,7 @@ notmuch_messages_move_to_next (notmuch_messages_t *messages)
 	return;
     }
 
-    if (messages->iterator == NULL)
+    if (! notmuch_messages_valid (messages))
 	return;
 
     messages->iterator = messages->iterator->next;
diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h
index 367e23e6..db49e77d 100644
--- a/lib/notmuch-private.h
+++ b/lib/notmuch-private.h
@@ -147,6 +147,8 @@ typedef enum {
     NOTMUCH_PRIVATE_STATUS_NO_MAIL_ROOT				= NOTMUCH_STATUS_NO_MAIL_ROOT,
     NOTMUCH_PRIVATE_STATUS_BAD_QUERY_SYNTAX			= NOTMUCH_STATUS_BAD_QUERY_SYNTAX,
     NOTMUCH_PRIVATE_STATUS_CLOSED_DATABASE			= NOTMUCH_STATUS_CLOSED_DATABASE,
+    NOTMUCH_PRIVATE_STATUS_ITERATOR_EXHAUSTED			= NOTMUCH_STATUS_ITERATOR_EXHAUSTED,
+    NOTMUCH_PRIVATE_STATUS_OPERATION_INVALIDATED		= NOTMUCH_STATUS_OPERATION_INVALIDATED,
 
     /* Then add our own private values. */
     NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG		= NOTMUCH_STATUS_LAST_STATUS,
@@ -508,6 +510,7 @@ struct _notmuch_messages {
     bool is_of_list_type;
     notmuch_doc_id_set_t *excluded_doc_ids;
     notmuch_message_node_t *iterator;
+    notmuch_status_t status;
 };
 
 notmuch_message_list_t *
diff --git a/lib/notmuch.h b/lib/notmuch.h
index 937fa24e..e634e41f 100644
--- a/lib/notmuch.h
+++ b/lib/notmuch.h
@@ -232,6 +232,20 @@ typedef enum {
      * Database is not fully opened, or has been closed
      */
     NOTMUCH_STATUS_CLOSED_DATABASE,
+    /**
+     * The iterator being examined has been exhausted and contains no more
+     * items.
+     */
+    NOTMUCH_STATUS_ITERATOR_EXHAUSTED,
+    /**
+     * An operation that was being performed on the database has been
+     * invalidated while in progress, and must be re-executed.
+     *
+     * This will typically happen while iterating over query results and the
+     * underlying Xapian database is modified by another process so that the
+     * currently open version cannot be read anymore.
+     */
+    NOTMUCH_STATUS_OPERATION_INVALIDATED,
     /**
      * Not an actual status value. Just a way to find out how many
      * valid status values there are.
@@ -1177,7 +1191,7 @@ notmuch_query_search_threads_st (notmuch_query_t *query, notmuch_threads_t **out
  *         return EXIT_FAILURE;
  *
  *     for (;
- *          notmuch_messages_valid (messages);
+ *          ! notmuch_messages_status (messages);
  *          notmuch_messages_move_to_next (messages))
  *     {
  *         message = notmuch_messages_get (messages);
@@ -1185,6 +1199,9 @@ notmuch_query_search_threads_st (notmuch_query_t *query, notmuch_threads_t **out
  *         notmuch_message_destroy (message);
  *     }
  *
+ *     if (notmuch_messages_status (messages) != NOTMUCH_STATUS_ITERATOR_EXHAUSTED)
+ *         return EXIT_FAILURE;
+ *
  *     notmuch_query_destroy (query);
  *
  * Note: If you are finished with a message before its containing
@@ -1516,10 +1533,35 @@ notmuch_thread_destroy (notmuch_thread_t *thread);
  *
  * See the documentation of notmuch_query_search_messages for example
  * code showing how to iterate over a notmuch_messages_t object.
+ *
+ * Note that an iterator may become invalid either due to getting exhausted or
+ * due to a runtime error. Use notmuch_messages_status to distinguish
+ * between those cases.
  */
 notmuch_bool_t
 notmuch_messages_valid (notmuch_messages_t *messages);
 
+/**
+ * Get the status of the given 'messages' iterator.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: The iterator is valid; notmuch_messages_get will
+ *     return a valid object
+ *
+ * NOTMUCH_STATUS_ITERATOR_EXHAUSTED: All items have been read
+ *
+ * NOTMUCH_STATUS_OUT_OF_MEMORY: Iteration failed to allocate memory
+ *
+ * NOTMUCH_STATUS_OPERATION_INVALIDATED: Iteration was invalidated by the
+ *     database. Re-open the database and try again.
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ */
+notmuch_status_t
+notmuch_messages_status (notmuch_messages_t *messages);
+
 /**
  * Get the current message from 'messages' as a notmuch_message_t.
  *
@@ -1540,8 +1582,8 @@ notmuch_messages_get (notmuch_messages_t *messages);
  *
  * If 'messages' is already pointing at the last message then the
  * iterator will be moved to a point just beyond that last message,
- * (where notmuch_messages_valid will return FALSE and
- * notmuch_messages_get will return NULL).
+ * (where notmuch_messages_status will return NOTMUCH_STATUS_ITERATOR_EXHAUSTED
+ * and notmuch_messages_get will return NULL).
  *
  * See the documentation of notmuch_query_search_messages for example
  * code showing how to iterate over a notmuch_messages_t object.
@@ -1627,8 +1669,9 @@ notmuch_message_get_thread_id (notmuch_message_t *message);
  * will return NULL.
  *
  * If there are no replies to 'message', this function will return
- * NULL. (Note that notmuch_messages_valid will accept that NULL
- * value as legitimate, and simply return FALSE for it.)
+ * NULL. (Note that notmuch_messages_status will accept that NULL
+ * value as legitimate, and simply return NOTMUCH_STATUS_ITERATOR_EXHAUSTED
+ * for it.)
  *
  * This function also returns NULL if it triggers a Xapian exception.
  *
diff --git a/lib/query.cc b/lib/query.cc
index 1c60c122..976fe76d 100644
--- a/lib/query.cc
+++ b/lib/query.cc
@@ -371,6 +371,7 @@ _notmuch_query_search_documents (notmuch_query_t *query,
 
 	messages->base.is_of_list_type = false;
 	messages->base.iterator = NULL;
+	messages->base.status = NOTMUCH_STATUS_SUCCESS;
 	messages->notmuch = notmuch;
 	new (&messages->iterator) Xapian::MSetIterator ();
 	new (&messages->iterator_end) Xapian::MSetIterator ();
@@ -509,9 +510,15 @@ _notmuch_mset_messages_get (notmuch_messages_t *messages)
 				       mset_messages->notmuch, doc_id,
 				       &status);
 
-    if (message == NULL &&
-	status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
-	INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
+    if (message == NULL) {
+	if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+	    INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
+	} else if (status != NOTMUCH_PRIVATE_STATUS_SUCCESS) {
+	    messages->status = COERCE_STATUS (status, "error creating a message");
+	    return NULL;
+	}
+
+	INTERNAL_ERROR ("NULL message with no error code\n");
     }
 
     if (messages->excluded_doc_ids &&
diff --git a/test/T641-database-modified-messages.sh b/test/T641-database-modified-messages.sh
index 200a950d..da0557c6 100755
--- a/test/T641-database-modified-messages.sh
+++ b/test/T641-database-modified-messages.sh
@@ -16,7 +16,6 @@ add_email_corpus
 cp -a $NOTMUCH_SRCDIR/test/corpora/lkml ${MAIL_DIR}/
 
 test_begin_subtest "catching DatabaseModifiedError in _notmuch_message_create"
-test_subtest_known_broken
 
 test_C ${MAIL_DIR} <<EOF
 #include <notmuch-test.h>
@@ -29,7 +28,9 @@ main (int argc, char **argv)
     notmuch_database_t *rw_db, *ro_db;
     notmuch_messages_t *messages_ro, *messages_rw;
     notmuch_query_t *query_ro, *query_rw;
+    notmuch_status_t status;
     char* msg = NULL;
+    unsigned try;
 
     EXPECT0 (notmuch_database_open_with_config (argv[1],
 						NOTMUCH_DATABASE_MODE_READ_ONLY,
@@ -62,13 +63,34 @@ main (int argc, char **argv)
 
     notmuch_database_close (rw_db);
 
-    for (;
-	 notmuch_messages_valid (messages_ro);
-	 notmuch_messages_move_to_next (messages_ro)) {
-	notmuch_message_t *message = notmuch_messages_get (messages_ro);
+    // try iterating over the query up to twice, we expect a Xapian
+    // DatabaseModifiedError (mapped to NOTMUCH_STATUS_OPERATION_INVALIDATED)
+    // on the first try
+    for (try = 0; try < 2; try++) {
+	for (;
+	     notmuch_messages_valid (messages_ro);
+	     notmuch_messages_move_to_next (messages_ro)) {
+	    notmuch_message_t *message = notmuch_messages_get (messages_ro);
+	}
+	status = notmuch_messages_status (messages_ro);
+	if (status != NOTMUCH_STATUS_OPERATION_INVALIDATED)
+	    break;
+
+	notmuch_query_destroy (query_ro);
+	notmuch_database_close (ro_db);
+
+	EXPECT0 (notmuch_database_open_with_config (argv[1],
+						    NOTMUCH_DATABASE_MODE_READ_ONLY,
+						    "", NULL, &ro_db, &msg));
+	query_ro = notmuch_query_create (ro_db, "");
+	assert (query_ro);
+	EXPECT0 (notmuch_query_search_messages (query_ro, &messages_ro));
     }
 
-    printf ("SUCCESS\n");
+    if (status == NOTMUCH_STATUS_ITERATOR_EXHAUSTED)
+	printf ("SUCCESS\n");
+    else
+	printf ("status: %s\n", notmuch_status_to_string (status));
     return 0;
 }
 EOF
-- 
2.39.5

_______________________________________________
notmuch mailing list -- notmuch@notmuchmail.org
To unsubscribe send an email to notmuch-leave@notmuchmail.org

Thread: