Commit 34df1d4c22 for bind

commit 34df1d4c226c16d774a023c0f4b3c1aa4f6ff463
Author: OndÅ™ej Surý <ondrej@isc.org>
Date:   Tue Oct 13 16:31:08 2026 +0200

    Reopen the dnstap output without pausing the loop manager

    dns_dt_reopen() paused all the event loops and their work threads for
    as long as it took to stop the fstrm I/O thread (which first flushes
    every queued data frame and closes the output), roll the file, and
    start a new I/O thread.  The pause only served to keep the other loops
    from submitting data frames to the I/O thread being torn down, yet
    every "rndc dnstap -reopen", "rndc dnstap -roll" and every automatic
    roll of a "dnstap-output" file stalled the whole server meanwhile.

    Publish the I/O thread and its per-loop input queues through RCU
    instead: the senders submit inside a read-side critical section, and
    dns_dt_reopen() unpublishes the old I/O thread, waits for a grace
    period, stops it (which flushes what was still queued for it), rolls
    the file and publishes a new one.  The other loops keep serving
    throughout; the data frames they produce while the output is closed
    are dropped and counted as such in the dnstap statistics.  The input
    queues must cover every thread that may submit, which only the library
    knows, so dns_dt_create() sizes them itself now: one per loop, taken
    on first use, plus one shared by the threads running outside the
    loops.

    The automatic roll was triggered by stat()ing the output under a mutex
    on every single data frame, which also serialized the loops on that
    lock.  Count the bytes of the data frames submitted to the current I/O
    thread instead (it starts at zero with every roll) and queue the roll
    once the count passes "dnstap-output ... size".

diff --git a/bin/named/server.c b/bin/named/server.c
index fa800705b6..8cd496f559 100644
--- a/bin/named/server.c
+++ b/bin/named/server.c
@@ -3435,11 +3435,6 @@ configure_dnstap(const cfg_obj_t **maps, dns_view_t *view) {
 		}

 		fopt = fstrm_iothr_options_init();
-		/*
-		 * Both network threads and worker threads may log dnstap data.
-		 */
-		fstrm_iothr_options_set_num_input_queues(fopt,
-							 2 * named_g_cpus);
 		fstrm_iothr_options_set_queue_model(
 			fopt, FSTRM_IOTHR_QUEUE_MODEL_MPSC);

diff --git a/lib/dns/dnstap.c b/lib/dns/dnstap.c
index c05d66492f..35ff190946 100644
--- a/lib/dns/dnstap.c
+++ b/lib/dns/dnstap.c
@@ -58,14 +58,17 @@
 #include <isc/buffer.h>
 #include <isc/file.h>
 #include <isc/log.h>
+#include <isc/loop.h>
 #include <isc/mem.h>
 #include <isc/mutex.h>
 #include <isc/once.h>
 #include <isc/result.h>
 #include <isc/sockaddr.h>
 #include <isc/thread.h>
+#include <isc/tid.h>
 #include <isc/time.h>
 #include <isc/types.h>
+#include <isc/urcu.h>
 #include <isc/util.h>

 #include <dns/dnstap.h>
@@ -97,6 +100,13 @@ struct dns_dthandle {
 	isc_mem_t *mctx;
 };

+typedef struct dt__io {
+	struct fstrm_iothr *iothr;
+	struct fstrm_iothr_queue **ioqs;
+	_Atomic(off_t) approx_size;
+	struct fstrm_iothr_queue *ioqs_s[ISC_TID_MAX + 1];
+} dt__io_t;
+
 struct dns_dtenv {
 	unsigned int magic;
 	isc_refcount_t references;
@@ -104,11 +114,9 @@ struct dns_dtenv {
 	isc_mem_t *mctx;
 	isc_loop_t *loop;

-	struct fstrm_iothr *iothr;
 	struct fstrm_iothr_options *fopt;

-	isc_mutex_t reopen_lock; /* locks 'reopen_queued' */
-	bool reopen_queued;
+	atomic_bool reopen_queued;

 	isc_region_t identity;
 	isc_region_t version;
@@ -118,26 +126,61 @@ struct dns_dtenv {
 	int rolls;
 	isc_log_rollsuffix_t suffix;
 	isc_stats_t *stats;
-};

-typedef struct ioq {
-	unsigned int generation;
-	struct fstrm_iothr_queue *ioq;
-} dt__ioq_t;
+	dt__io_t *io; /* RCU-protected */
+};

 static void
 destroy(dns_dtenv_t *env);

-static thread_local dt__ioq_t dt_ioq = { 0 };
-
-static atomic_uint_fast32_t global_generation;
-
 #if DNS_DTENV_TRACE
 ISC_REFCOUNT_TRACE_IMPL(dns_dtenv, destroy);
 #else
 ISC_REFCOUNT_IMPL(dns_dtenv, destroy);
 #endif /* DNS_DTENV_TRACE */

+/*
+ * Start a new I/O thread writing to '*fwp'; on success, the I/O thread
+ * takes over the writer.  The result is not published yet.
+ */
+static dt__io_t *
+io_new(dns_dtenv_t *env, struct fstrm_iothr_options *fopt,
+       struct fstrm_writer **fwp) {
+	struct fstrm_iothr *iothr = fstrm_iothr_init(fopt, fwp);
+	if (iothr == NULL) {
+		isc_log_write(DNS_LOGCATEGORY_DNSTAP, DNS_LOGMODULE_DNSTAP,
+			      ISC_LOG_WARNING,
+			      "unable to initialize dnstap I/O thread");
+		return NULL;
+	}
+
+	dt__io_t *io = isc_mem_get(env->mctx, sizeof(*io));
+	*io = (dt__io_t){
+		.iothr = iothr,
+		.ioqs = &io->ioqs_s[1],
+	};
+
+	io->ioqs[ISC_TID_UNKNOWN] = fstrm_iothr_get_input_queue(io->iothr);
+
+	return io;
+}
+
+/*
+ * Publish 'iothr' (which may be NULL) as the I/O thread the senders
+ * submit to, together with the input queue for each thread, and return
+ * the previous I/O thread.  The caller may only stop that one after a
+ * grace period, as senders can still be using it and its queues.
+ */
+static void
+io_publish(dns_dtenv_t *env, dt__io_t *io) {
+	dt__io_t *old_io = rcu_xchg_pointer(&env->io, io);
+	if (old_io != NULL) {
+		synchronize_rcu();
+		fstrm_iothr_destroy(&old_io->iothr);
+		isc_mem_put(env->mctx, old_io, sizeof(*old_io));
+	}
+}
+
 isc_result_t
 dns_dt_create(isc_mem_t *mctx, dns_dtmode_t mode, const char *path,
 	      struct fstrm_iothr_options **foptp, isc_loop_t *loop,
@@ -154,11 +197,12 @@ dns_dt_create(isc_mem_t *mctx, dns_dtmode_t mode, const char *path,
 	REQUIRE(envp != NULL && *envp == NULL);
 	REQUIRE(foptp != NULL && *foptp != NULL);

+	/* Number of loops + ISC_TID_UNKNOWN (-1) */
+	fstrm_iothr_options_set_num_input_queues(*foptp, isc_tid_count() + 1);
+
 	isc_log_write(DNS_LOGCATEGORY_DNSTAP, DNS_LOGMODULE_DNSTAP,
 		      ISC_LOG_INFO, "opening dnstap destination '%s'", path);

-	atomic_fetch_add_release(&global_generation, 1);
-
 	env = isc_mem_get(mctx, sizeof(*env));
 	*env = (dns_dtenv_t){
 		.loop = loop,
@@ -166,7 +210,6 @@ dns_dt_create(isc_mem_t *mctx, dns_dtmode_t mode, const char *path,
 	};

 	isc_mem_attach(mctx, &env->mctx);
-	isc_mutex_init(&env->reopen_lock);
 	env->path = isc_mem_strdup(env->mctx, path);
 	isc_refcount_init(&env->references, 1);
 	isc_stats_create(env->mctx, &env->stats, dns_dnstapcounter_max);
@@ -203,11 +246,8 @@ dns_dt_create(isc_mem_t *mctx, dns_dtmode_t mode, const char *path,
 		CLEANUP(ISC_R_FAILURE);
 	}

-	env->iothr = fstrm_iothr_init(*foptp, &fw);
-	if (env->iothr == NULL) {
-		isc_log_write(DNS_LOGCATEGORY_DNSTAP, DNS_LOGMODULE_DNSTAP,
-			      ISC_LOG_WARNING,
-			      "unable to initialize dnstap I/O thread");
+	env->io = io_new(env, *foptp, &fw);
+	if (env->io == NULL) {
 		fstrm_writer_destroy(&fw);
 		CLEANUP(ISC_R_FAILURE);
 	}
@@ -234,7 +274,6 @@ cleanup:
 	}

 	if (result != ISC_R_SUCCESS) {
-		isc_mutex_destroy(&env->reopen_lock);
 		isc_mem_free(env->mctx, env->path);
 		if (env->stats != NULL) {
 			isc_stats_detach(&env->stats);
@@ -275,15 +314,14 @@ isc_result_t
 dns_dt_reopen(dns_dtenv_t *env, int roll) {
 	isc_result_t result = ISC_R_SUCCESS;
 	fstrm_res res;
-	isc_logfile_t file;
 	struct fstrm_unix_writer_options *fuwopt = NULL;
 	struct fstrm_file_options *ffwopt = NULL;
 	struct fstrm_writer_options *fwopt = NULL;
 	struct fstrm_writer *fw = NULL;
+	dt__io_t *io = NULL;

 	REQUIRE(VALID_DTENV(env));
-
-	isc_loopmgr_pause();
+	REQUIRE(env->loop == NULL || env->loop == isc_loop());

 	/*
 	 * Check that we can create a new fw object.
@@ -320,47 +358,57 @@ dns_dt_reopen(dns_dtenv_t *env, int roll) {
 		CLEANUP(ISC_R_FAILURE);
 	}

-	/*
-	 * We are committed here.
-	 */
-	isc_log_write(DNS_LOGCATEGORY_DNSTAP, DNS_LOGMODULE_DNSTAP,
-		      ISC_LOG_INFO, "%s dnstap destination '%s'",
-		      (roll < 0) ? "reopening" : "rolling", env->path);
-
-	atomic_fetch_add_release(&global_generation, 1);
-
-	if (env->iothr != NULL) {
-		fstrm_iothr_destroy(&env->iothr);
-	}
-
 	if (roll == 0) {
 		roll = env->rolls;
 	}

+	/*
+	 * Depublish the old input queues.  This waits for the senders
+	 * that may still be using the old ones to leave their
+	 * read-side critical sections before stopping the old I/O
+	 * thread, which flushes the data frames still queued for it
+	 * into the (already rolled) old output and closes it.
+	 *
+	 * This is needed to prevent the old writers and new writers
+	 * trashing the same file in case when the new and the old
+	 * file is the same.
+	 */
+	io_publish(env, NULL);
+
 	if (env->mode == dns_dtmode_file && roll != 0) {
 		/*
 		 * Create a temporary isc_logfile_t structure so we can
 		 * take advantage of the logfile rolling facility.
 		 */
 		char *filename = isc_mem_strdup(env->mctx, env->path);
-		file.name = filename;
-		file.stream = NULL;
-		file.versions = roll;
-		file.maximum_size = 0;
-		file.maximum_reached = false;
-		file.suffix = env->suffix;
+		isc_logfile_t file = {
+			.name = filename,
+			.stream = NULL,
+			.versions = roll,
+			.suffix = env->suffix,
+		};
 		result = isc_logfile_roll(&file);
 		isc_mem_free(env->mctx, filename);
 		CHECK(result);
 	}

-	env->iothr = fstrm_iothr_init(env->fopt, &fw);
-	if (env->iothr == NULL) {
-		isc_log_write(DNS_LOGCATEGORY_DNSTAP, DNS_LOGMODULE_DNSTAP,
-			      ISC_LOG_WARNING,
-			      "unable to initialize dnstap I/O thread");
+	/*
+	 * We are committed here.
+	 */
+	isc_log_write(DNS_LOGCATEGORY_DNSTAP, DNS_LOGMODULE_DNSTAP,
+		      ISC_LOG_INFO, "%s dnstap destination '%s'",
+		      (roll == ISC_LOG_ROLLNEVER) ? "reopening" : "rolling",
+		      env->path);
+
+	/*
+	 * Create the new input queue and publish the new I/O thread
+	 * and its per-thread input queues.
+	 */
+	io = io_new(env, env->fopt, &fw);
+	if (io == NULL) {
 		CLEANUP(ISC_R_FAILURE);
 	}
+	io_publish(env, io);

 cleanup:
 	if (fw != NULL) {
@@ -379,8 +427,6 @@ cleanup:
 		fstrm_writer_options_destroy(&fwopt);
 	}

-	isc_loopmgr_resume();
-
 	return result;
 }

@@ -421,35 +467,6 @@ dns_dt_setversion(dns_dtenv_t *env, const char *version) {
 	return toregion(env, &env->version, version);
 }

-static void
-set_dt_ioq(unsigned int generation, struct fstrm_iothr_queue *ioq) {
-	dt_ioq.generation = generation;
-	dt_ioq.ioq = ioq;
-}
-
-static struct fstrm_iothr_queue *
-dt_queue(dns_dtenv_t *env) {
-	REQUIRE(VALID_DTENV(env));
-
-	unsigned int generation;
-
-	if (env->iothr == NULL) {
-		return NULL;
-	}
-
-	generation = atomic_load_acquire(&global_generation);
-	if (dt_ioq.ioq != NULL && dt_ioq.generation != generation) {
-		set_dt_ioq(0, NULL);
-	}
-	if (dt_ioq.ioq == NULL) {
-		struct fstrm_iothr_queue *ioq =
-			fstrm_iothr_get_input_queue(env->iothr);
-		set_dt_ioq(generation, ioq);
-	}
-
-	return dt_ioq.ioq;
-}
-
 isc_result_t
 dns_dt_getstats(dns_dtenv_t *env, isc_stats_t **statsp) {
 	REQUIRE(VALID_DTENV(env));
@@ -468,11 +485,7 @@ destroy(dns_dtenv_t *env) {
 		      ISC_LOG_INFO, "closing dnstap");
 	env->magic = 0;

-	atomic_fetch_add(&global_generation, 1);
-
-	if (env->iothr != NULL) {
-		fstrm_iothr_destroy(&env->iothr);
-	}
+	io_publish(env, NULL);
 	if (env->fopt != NULL) {
 		fstrm_iothr_options_destroy(&env->fopt);
 	}
@@ -524,24 +537,44 @@ pack_dt(const Dnstap__Dnstap *d, void **buf, size_t *sz) {
 }

 static void
-send_dt(dns_dtenv_t *env, void *buf, size_t len) {
-	struct fstrm_iothr_queue *ioq;
-	fstrm_res res;
+check_file_size_and_maybe_reopen(dns_dtenv_t *env, off_t size);

+static void
+send_dt(dns_dtenv_t *env, void *buf, size_t len) {
 	REQUIRE(env != NULL);

 	if (buf == NULL) {
 		return;
 	}

-	ioq = dt_queue(env);
-	if (ioq == NULL) {
-		free(buf);
-		return;
+	/*
+	 * dns_dt_reopen() can replace the I/O thread at any time from the
+	 * main loop; the read-side critical section keeps the one we picked
+	 * alive until the (non-blocking) submit has returned.  While there
+	 * is none, the data frame is dropped.
+	 */
+	fstrm_res res = fstrm_res_failure;
+	off_t fstrm_size = len + sizeof(uint32_t);
+
+	rcu_read_lock();
+	dt__io_t *io = rcu_dereference(env->io);
+	if (io != NULL) {
+		isc_tid_t tid = isc_tid();
+		if (io->ioqs[tid] == NULL) {
+			/* Lazy initialization */
+			io->ioqs[tid] = fstrm_iothr_get_input_queue(io->iothr);
+		}
+		res = fstrm_iothr_submit(io->iothr, io->ioqs[tid], buf, len,
+					 fstrm_free_wrapper, NULL);
+
+		if (res == fstrm_res_success && env->max_size != 0) {
+			fstrm_size += atomic_fetch_add_acq_rel(
+				&io->approx_size, len + sizeof(uint32_t));
+			check_file_size_and_maybe_reopen(env, fstrm_size);
+		}
 	}
+	rcu_read_unlock();

-	res = fstrm_iothr_submit(env->iothr, ioq, buf, len, fstrm_free_wrapper,
-				 NULL);
 	if (res != fstrm_res_success) {
 		if (env->stats != NULL) {
 			isc_stats_increment(env->stats, dns_dnstapcounter_drop);
@@ -679,48 +712,38 @@ perform_reopen(void *arg) {
 	dns_dt_reopen(env, env->rolls);

 	/* Re-allow output file rolling. */
-	LOCK(&env->reopen_lock);
-	env->reopen_queued = false;
-	UNLOCK(&env->reopen_lock);
+	atomic_store_release(&env->reopen_queued, false);

 	dns_dtenv_detach(&env);
 }

 /*%
- * Check whether a dnstap output file roll is due and if so, initiate it (the
- * actual roll happens asynchronously).
+ * Check whether a dnstap output file roll is due and if so, initiate it
+ * (the actual roll happens asynchronously).
  */
 static void
-check_file_size_and_maybe_reopen(dns_dtenv_t *env) {
-	struct stat statbuf;
-
+check_file_size_and_maybe_reopen(dns_dtenv_t *env, off_t size) {
 	/* If a loopmgr wasn't specified, abort. */
 	if (env->loop == NULL) {
 		return;
 	}

-	/*
-	 * If an output file roll is not currently queued, check the current
-	 * size of the output file to see whether a roll is needed.  Return if
-	 * it is not.
-	 */
-	LOCK(&env->reopen_lock);
-	if (env->reopen_queued || stat(env->path, &statbuf) < 0 ||
-	    statbuf.st_size <= env->max_size)
+	if (size <= env->max_size) {
+		return;
+	}
+
+	/* Return if the reload is already queued */
+	if (!atomic_compare_exchange_strong_acq_rel(&env->reopen_queued,
+						    &(bool){ false }, true))
 	{
-		goto unlock_and_return;
+		return;
 	}

 	/*
-	 * Send an event to roll the output file, then disallow output file
-	 * rolling until the roll we queue is completed.
+	 * Asynchronously roll the output file.
 	 */
 	dns_dtenv_ref(env);
 	isc_async_run(env->loop, perform_reopen, env);
-	env->reopen_queued = true;
-
-unlock_and_return:
-	UNLOCK(&env->reopen_lock);
 }

 void
@@ -728,7 +751,8 @@ dns_dt_send(dns_view_t *view, dns_dtmsgtype_t msgtype, isc_sockaddr_t *qaddr,
 	    isc_sockaddr_t *raddr, dns_transport_type_t transport,
 	    isc_region_t *zone, isc_time_t *qtime, isc_time_t *rtime,
 	    isc_buffer_t *buf) {
-	isc_time_t now, *t;
+	isc_time_t now = isc_time_now();
+	isc_time_t *t = &now;
 	dns_dtmsg_t dm;

 	REQUIRE(DNS_VIEW_VALID(view));
@@ -743,13 +767,6 @@ dns_dt_send(dns_view_t *view, dns_dtmsgtype_t msgtype, isc_sockaddr_t *qaddr,

 	REQUIRE(VALID_DTENV(view->dtenv));

-	if (view->dtenv->max_size != 0) {
-		check_file_size_and_maybe_reopen(view->dtenv);
-	}
-
-	now = isc_time_now();
-	t = &now;
-
 	init_msg(view->dtenv, &dm, dnstap_type(msgtype));

 	/* Query/response times */
diff --git a/lib/dns/include/dns/dnstap.h b/lib/dns/include/dns/dnstap.h
index 191d964158..af69e4d9e3 100644
--- a/lib/dns/include/dns/dnstap.h
+++ b/lib/dns/include/dns/dnstap.h
@@ -150,11 +150,11 @@ dns_dt_create(isc_mem_t *mctx, dns_dtmode_t mode, const char *path,
  *	with "file:", then dnstap logs are sent to a file instead of a
  *	socket.
  *
- *\li	'*foptp' set the options for fstrm_iothr_init(). '*foptp' must have
- *	have had the number of input queues set and this should be set
- *	to the number of worker threads.  Additionally the queue model
- *	should also be set.  Other options may be set if desired.
- *	If dns_dt_create succeeds the *foptp is set to NULL.
+ *\li	'*foptp' set the options for fstrm_iothr_init().  The number of
+ *	input queues is set by dns_dt_create() itself: one per event loop
+ *	plus one shared by the threads that run outside the loops, which
+ *	calls for the MPSC queue model.  Other options may be set if
+ *	desired.  If dns_dt_create succeeds the *foptp is set to NULL.
  *
  *\li	'loop' needs to be set to the loop in which
  *	dns_dt_reopen() will be called.  This is not an optional parameter:
@@ -203,7 +203,7 @@ dns_dt_setupfile(dns_dtenv_t *env, uint64_t max_size, int rolls,
 isc_result_t
 dns_dt_reopen(dns_dtenv_t *env, int roll);
 /*%<
- * Reopens files established by dns_dt_create2().
+ * Reopens files established by dns_dt_create().
  *
  * If 'roll' is non-negative and 'env->mode' is dns_dtmode_file,
  * then the file is automatically rolled over before reopening.
@@ -211,11 +211,15 @@ dns_dt_reopen(dns_dtenv_t *env, int roll);
  * keep.  If 'roll' is negative, or if 'env->mode' is dns_dtmode_unix,
  * then the channel is simply reopened.
  *
- * Note: dns_dt_reopen() uses task-exclusive mode and must be run in the
- * context of env->reopen_task.
+ * Note: dns_dt_reopen() must be called on the loop that was given to
+ * dns_dt_create() and never concurrently with itself.  It does not stop
+ * the senders on the other loops: the data frames they produce while the
+ * output is being closed and reopened are dropped and counted as such in
+ * the dnstap statistics.
  *
  * Requires:
  *\li	'env' is a valid dnstap environment.
+ *\li	The current loop is 'env->loop' (unless that is NULL).
  */

 isc_result_t
diff --git a/tests/dns/dnstap_test.c b/tests/dns/dnstap_test.c
index ef76159681..40f87f4d4f 100644
--- a/tests/dns/dnstap_test.c
+++ b/tests/dns/dnstap_test.c
@@ -90,7 +90,6 @@ ISC_LOOP_TEST_IMPL(dns_dt_create) {

 	fopt = fstrm_iothr_options_init();
 	assert_non_null(fopt);
-	fstrm_iothr_options_set_num_input_queues(fopt, 1);

 	result = dns_dt_create(isc_g_mctx, dns_dtmode_file, TAPFILE, &fopt,
 			       NULL, &dtenv);
@@ -106,7 +105,6 @@ ISC_LOOP_TEST_IMPL(dns_dt_create) {

 	fopt = fstrm_iothr_options_init();
 	assert_non_null(fopt);
-	fstrm_iothr_options_set_num_input_queues(fopt, 1);

 	result = dns_dt_create(isc_g_mctx, dns_dtmode_unix, TAPSOCK, &fopt,
 			       NULL, &dtenv);
@@ -123,7 +121,6 @@ ISC_LOOP_TEST_IMPL(dns_dt_create) {

 	fopt = fstrm_iothr_options_init();
 	assert_non_null(fopt);
-	fstrm_iothr_options_set_num_input_queues(fopt, 1);

 	result = dns_dt_create(isc_g_mctx, 33, TAPSOCK, &fopt, NULL, &dtenv);
 	assert_int_equal(result, ISC_R_FAILURE);
@@ -169,7 +166,6 @@ ISC_LOOP_TEST_IMPL(dns_dt_send) {

 	fopt = fstrm_iothr_options_init();
 	assert_non_null(fopt);
-	fstrm_iothr_options_set_num_input_queues(fopt, 1);

 	result = dns_dt_create(isc_g_mctx, dns_dtmode_file, TAPFILE, &fopt,
 			       NULL, &dtenv);