Transactions in retry hintsdb
[exim.git] / src / src / retry.c
index a040c33108a572fa13091f6627f07632fc68a234..fdcb6abea8a48f813315d34b9f88e06168a4c527 100644 (file)
@@ -2,8 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
+/* Copyright (c) The Exim Maintainers 2020 - 2024 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions concerned with retrying unsuccessful deliveries. */
 
@@ -29,32 +31,31 @@ Returns:        TRUE if the ultimate timeout has been reached
 */
 
 BOOL
-retry_ultimate_address_timeout(uschar *retry_key, const uschar *domain,
+retry_ultimate_address_timeout(const uschar * retry_key, const uschar *domain,
   dbdata_retry *retry_record, time_t now)
 {
 BOOL address_timeout;
+retry_config * retry;
 
 DEBUG(D_retry)
   {
   debug_printf("retry time not reached: checking ultimate address timeout\n");
-  debug_printf("  now=%d first_failed=%d next_try=%d expired=%d\n",
-    (int)now, (int)retry_record->first_failed,
-    (int)retry_record->next_try, retry_record->expired);
+  debug_printf("  now=" TIME_T_FMT " first_failed=" TIME_T_FMT
+               " next_try=" TIME_T_FMT " expired=%c\n",
+               now, retry_record->first_failed,
+               retry_record->next_try, retry_record->expired ? 'T' : 'F');
   }
 
-retry_config *retry =
-  retry_find_config(retry_key+2, domain,
+retry = retry_find_config(retry_key+2, domain,
     retry_record->basic_errno, retry_record->more_errno);
 
-if (retry != NULL && retry->rules != NULL)
+if (retry && retry->rules)
   {
   retry_rule *last_rule;
-  for (last_rule = retry->rules;
-       last_rule->next != NULL;
-       last_rule = last_rule->next);
+  for (last_rule = retry->rules; last_rule->next; last_rule = last_rule->next) ;
   DEBUG(D_retry)
-    debug_printf("  received_time=%d diff=%d timeout=%d\n",
-      (int)received_time.tv_sec, (int)(now - received_time.tv_sec), last_rule->timeout);
+    debug_printf("  received_time=" TIME_T_FMT " diff=%d timeout=%d\n",
+      received_time.tv_sec, (int)(now - received_time.tv_sec), last_rule->timeout);
   address_timeout = (now - received_time.tv_sec > last_rule->timeout);
   }
 else
@@ -74,6 +75,29 @@ return address_timeout;
 
 
 
+const uschar *
+retry_host_key_build(const host_item * host, BOOL incl_ip,
+  const uschar * portstring)
+{
+const uschar * s = host->name;
+gstring * g = string_is_ip_address(s, NULL)
+  ? string_fmt_append(NULL, "T:[%s]", s)    /* wrap a name which is a bare ip */
+  : string_fmt_append(NULL, "T:%s",   s);
+
+s = host->address;
+if (incl_ip)
+  g = Ustrchr(s, ':')
+    ? string_fmt_append(g, ":[%s]", s)     /* wrap an ipv6  */
+    : string_fmt_append(g, ":%s",   s);
+
+if (portstring)
+  g = string_cat(g, portstring);
+
+gstring_release_unused(g);
+return string_from_gstring(g);
+}
+
+
 /*************************************************
 *     Set status of a host+address item          *
 *************************************************/
@@ -123,19 +147,19 @@ Returns:    TRUE if the host has expired but is usable because
 
 BOOL
 retry_check_address(const uschar *domain, host_item *host, uschar *portstring,
-  BOOL include_ip_address, uschar **retry_host_key, uschar **retry_message_key)
+  BOOL include_ip_address,
+  const uschar **retry_host_key, const uschar **retry_message_key)
 {
 BOOL yield = FALSE;
 time_t now = time(NULL);
-uschar *host_key, *message_key;
-open_db dbblock;
-open_db *dbm_file;
-tree_node *node;
-dbdata_retry *host_retry_record, *message_retry_record;
+const uschar * host_key, * message_key;
+open_db dbblock, * dbm_file;
+tree_node * node;
+dbdata_retry * host_retry_record, * message_retry_record;
 
 *retry_host_key = *retry_message_key = NULL;
 
-DEBUG(D_transport|D_retry) debug_printf("checking status of %s\n", host->name);
+DEBUG(D_transport|D_retry) debug_printf("checking retry status of %s\n", host->name);
 
 /* Do nothing if status already set; otherwise initialize status as usable. */
 
@@ -143,14 +167,11 @@ if (host->status != hstatus_unknown) return FALSE;
 host->status = hstatus_usable;
 
 /* Generate the host key for the unusable tree and the retry database. Ensure
-host names are lower cased (that's what %S does). */
-
-host_key = include_ip_address?
-  string_sprintf("T:%S:%s%s", host->name, host->address, portstring) :
-  string_sprintf("T:%S%s", host->name, portstring);
-
-/* Generate the message-specific key */
+host names are lower cased (that's what %S does).
+Generate the message-specific key too.
+Be sure to maintain lack-of-spaces in retry keys; exinext depends on it. */
 
+host_key = retry_host_key_build(host, include_ip_address, portstring);
 message_key = string_sprintf("%s:%s", host_key, message_id);
 
 /* Search the tree of unusable IP addresses. This is filled in when deliveries
@@ -171,7 +192,12 @@ if ((node = tree_search(tree_unusable, host_key)))
 /* Open the retry database, giving up if there isn't one. Otherwise, search for
 the retry records, and then close the database again. */
 
-if (!(dbm_file = dbfn_open(US"retry", O_RDONLY, &dbblock, FALSE)))
+if (!continue_retry_db)
+  dbm_file = dbfn_open(US"retry", O_RDONLY, &dbblock, FALSE, TRUE);
+else if ((dbm_file = continue_retry_db) == (open_db *)-1)
+  dbm_file = NULL;
+
+if (!dbm_file)
   {
   DEBUG(D_deliver|D_retry|D_hints_lookup)
     debug_printf("no retry data available\n");
@@ -179,7 +205,8 @@ if (!(dbm_file = dbfn_open(US"retry", O_RDONLY, &dbblock, FALSE)))
   }
 host_retry_record = dbfn_read(dbm_file, host_key);
 message_retry_record = dbfn_read(dbm_file, message_key);
-dbfn_close(dbm_file);
+if (!continue_retry_db)
+  dbfn_close(dbm_file);
 
 /* Ignore the data if it is too old - too long since it was written */
 
@@ -290,9 +317,9 @@ Returns:  nothing
 */
 
 void
-retry_add_item(address_item *addr, uschar *key, int flags)
+retry_add_item(address_item * addr, const uschar * key, int flags)
 {
-retry_item *rti = store_get(sizeof(retry_item));
+retry_item * rti = store_get(sizeof(retry_item), GET_UNTAINTED);
 host_item * host = addr->host_used;
 
 rti->next = addr->retries;
@@ -343,11 +370,11 @@ Returns:       pointer to retry rule, or NULL
 */
 
 retry_config *
-retry_find_config(const uschar *key, const uschar *alternate, int basic_errno,
+retry_find_config(const uschar * key, const uschar * alternate, int basic_errno,
   int more_errno)
 {
-const uschar *colon = Ustrchr(key, ':');
-retry_config *yield;
+const uschar * colon = Ustrchr(key, ':');
+retry_config * yield;
 
 /* If there's a colon in the key, there are two possibilities:
 
@@ -355,7 +382,8 @@ retry_config *yield;
 
       hostname:ip+port
 
-    In this case, we copy the host name.
+    In this case, we copy the host name (which could be an [ip], including
+    being an [ipv6], and we drop the []).
 
 (2) This is a key for a pipe, file, or autoreply delivery, in the format
 
@@ -369,6 +397,8 @@ retry_config *yield;
 if (colon)
   key = isalnum(*key)
     ? string_copyn(key, colon-key)     /* the hostname */
+    : *key == '['
+    ? string_copyn(key+1, Ustrchr(key, ']')-1-key)     /* the ip */
     : Ustrrchr(key, ':') + 1;          /* Take from the last colon */
 
 /* Sort out the keys */
@@ -518,29 +548,26 @@ Returns:        nothing
 */
 
 void
-retry_update(address_item **addr_defer, address_item **addr_failed,
-  address_item **addr_succeed)
+retry_update(address_item ** addr_defer, address_item ** addr_failed,
+  address_item ** addr_succeed)
 {
-open_db dbblock;
-open_db *dbm_file = NULL;
+open_db dbblock, * dbm_file = NULL;
 time_t now = time(NULL);
-int i;
 
-DEBUG(D_retry) debug_printf("Processing retry items\n");
+DEBUG(D_retry) { debug_printf("Processing retry items\n"); acl_level++; }
 
 /* Three-times loop to handle succeeded, failed, and deferred addresses.
 Deferred addresses must be handled after failed ones, because some may be moved
 to the failed chain if they have timed out. */
 
-for (i = 0; i < 3; i++)
+for (int i = 0; i < 3; i++)
   {
-  address_item *endaddr, *addr;
-  address_item *last_first = NULL;
-  address_item **paddr = i==0 ? addr_succeed :
-    i==1 ? addr_failed : addr_defer;
-  address_item **saved_paddr = NULL;
+  address_item * endaddr, *addr;
+  address_item * last_first = NULL;
+  address_item ** paddr = i==0 ? addr_succeed : i==1 ? addr_failed : addr_defer;
+  address_item ** saved_paddr = NULL;
 
-  DEBUG(D_retry) debug_printf("%s addresses:\n",
+  DEBUG(D_retry) debug_printf_indent("%s addresses:\n",
     i == 0 ? "Succeeded" : i == 1 ? "Failed" : "Deferred");
 
   /* Loop for each address on the chain. For deferred addresses, the whole
@@ -556,19 +583,18 @@ for (i = 0; i < 3; i++)
   while ((endaddr = *paddr))
     {
     BOOL timed_out = FALSE;
-    retry_item *rti;
 
     for (addr = endaddr; addr; addr = addr->parent)
       {
       int update_count = 0;
       int timedout_count = 0;
 
-      DEBUG(D_retry) debug_printf(" %s%s\n", addr->address,
+      DEBUG(D_retry) debug_printf_indent(" %s%s\n", addr->address,
                addr->retries ? "" : ": no retry items");
 
       /* Loop for each retry item. */
 
-      for (rti = addr->retries; rti; rti = rti->next)
+      for (retry_item * rti = addr->retries; rti; rti = rti->next)
         {
         uschar *message;
         int message_length, message_space, failing_interval, next_try;
@@ -583,12 +609,12 @@ for (i = 0; i < 3; i++)
         reached their retry next try time. */
 
         if (!dbm_file)
-          dbm_file = dbfn_open(US"retry", O_RDWR, &dbblock, TRUE);
+          dbm_file = dbfn_open(US"retry", O_RDWR, &dbblock, TRUE, TRUE);
 
         if (!dbm_file)
           {
           DEBUG(D_deliver|D_retry|D_hints_lookup)
-            debug_printf("retry database not available for updating\n");
+            debug_printf_indent("retry database not available for updating\n");
           return;
           }
 
@@ -610,7 +636,7 @@ for (i = 0; i < 3; i++)
           {
           (void)dbfn_delete(dbm_file, rti->key);
           DEBUG(D_retry)
-            debug_printf("deleted retry information for %s\n", rti->key);
+            debug_printf_indent("deleted retry information for %s\n", rti->key);
           continue;
           }
 
@@ -630,7 +656,7 @@ for (i = 0; i < 3; i++)
              rti->flags & rf_host ? addr->domain : NULL,
              rti->basic_errno, rti->more_errno)))
           {
-          DEBUG(D_retry) debug_printf("No configured retry item for %s%s%s\n",
+          DEBUG(D_retry) debug_printf_indent("No configured retry item for %s%s%s\n",
             rti->key,
             rti->flags & rf_host ? US" or " : US"",
             rti->flags & rf_host ? addr->domain : US"");
@@ -640,11 +666,11 @@ for (i = 0; i < 3; i++)
 
         DEBUG(D_retry)
           if (rti->flags & rf_host)
-            debug_printf("retry for %s (%s) = %s %d %d\n", rti->key,
+            debug_printf_indent("retry for %s (%s) = %s %d %d\n", rti->key,
               addr->domain, retry->pattern, retry->basic_errno,
               retry->more_errno);
           else
-            debug_printf("retry for %s = %s %d %d\n", rti->key, retry->pattern,
+            debug_printf_indent("retry for %s = %s %d %d\n", rti->key, retry->pattern,
               retry->basic_errno, retry->more_errno);
 
         /* Set up the message for the database retry record. Because DBM
@@ -657,19 +683,27 @@ for (i = 0; i < 3; i++)
          ? US string_printing(rti->message)
          : US"unknown error";
         message_length = Ustrlen(message);
-        if (message_length > 150) message_length = 150;
+        if (message_length > EXIM_DB_RLIMIT)
+         {
+         DEBUG(D_retry)
+           debug_printf_indent("truncating message from %u to %u bytes\n",
+                               message_length, EXIM_DB_RLIMIT);
+         message_length = EXIM_DB_RLIMIT;
+         }
 
         /* Read a retry record from the database or construct a new one.
         Ignore an old one if it is too old since it was last updated. */
 
-        retry_record = dbfn_read(dbm_file, rti->key);
+        retry_record = dbfn_read_with_length(dbm_file, rti->key,
+                                             &message_space);
         if (  retry_record
           && now - retry_record->time_stamp > retry_data_expire)
           retry_record = NULL;
 
         if (!retry_record)
           {
-          retry_record = store_get(sizeof(dbdata_retry) + message_length);
+          retry_record = store_get(sizeof(dbdata_retry) + message_length,
+                                  message);
           message_space = message_length;
           retry_record->first_failed = now;
           retry_record->last_try = now;
@@ -677,12 +711,12 @@ for (i = 0; i < 3; i++)
           retry_record->expired = FALSE;
           retry_record->text[0] = 0;      /* just in case */
           }
-        else message_space = Ustrlen(retry_record->text);
+       else message_space -= sizeof(dbdata_retry);
 
         /* Compute how long this destination has been failing */
 
         failing_interval = now - retry_record->first_failed;
-        DEBUG(D_retry) debug_printf("failing_interval=%d message_age=%d\n",
+        DEBUG(D_retry) debug_printf_indent("failing_interval=%d message_age=%d\n",
           failing_interval, message_age);
 
         /* For a non-host error, if the message has been on the queue longer
@@ -764,7 +798,7 @@ for (i = 0; i < 3; i++)
            ;
           if (now - received_time.tv_sec > last_rule->timeout)
             {
-            DEBUG(D_retry) debug_printf("on queue longer than maximum retry\n");
+            DEBUG(D_retry) debug_printf_indent("on queue longer than maximum retry\n");
             timedout_count++;
             rule = NULL;
             }
@@ -808,15 +842,17 @@ for (i = 0; i < 3; i++)
         if (next_try - now > retry_interval_max)
           next_try = now + retry_interval_max;
 
-        /* If the new message length is greater than the previous one, we
-        have to copy the record first. */
+        /* If the new message length is greater than the previous one, we have
+       to copy the record first.  If we're using an old one, the read used
+       tainted memory so we're ok to write into it. */
 
-        if (message_length > message_space)
-          {
-          dbdata_retry *newr = store_get(sizeof(dbdata_retry) + message_length);
-          memcpy(newr, retry_record, sizeof(dbdata_retry));
-          retry_record = newr;
-          }
+       if (message_length > message_space)
+         {
+         dbdata_retry * newr =
+           store_get(sizeof(dbdata_retry) + message_length, message);
+         memcpy(newr, retry_record, sizeof(dbdata_retry));
+         retry_record = newr;
+         }
 
         /* Set up the retry record; message_length may be less than the string
         length for very long error strings. */
@@ -826,16 +862,16 @@ for (i = 0; i < 3; i++)
         retry_record->basic_errno = rti->basic_errno;
         retry_record->more_errno = rti->more_errno;
         Ustrncpy(retry_record->text, message, message_length);
-        retry_record->text[message_length] = 0;
+        retry_record->text[message_length] = 0;        /* nul-term string in db */
 
         DEBUG(D_retry)
           {
           int letter = retry_record->more_errno & 255;
-          debug_printf("Writing retry data for %s\n", rti->key);
-          debug_printf("  first failed=%d last try=%d next try=%d expired=%d\n",
+          debug_printf_indent("Writing retry data for %s\n", rti->key);
+          debug_printf_indent("  first failed=%d last try=%d next try=%d expired=%d\n",
             (int)retry_record->first_failed, (int)retry_record->last_try,
             (int)retry_record->next_try, retry_record->expired);
-          debug_printf("  errno=%d more_errno=", retry_record->basic_errno);
+          debug_printf_indent("  errno=%d more_errno=", retry_record->basic_errno);
           if (letter == 'A' || letter == 'M')
             debug_printf("%d,%c", (retry_record->more_errno >> 8) & 255,
               letter);
@@ -855,12 +891,12 @@ for (i = 0; i < 3; i++)
       if (update_count > 0 && update_count == timedout_count)
         if (!testflag(endaddr, af_retry_skipped))
           {
-          DEBUG(D_retry) debug_printf("timed out: all retries expired\n");
+          DEBUG(D_retry) debug_printf_indent("timed out: all retries expired\n");
           timed_out = TRUE;
           }
         else
           DEBUG(D_retry)
-            debug_printf("timed out but some hosts were skipped\n");
+            debug_printf_indent("timed out but some hosts were skipped\n");
       }     /* Loop for an address and its parents */
 
     /* If this is a deferred address, and retry processing was requested by
@@ -888,16 +924,17 @@ for (i = 0; i < 3; i++)
         for (;; addr = addr->next)
           {
           setflag(addr, af_retry_timedout);
-          addr->message = (addr->message == NULL)? US"retry timeout exceeded" :
-            string_sprintf("%s: retry timeout exceeded", addr->message);
-          addr->user_message = (addr->user_message == NULL)?
-            US"retry timeout exceeded" :
-            string_sprintf("%s: retry timeout exceeded", addr->user_message);
+          addr->message = addr->message
+            ? string_sprintf("%s: retry timeout exceeded", addr->message)
+           : US"retry timeout exceeded";
+          addr->user_message = addr->user_message
+           ? string_sprintf("%s: retry timeout exceeded", addr->user_message)
+           : US"retry timeout exceeded";
           log_write(0, LOG_MAIN, "** %s%s%s%s: retry timeout exceeded",
             addr->address,
-           (addr->parent == NULL)? US"" : US" <",
-           (addr->parent == NULL)? US"" : addr->parent->address,
-           (addr->parent == NULL)? US"" : US">");
+            addr->parent ? US" <" : US"",
+            addr->parent ? addr->parent->address : US"",
+            addr->parent ? US">" : US"");
 
           if (addr == endaddr) break;
           }
@@ -926,7 +963,9 @@ for (i = 0; i < 3; i++)
 
 if (dbm_file) dbfn_close(dbm_file);
 
-DEBUG(D_retry) debug_printf("end of retry processing\n");
+DEBUG(D_retry) { acl_level--; debug_printf("end of retry processing\n"); }
 }
 
 /* End of retry.c */
+/* vi: aw ai sw=2
+*/