process, we can loop there. A two-phase queue run will benefit,
particularly for mailinglist and smarthost cases.
+JH/02 Add transaction support for hintsdbs. The sole initial provider is
+ sqlite, and is used for the wait-trasnprot DB. Transactions imply
+ locking internal to the DB. We no longer need a separate lockfile, can
+ keep the DB handle open for extended periods, and still potentially
+ benefit from concurrency on non-conlicting record uses.
+
Exim version 4.98
-----------------
+/* Only for transaction-capable DB types. Open without locking or
+starting a transaction. "lof" and "panic" always true; read/write mode.
+*/
+
+open_db *
+dbfn_open_multi(const uschar * name, open_db * dbblock)
+{
+int rc, save_errno;
+flock_t lock_data;
+uschar dirname[PATHLEN], filename[PATHLEN];
+
+DEBUG(D_hints_lookup) acl_level++;
+
+dbblock->lockfd = -1;
+db_dir_make(TRUE);
+
+snprintf(CS dirname, sizeof(dirname), "%s/db", spool_directory);
+snprintf(CS filename, sizeof(filename), "%s/%s", dirname, name);
+
+priv_drop_temp(exim_uid, exim_gid);
+dbblock->dbptr = exim_dbopen_multi(filename, dirname, O_RDWR, EXIMDB_MODE);
+if (!dbblock->dbptr && errno == ENOENT)
+ {
+ DEBUG(D_hints_lookup)
+ debug_printf_indent("%s appears not to exist: trying to create\n", filename);
+ dbblock->dbptr = exim_dbopen_multi(filename, dirname, O_RDWR|O_CREAT, EXIMDB_MODE);
+ }
+save_errno = errno;
+priv_restore();
+
+/* If the open has failed, return NULL, leaving errno set. If lof is TRUE,
+log the event - also for debugging - but debug only if the file just doesn't
+exist. */
+
+if (!dbblock->dbptr)
+ {
+ errno = save_errno;
+ if (save_errno != ENOENT)
+ log_write(0, LOG_MAIN, "%s", string_open_failed("DB file %s",
+ filename));
+ else
+ DEBUG(D_hints_lookup)
+ debug_printf_indent("%s\n", CS string_open_failed("DB file %s",
+ filename));
+ errno = save_errno;
+ DEBUG(D_hints_lookup) acl_level--;
+ return NULL;
+ }
+
+DEBUG(D_hints_lookup) debug_printf_indent(
+ "opened hints database %s for transactions: NOLOCK flags=O_RDWR\n", filename);
+
+/* Pass back the block containing the opened database handle */
+
+return dbblock;
+}
+
+
+BOOL
+dbfn_transaction_start(open_db * dbp)
+{
+DEBUG(D_hints_lookup) debug_printf_indent("dbfn_transaction_start\n");
+return exim_dbtransaction_start(dbp->dbptr);
+}
+void
+dbfn_transaction_commit(open_db * dbp)
+{
+DEBUG(D_hints_lookup) debug_printf_indent("dbfn_transaction_commit\n");
+return exim_dbtransaction_commit(dbp->dbptr);
+}
+
+
/*************************************************
* Unlock and close a database file *
*/
void
-dbfn_close(open_db *dbblock)
+dbfn_close(open_db * dbp)
{
-int * fdp = &dbblock->lockfd;
+int * fdp = &dbp->lockfd;
-exim_dbclose(dbblock->dbptr);
+exim_dbclose(dbp->dbptr);
if (*fdp >= 0) (void)close(*fdp);
DEBUG(D_hints_lookup)
{
}
+void
+dbfn_close_multi(open_db * dbp)
+{
+exim_dbclose_multi(dbp->dbptr);
+DEBUG(D_hints_lookup)
+ {
+ debug_printf_indent("closed hints database\n");
+ acl_level--;
+ }
+}
+
+
/*************************************************
/* Functions for reading/writing exim database files */
void dbfn_close(open_db *);
+void dbfn_close_multi(open_db *);
int dbfn_delete(open_db *, const uschar *);
open_db *dbfn_open(const uschar *, int, open_db *, BOOL, BOOL);
+open_db *dbfn_open_multi(const uschar *, open_db *);
void *dbfn_read_with_length(open_db *, const uschar *, int *);
void *dbfn_read_enforce_length(open_db *, const uschar *, size_t);
uschar *dbfn_scan(open_db *, BOOL, EXIM_CURSOR **);
int dbfn_write(open_db *, const uschar *, void *, int);
+BOOL dbfn_transaction_start(open_db *);
+void dbfn_transaction_commit(open_db *);
/* Macro for the common call to read without wanting to know the length. */
search_tidyup();
+/*
+A continued-tpt will, in the tpt parent here, call par_reduce for
+the one child. But we are hoping to never do continued-transport...
+SO.... we may have called par_reduce for a single child, above when we'd
+hit the limit on child-count. Possibly multiple times with different
+transports and target hosts. Does it matter if several return a suggested
+next-id, and we lose all but the last? Hmm. Less parallel working would
+happen. Perhaps still do continued-tpt once one has been set? No, that won't
+work for all cases.
+BAH.
+Could take the initial continued-tpt hit, and then do the next-id thing?
+
+do_remote_deliveries par_reduce par_wait par_read_pipe
+*/
+
+if ( continue_transport
+ && !continue_wait_db
+ && !exim_lockfile_needed()
+ )
+ {
+ open_db * dbp = store_get(sizeof(open_db), GET_UNTAINTED);
+ continue_wait_db = dbfn_open_multi(
+ string_sprintf("wait-%.200s", continue_transport), dbp);
+ continue_next_id[0] = '\0';
+ }
+
if ((pid = exim_fork(US"transport")) == 0)
{
int fd = pfd[pipe_write];
rmt_dlv_checked_write(fd, 'I', '0', big_buffer, ptr - big_buffer);
}
+/*XXX new code*/
/* Continuation message-id */
if (*continue_next_id)
rmt_dlv_checked_write(fd, 'Z', '1', continue_next_id, MESSAGE_ID_LENGTH);
(continue_transport gets set to NULL) before we consider any other addresses
in this message. */
- if (continue_transport) par_reduce(0, fallback);
+ if (continue_transport)
+ {
+ par_reduce(0, fallback);
+ if (continue_wait_db && !continue_next_id)
+ { dbfn_close_multi(continue_wait_db); continue_wait_db = NULL; }
+ }
/* Otherwise, if we are running in the test harness, wait a bit, to let the
newly created process get going before we create another process. This should
ensure repeatability in the tests. Wait long enough for most cases to complete
the transport. */
- else testharness_pause_ms(600);
+ else
+ testharness_pause_ms(600);
continue;
uschar * filter_message, * info;
open_db dbblock, * dbm_file;
extern int acl_where;
-CONTINUED_ID:
+CONTINUED_ID:
final_yield = DELIVER_ATTEMPTED_NORMAL;
now = time(NULL);
addr_last = NULL;
uschar continue_next_id[MESSAGE_ID_LENGTH +1] = {[0]='\0'};
int continue_sequence = 1;
uschar *continue_transport = NULL;
+#ifndef COMPILE_UTILITY
+open_db *continue_wait_db = NULL;
+#endif
#ifndef DISABLE_ESMTP_LIMITS
unsigned continue_limit_mail = 0;
unsigned continue_limit_rcpt = 0;
extern uschar continue_next_id[]; /* Next message_id from hintsdb */
extern int continue_sequence; /* Sequence num for continued delivery */
extern uschar *continue_transport; /* Transport for continued delivery */
+#ifndef COMPILE_UTILITY
+extern open_db *continue_wait_db; /* Hintsdb for wait-transport */
+#endif
#ifndef DISABLE_ESMTP_LIMITS
extern unsigned continue_limit_mail; /* Peer advertised limit */
extern unsigned continue_limit_rcpt;
/* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
static inline EXIM_DB *
-exim_dbopen__(const uschar * name, const uschar * dirname, int flags,
+exim_dbopen_multi(const uschar * name, const uschar * dirname, int flags,
unsigned mode)
{
EXIM_DB * dbp;
if ((ret = sqlite3_open_v2(CCS name, &dbp, sflags, NULL)) == SQLITE_OK)
{
sqlite3_busy_timeout(dbp, 5000);
- ret = sqlite3_exec(dbp, "BEGIN TRANSACTION;", NULL, NULL, NULL);
- if (ret == SQLITE_OK && flags & O_CREAT)
+ if (flags & O_CREAT)
ret = sqlite3_exec(dbp,
"CREATE TABLE IF NOT EXISTS tbl (ky TEXT PRIMARY KEY, dat BLOB);",
NULL, NULL, NULL);
return ret == SQLITE_OK ? dbp : NULL;
}
+static inline BOOL
+exim_dbtransaction_start(EXIM_DB * dbp)
+{
+return sqlite3_exec(dbp, "BEGIN TRANSACTION;", NULL, NULL, NULL) == SQLITE_OK;
+}
+
+static inline EXIM_DB *
+exim_dbopen__(const uschar * name, const uschar * dirname, int flags,
+ unsigned mode)
+{
+EXIM_DB * dbp = exim_dbopen_multi(name, dirname, flags, mode);
+if (!dbp || exim_dbtransaction_start(dbp))
+ return dbp;
+sqlite3_close(dbp);
+return NULL;
+}
+
/* EXIM_DBGET - returns TRUE if successful, FALSE otherwise */
/* note we alloc'n'copy - the caller need not do so */
/* result has a NUL appended, but the length is as per the DB */
/* EXIM_DBCLOSE */
-static void
-exim_dbclose__(EXIM_DB * dbp)
+static inline void
+exim_dbclose_multi(EXIM_DB * dbp)
{
-(void) sqlite3_exec(dbp, "COMMIT TRANSACTION;", NULL, NULL, NULL);
sqlite3_close(dbp);
}
+static inline void
+exim_dbtransaction_commit(EXIM_DB * dbp)
+{
+(void) sqlite3_exec(dbp, "COMMIT TRANSACTION;", NULL, NULL, NULL);
+}
+static inline void
+exim_dbclose__(EXIM_DB * dbp)
+{
+exim_dbtransaction_commit(dbp);
+exim_dbclose_multi(dbp);
+}
/* Datum access */
return TRUE;
}
+static inline EXIM_DB *
+exim_dbopen_multi(const uschar * name, const uschar * dirname, int flags,
+ unsigned mode) { return NULL; }
+static inline void exim_dbclose_multi(EXIM_DB * dbp) {}
+static inline BOOL exim_dbtransaction_start(EXIM_DB * dbp) { return FALSE; }
+static inline void exim_dbtransaction_commit(EXIM_DB * dbp) {}
+
/* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
static inline EXIM_DB *
exim_dbopen__(const uschar * name, const uschar * dirname, int flags,
return TRUE;
}
+static inline EXIM_DB *
+exim_dbopen_multi(const uschar * name, const uschar * dirname, int flags,
+ unsigned mode) { return NULL; }
+static inline void exim_dbclose_multi(EXIM_DB * dbp) {}
+static inline BOOL exim_dbtransaction_start(EXIM_DB * dbp) { return FALSE; }
+static inline void exim_dbtransaction_commit(EXIM_DB * dbp) {}
+
/* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
/* The API changed for DB 4.1. - and we also starting using the "env" with a
specified working dir, to avoid the DBCONFIG file trap. */
return TRUE;
}
+static inline EXIM_DB *
+exim_dbopen_multi(const uschar * name, const uschar * dirname, int flags,
+ unsigned mode) { return NULL; }
+static inline void exim_dbclose_multi(EXIM_DB * dbp) {}
+static inline BOOL exim_dbtransaction_start(EXIM_DB * dbp) { return FALSE; }
+static inline void exim_dbtransaction_commit(EXIM_DB * dbp) {}
+
/* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
static inline EXIM_DB *
exim_dbopen__(const uschar * name, const uschar * dirname, int flags,
return TRUE;
}
+static inline EXIM_DB *
+exim_dbopen_multi(const uschar * name, const uschar * dirname, int flags,
+ unsigned mode) { return NULL; }
+static inline void exim_dbclose_multi(EXIM_DB * dbp) {}
+static inline BOOL exim_dbtransaction_start(EXIM_DB * dbp) { return FALSE; }
+static inline void exim_dbtransaction_commit(EXIM_DB * dbp) {}
+
/* EXIM_DBOPEN - return pointer to an EXIM_DB, NULL if failed */
static inline EXIM_DB *
exim_dbopen__(const uschar * name, const uschar * dirname, int flags,
return TRUE;
}
+static inline EXIM_DB *
+exim_dbopen_multi(const uschar * name, const uschar * dirname, int flags,
+ unsigned mode) { return NULL; }
+static inline void exim_dbclose_multi(EXIM_DB * dbp) {}
+static inline BOOL exim_dbtransaction_start(EXIM_DB * dbp) { return FALSE; }
+static inline void exim_dbtransaction_commit(EXIM_DB * dbp) {}
+
/* EXIM_DBOPEN - returns a EXIM_DB *, NULL if failed */
/* Check that the name given is not present. This catches
a directory name; otherwise we would create the name.pag and
refer to the sender instead of recipients.
If queue_2stage is set, the queue is scanned twice. The first time, queue_smtp
-is set so that routing is done for all messages. Thus in the second run those
-that are routed to the same host should go down the same SMTP connection.
+is set so that routing is done for all messages. A call of the transport adds
+each message_id in turn to a list for the resulting host.
+Then in the second run those that are routed to the same host should all go down
+a single SMTP connection.
Arguments:
q queue-runner descriptor
else break;
#ifdef MEASURE_TIMING
- report_time_since(×tamp_startup, US"queue_run 1st phase done");
+ report_time_since(×tamp_startup, US"queue_run phase 1 done");
#endif
q->queue_2stage = f.queue_2stage = FALSE;
DEBUG(D_queue_run) debug_printf("queue_run phase 2 start\n");
DEBUG(D_transport) debug_printf("updating wait-%s database\n", tpname);
-/* Open the database for this transport */
+/* Open the database (or transaction) for this transport */
-if (!(dbp = dbfn_open(string_sprintf("wait-%.200s", tpname),
- O_RDWR, &dbblock, TRUE, TRUE)))
+if ( continue_wait_db
+ ? !dbfn_transaction_start(dbp = continue_wait_db)
+ : !(dbp = dbfn_open(string_sprintf("wait-%.200s", tpname),
+ O_RDWR, &dbblock, TRUE, TRUE))
+ )
return;
/* Scan the list of hosts for which this message is waiting, and ensure
/* All now done */
-dbfn_close(dbp);
+if (continue_wait_db)
+ dbfn_transaction_commit(dbp);
+else
+ dbfn_close(dbp);
}
/* Open the waiting information database. */
-if (!(dbp = dbfn_open(string_sprintf("wait-%.200s", transport_name),
- O_RDWR, &dbblock, TRUE, TRUE)))
+if ( continue_wait_db
+ ? !dbfn_transaction_start(dbp = continue_wait_db)
+ : !(dbp = dbfn_open(string_sprintf("wait-%.200s", transport_name),
+ O_RDWR, &dbblock, TRUE, TRUE))
+ )
goto retfalse;
/* See if there is a record for this host; if not, there's nothing to do. */
dbfn_write(dbp, hostname, host_record, (int)sizeof(dbdata_wait) + host_length);
}
-dbfn_close(dbp);
+if (continue_wait_db)
+ dbfn_transaction_commit(dbp);
+else
+ dbfn_close(dbp);
DEBUG(D_transport)
{
return TRUE;
dbclose_false:
- dbfn_close(dbp);
+ if (continue_wait_db)
+ dbfn_transaction_commit(dbp);
+ else
+ dbfn_close(dbp);
retfalse:
DEBUG(D_transport)
DEBUG(D_transport) debug_printf("transport_pass_socket entered\n");
+/*XXX we'd prefer this never happens, by not calling here for this
+case (instead, passing back the next-id. But in case it does... */
+if (continue_wait_db)
+ { dbfn_close_multi(continue_wait_db); continue_wait_db = NULL; }
+
#ifndef DISABLE_ESMTP_LIMITS
continue_limit_mail = peer_limit_mail;
continue_limit_rcpt = peer_limit_rcpt;
/* If we are running in the test harness, wait for a bit to allow the
previous process time to finish, write the log, etc., so that the output is
always in the same order for automatic comparison. */
+ /* The double-fork goes back at least as far as 0.53 (June 1996). As of
+ 2024 I'm unclear why it is needed, especially given the following exec.
+ I suppose it means that the parent [caller of transport_pass_socket()]
+ [ that would be the "transport" proc ]
+ has no direct extant child procs, from this operation. Does it wait
+ for children? Not obviously so far, and a quick test has is working
+ apparently ok with a single fork. Further: The "transport" proc goes
+ on to only send results back up the pipe to its parent, the "delivery"
+ proc. It'd be kinda nice to swap the sequence around: send the results back,
+ omit the forking entrely, and exec the new transport. But the code
+ it all in the wrong place (the pipe-write in deliver.c and here we're
+ down in the transport). Perhaps we could pass the suggested next
+ msgid back up the pipe?
+
+ How would this interact with the existing TLS proxy process?
+ Probably the way continue_more does at present. That feature is
+ for the case where a single message has more (recip) addrs than
+ can be handled in a single call to the transport. The continue-more
+ flag is set; the transport avoids doing a continue-transport fork/exec,
+ closes TLS and passes back to the deliver proc and exits. The deliver proc
+ makes a further call to the transport. An RSET is sent on the conn;
+ and the still-open conn is left for the deliver proc to make another
+ call to the transport with it open. That only works because it was
+ originally a continued-conn, also, so the deliver proc has the conn.
+ - So if already a continued-conn, could pass back the next-message-id
+ rather than doing a further continued-conn - but we'd have to re-establish
+ TLS.
+ [ Which is a pity, and should also be worked on. ]
+ We do not need to pass the wait-tpt hintsdb handle across an exec-for-
+ continued-conn because the deliver proc can know what tpt will be used,
+ so the name is predictable and it cam open it. May as well do that for
+ any remote tpt, and skip the open/close code in the tpt. Perhaps local
+ tpts also for consistency. But... only for transaction-capable DB providers
+ (and we will be assuming they are sequential-fork-safe).
+
+ Architecture. The transport is a separate proc so that it can
+ - go badly wrong and die, being monitored from a safer parent
+ - manipulate privs, to deliver to local files. But here, we're smtp
+ and don't to that!
+ */
testharness_pause_ms(500);
transport_do_pass_socket(transport_name, hostname, hostaddress,
been used, which we do under TLSv1.3 for the gsasl SCRAM*PLUS methods.
But we were always doing it anyway. */
- tls_close(sx->cctx.tls_ctx,
- sx->send_tlsclose ? TLS_SHUTDOWN_WAIT : TLS_SHUTDOWN_WONLY);
- sx->send_tlsclose = FALSE;
- sx->cctx.tls_ctx = NULL;
- tls_out.active.sock = -1;
- smtp_peer_options = smtp_peer_options_wrap;
- sx->ok = !sx->smtps
- && smtp_write_command(sx, SCMD_FLUSH, "EHLO %s\r\n", sx->helo_data)
- >= 0
- && smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
- '2', ob->command_timeout);
+ tls_close(sx->cctx.tls_ctx,
+ sx->send_tlsclose ? TLS_SHUTDOWN_WAIT : TLS_SHUTDOWN_WONLY);
+ sx->send_tlsclose = FALSE;
+ sx->cctx.tls_ctx = NULL;
+ tls_out.active.sock = -1;
+ smtp_peer_options = smtp_peer_options_wrap;
+ sx->ok = !sx->smtps
+ && smtp_write_command(sx, SCMD_FLUSH, "EHLO %s\r\n", sx->helo_data)
+ >= 0
+ && smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
+ '2', ob->command_timeout);
if (sx->ok && f.continue_more)
goto TIDYUP; /* More addresses for another run */