lib: handle DatabaseModifiedError in _notmuch_message_create master
authorAnton Khirnov <anton@khirnov.net>
Mon, 7 Jul 2025 12:27:06 +0000 (14:27 +0200)
committerDavid Bremner <david@tethera.net>
Thu, 24 Jul 2025 10:30:58 +0000 (07:30 -0300)
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.

Amended-By: David Bremner <david@tethera.net>
            [replace use of notmuch_messages_valid]

lib/database.cc
lib/message.cc
lib/messages.c
lib/notmuch-private.h
lib/notmuch.h
lib/query.cc
test/T641-database-modified-messages.sh

index 737a3f3060677b4e7fd5a55fbe5268082f0867f2..8f687eee82cc189af1b84edadaf757c41b79a6c2 100644 (file)
@@ -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";
index 46638f800997d4c5250b3592e4be8ee79abc2a21..ea815efeafceb31c10ccf2bc191c308d7a6f0b02 100644 (file)
@@ -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,
index eec0a1622a3fc4fb2961fbddaff8ed222a3c4331..619b1d2fc025439d47e0d6c8a63e655b6a5df564 100644 (file)
@@ -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_bool_t
-notmuch_messages_valid (notmuch_messages_t *messages)
+notmuch_status_t
+notmuch_messages_status (notmuch_messages_t *messages)
 {
+    bool valid;
+
     if (messages == NULL)
-       return false;
+       return NOTMUCH_STATUS_ITERATOR_EXHAUSTED;
+
+    if (messages->status != NOTMUCH_STATUS_SUCCESS)
+       return messages->status;
 
     if (! messages->is_of_list_type)
-       return _notmuch_mset_messages_valid (messages);
+       valid = _notmuch_mset_messages_valid (messages);
+    else
+       valid = messages->iterator != NULL;
+
+    return valid ?
+       NOTMUCH_STATUS_SUCCESS : NOTMUCH_STATUS_ITERATOR_EXHAUSTED;
+}
 
-    return (messages->iterator != NULL);
+notmuch_bool_t
+notmuch_messages_valid (notmuch_messages_t *messages)
+{
+    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;
index 367e23e610d0220df108eed1cc078694b10bfea9..db49e77d7a4dfa8c2d4091275d4cbdadf973930f 100644 (file)
@@ -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 *
index 937fa24edf069aa3bb2cef64ecf0c05e0851bf56..e634e41f67110302b25e547f54e3daf5f47acc51 100644 (file)
@@ -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.
  *
index 1c60c122c8e9eedd22b2e5481e8e189719551994..976fe76dd1bf8ebdfab5c4c988c037af087a77f9 100644 (file)
@@ -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 &&
index 200a950d270ea328430b2db22ddab0ef8819d9b4..d27129d41935f116f3c9c0e5ccc96fb8967b6f22 100755 (executable)
@@ -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,
@@ -54,7 +55,7 @@ main (int argc, char **argv)
     EXPECT0 (notmuch_query_search_messages (query_rw, &messages_rw));
 
     for (;
-        notmuch_messages_valid (messages_rw);
+        ! notmuch_messages_status (messages_rw);
         notmuch_messages_move_to_next (messages_rw)) {
        notmuch_message_t *message = notmuch_messages_get (messages_rw);
        EXPECT0 (notmuch_message_add_tag (message, "tag"));
@@ -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_status (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