]> git.kaiwu.me - haproxy.git/commitdiff
MEDIUM: filters: Activate the instances inherited by dynamic backends
authorChristopher Faulet <cfaulet@haproxy.com>
Wed, 7 Oct 2026 15:03:34 +0000 (17:03 +0200)
committerChristopher Faulet <cfaulet@haproxy.com>
Fri, 9 Oct 2026 14:17:36 +0000 (16:17 +0200)
The filter instances of the "add backend" CLI command were copied from the
defaults section but never finalized: the parse callbacks of the inherited
instances were not called, the filter-enable, filter-disable and
filter-sequence directives were not applied, and the flat evaluation orders
were not produced. As a result the new backend had no filter analyzer
enabled and no stream filter instance was attached to the streams routed
through it ("show filter instances" already listed the instances as
"enabled, inherited").

Three helpers are added, called by the "add backend" and "del backend" CLI
commands:

- flt_start_new_proxy() finalizes the instances (parse callbacks,
  filter-enable/filter-disable, filter-sequence, flattening of the three
  evaluation orders) and applies the globally requested filter mode,
  mirroring the pre-check stage of the startup. It is called before
  proxy_finalize(), while the threads are isolated, so that the filter
  analyzers are enabled if the new proxy has filters;

- flt_init_new_proxy() initializes the filters, mirroring the post-check
  and per-thread initialization stages. The 'init' callbacks run on the
  thread performing the creation, as they run on the main thread during
  the startup. The 'init_per_thread' callbacks may initialize thread-local
  variables, so they must run on every thread: they are deferred to each
  thread with twork items, queued while the threads are still parked by
  the thread isolation, and run as their first pending work when they
  resume. The backend is not reachable by the switching rules until it is
  published with "publish backend", which happens after the items
  completed (only "force-be-switch" could select it in-between). Each item
  holds a reference on the proxy, so a concurrent "del backend" cannot
  destroy it before its per-thread callbacks ran;

- flt_deinit_new_proxy() defers the 'deinit_per_thread' callbacks to every
  thread the same way during the deletion, as they may release thread-local
  resources (e.g. references in the per-thread Lua states). The proxy is
  only destroyed once every thread ran its callbacks, when its last
  reference is released.

doc/internals/api/filters.txt
include/haproxy/filters.h
reg-tests/filters/dyn-backend.vtc [new file with mode: 0644]
reg-tests/filters/legacy-mode.vtc
src/filters.c
src/proxy.c

index 4813aa014e1b3c094c5cdaca78f1f9ab158a8879..c05c3e6009db8f2b84ad1703df8def8d385184e9 100644 (file)
@@ -887,6 +887,12 @@ HAProxy is started with more than one thread. If it is started with one thread
 (or compiled without the threads support), these callbacks will be silently
 ignored (in this case, global.nbthread will be always equal to one).
 
+For a proxy dynamically created at runtime (with the "add backend" CLI
+command), the lifecycle follows the same stages: the check and init callbacks
+run on the thread performing the creation, the per-thread callbacks are
+deferred to every thread, and the deinit callbacks run on the thread
+performing the deletion, once every thread ran its per-thread callbacks.
+
 
 3.4. HANDLING THE STREAMS ACTIVITY
 -----------------------------------
index 057b3ec04f7c135501c59b7d4298095dac60f8f8..18a66f038a8852373b4275490202fb88feb9f690 100644 (file)
@@ -153,6 +153,11 @@ int  flt_check(struct proxy *p);
 int  flt_has_explicit_config(const struct proxy *px);
 int  flt_copy_instances(struct proxy *px, const struct proxy *defpx);
 void flt_free_instances(struct proxy *px);
+
+int  flt_start_new_proxy(struct proxy *px);
+int  flt_init_new_proxy(struct proxy *px);
+void flt_deinit_new_proxy(struct proxy *px);
+
 int  flt_init_class_refs(struct proxy *px);
 int  flt_add_implicit_instance(struct proxy *px, struct filter_class *cls, const char *id,
                                 char **args, const char *file, int line);
diff --git a/reg-tests/filters/dyn-backend.vtc b/reg-tests/filters/dyn-backend.vtc
new file mode 100644 (file)
index 0000000..3ca0a5c
--- /dev/null
@@ -0,0 +1,146 @@
+varnishtest "Dynamic backends inherit and activate their filter instances"
+
+feature cmd "$HAPROXY_PROGRAM -cc 'feature(LUA) && feature(THREAD)'"
+feature ignore_unknown_macro
+
+# The instances of the defaults section are inherited by the dynamic
+# backends, finalized at creation time and initialized on every thread: the
+# per-thread Lua states of the D-scoped instances are ready when the streams
+# are attached on both threads.
+server s1 {
+    # Before the dynamic backend exists, the request falls through the
+    # use_backend rule to the default backend, which has no filter.
+    rxreq
+    expect req.http.x-probe-order == <undef>
+    txresp -body "no filter yet"
+    close
+    accept
+
+    rxreq
+    expect req.http.x-probe-order == "D.a;D.b;"
+    expect req.http.x-probe-threads == "D.a:1;D.b:1;"
+    expect req.http.x-probe-instances == "D.a:2;D.b:1;"
+    expect req.http.x-probe-errors == <undef>
+    txresp -body "dyn thread one"
+    close
+    accept
+
+    rxreq
+    expect req.http.x-probe-order == "D.a;D.b;"
+    expect req.http.x-probe-threads == "D.a:2;D.b:2;"
+    expect req.http.x-probe-instances == "D.a:2;D.b:1;"
+    expect req.http.x-probe-errors == <undef>
+    txresp -body "dyn thread two"
+    close
+    accept
+
+    rxreq
+    expect req.http.x-probe-order == "D.a;D.b;"
+    expect req.http.x-probe-threads == "D.a:1;D.b:1;"
+    expect req.http.x-probe-instances == "D.a:4;D.b:3;"
+    expect req.http.x-probe-errors == <undef>
+    txresp -body "dyn thread one again"
+} -start
+
+haproxy h1 -conf {
+    global
+        chroot /
+        nbthread 2
+        thread-groups 1
+        tune.lua.bool-sample-conversion normal
+        lua-load-per-thread "${testdir}/lua-probe.lua"
+
+    defaults plain
+        mode http
+        timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+
+    # The instances of this defaults section are inherited and activated by
+    # the dynamic backends created from it. probe-c is explicitly disabled.
+    # The global sequence changes the attachment order (b then a), the
+    # response sequence the response evaluation order, the request order
+    # keeps the declaration order.
+    defaults dynbe from plain
+        filter-config lua id probe-a enabled D a
+        filter-config lua id probe-b enabled D b
+        filter-config lua id probe-c enabled D c
+        filter-sequence global lua/probe-b:before(lua/probe-a)
+        filter-sequence response lua/probe-b:before(lua/probe-a)
+        filter-disable lua/probe-c
+
+    frontend one from plain
+        bind "fd@${one}" thread 1
+        use_backend %[req.hdr(x-be)]
+        default_backend be
+
+    frontend two from plain
+        bind "fd@${two}" thread 2
+        use_backend %[req.hdr(x-be)]
+        default_backend be
+
+    backend be from plain
+        server s1 ${s1_addr}:${s1_port}
+} -start
+
+# The dynamic backend does not exist yet, the request is served by the
+# default backend.
+client c1 -connect ${h1_one_sock} {
+    txreq -hdr "x-be: dyn"
+    rxresp
+    expect resp.status == 200
+    expect resp.body == "no filter yet"
+} -run
+
+haproxy h1 -cli {
+    send "add backend dyn from dynbe"
+    expect ~ "New backend registered."
+
+    send "add server dyn/s1 ${s1_addr}:${s1_port}"
+    expect ~ "New server registered."
+
+    send "enable server dyn/s1"
+    expect ~ ".*"
+
+    send "publish backend dyn"
+    expect ~ "Backend published."
+
+    # A second dynamic backend, kept unpublished and empty so it can be
+    # deleted: its per-thread filter contexts are deinitialized on every
+    # thread during the deletion.
+    send "add backend dyn2 from dynbe"
+    expect ~ "New backend registered."
+}
+
+# The inherited instances are active on the first thread, with the response
+# evaluation order from the inherited sequence.
+client c2 -connect ${h1_one_sock} {
+    txreq -hdr "x-be: dyn"
+    rxresp
+    expect resp.status == 200
+    expect resp.http.x-probe-order == "D.b;D.a;"
+    expect resp.body == "dyn thread one"
+} -run
+
+# ... and on the second thread.
+client c3 -connect ${h1_two_sock} {
+    txreq -hdr "x-be: dyn"
+    rxresp
+    expect resp.status == 200
+    expect resp.http.x-probe-order == "D.b;D.a;"
+    expect resp.body == "dyn thread two"
+} -run
+
+haproxy h1 -cli {
+    send "del backend dyn2"
+    expect ~ "Backend deleted."
+}
+
+# The deletion of dyn2 released the per-thread contexts of every thread
+# without altering the ones of dyn.
+client c4 -connect ${h1_one_sock} {
+    txreq -hdr "x-be: dyn"
+    rxresp
+    expect resp.status == 200
+    expect resp.body == "dyn thread one again"
+} -run
index b42c0fbb9fee8c33d72078dacbabf2cebe3434f0..c0301c5def51323c507e1ff9d904b2c872cae3a2 100644 (file)
@@ -1,30 +1,40 @@
 varnishtest "Enforce the legacy filter mode on the proxies without declaration"
 
-# Regression test for the "filter-api downgrade-to-legacy" global option:
-# the proxies without explicit filter declaration must use the legacy filter
-# mode, the ones with an explicit "filter-*" declaration must keep the new mode.
+# Regression test for the "filter-api downgrade-to-legacy" global option: the proxies
+# without explicit filter declaration must use the legacy filter mode, the
+# ones with an explicit "filter-*" declaration must keep the new mode. The
+# backends dynamically created with the "add backend" CLI command follow
+# the same rule.
 
 feature ignore_unknown_macro
 
 server s1 {
     rxreq
     txresp -hdr "Content-Type: text/plain" -bodylen 500
-} -repeat 2 -start
+} -repeat 3 -start
 
 haproxy h1 -conf {
     global
         filter-api downgrade-to-legacy
+       chroot /
 
-    defaults
+    defaults plain
         mode http
         timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
         timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
         timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
 
+    # The instances of this defaults section, and the compression options,
+    # are inherited by the backends created at runtime with "add backend".
+    # Without explicit filter declaration, they work in legacy mode.
+    defaults dynbe from plain
+        compression algo gzip
+        compression type text/plain
+
     # No filter declaration: with the legacy filter mode enforced globally,
     # the implicit declaration of the compression filter is handled in the
     # legacy mode, it must be operational.
-    listen li-implicit
+    listen li-implicit from plain
         bind "fd@${li1}"
         compression algo gzip
         compression type text/plain
@@ -34,12 +44,20 @@ haproxy h1 -conf {
     # declaration always wins over the mode enforced globally, and the
     # explicit instance replaces the implicit one. It is not enabled, so
     # the compression is not performed.
-    listen li-explicit
+    listen li-explicit from plain
         bind "fd@${li2}"
         filter-config http-comp-res
         compression algo gzip
         compression type text/plain
         server s1 ${s1_addr}:${s1_port}
+
+    frontend fe from plain
+        bind "fd@${fe}"
+        use_backend %[req.hdr(x-be)]
+        default_backend be
+
+    backend be from plain
+        server s1 ${s1_addr}:${s1_port}
 } -start
 
 client c-implicit -connect ${h1_li1_sock} {
@@ -53,3 +71,27 @@ client c-explicit -connect ${h1_li2_sock} {
     rxresp
     expect resp.http.content-encoding == <undef>
 } -run
+
+# The dynamic backend inherits the compression options of the dynbe
+# defaults section. Without explicit filter declaration, the legacy filter
+# mode is enforced, the implicit compression is handled in the legacy
+# mode.
+haproxy h1 -cli {
+    send "add backend dyn from dynbe"
+    expect ~ "New backend registered."
+
+    send "add server dyn/s1 ${s1_addr}:${s1_port}"
+    expect ~ "New server registered."
+
+    send "enable server dyn/s1"
+    expect ~ ".*"
+
+    send "publish backend dyn"
+    expect ~ "Backend published."
+}
+
+client c-dyn -connect ${h1_fe_sock} {
+    txreq -url /dyn -hdr "x-be: dyn" -hdr "Accept-Encoding: gzip"
+    rxresp
+    expect resp.http.content-encoding == gzip
+} -run
index 6523568c6e2e740e706b761b1bd2541ba811d471..59145d5565e0f1156350e02d4493caf80a5ad2f6 100644 (file)
@@ -20,6 +20,7 @@
 #include <haproxy/filters.h>
 #include <haproxy/flt_decomp.h>
 #include <haproxy/flt_http_comp.h>
+#include <haproxy/global.h>
 #include <haproxy/http_ana.h>
 #include <haproxy/http_htx.h>
 #include <haproxy/htx.h>
@@ -27,7 +28,9 @@
 #include <haproxy/namespace.h>
 #include <haproxy/proxy.h>
 #include <haproxy/stream.h>
+#include <haproxy/thread.h>
 #include <haproxy/tools.h>
+#include <haproxy/twork.h>
 #include <haproxy/trace.h>
 
 
@@ -2949,6 +2952,191 @@ static int flt_precheck_instances_all()
        return 0;
 }
 
+/**************************************************************************
+ * Filter instances of proxies created at runtime
+ *************************************************************************/
+
+/* Deferred per-thread work item for a proxy created at runtime: runs
+ * flt_init_per_thread() for the proxy <arg.ptr>, in the context of the
+ * thread the item was posted to, as the filters may initialize thread-local
+ * variables. The ERR_* flags are returned to the completion callback, but
+ * only for reporting: the creation of the proxy cannot be aborted anymore.
+ */
+static union twork_arg flt_twork_init_per_thread(union twork_arg arg)
+{
+       struct proxy *px = arg.ptr;
+       int err_code;
+
+       err_code = flt_init_per_thread(px);
+       if (err_code & (ERR_ABORT|ERR_FATAL))
+               ha_alert("Failed to initialize the per-thread filters of proxy '%s' for thread %u.\n",
+                        px->id, tid);
+       return twk_u32(err_code);
+}
+
+/* Deferred per-thread work item for a proxy created at runtime: runs
+ * flt_deinit_per_thread() for the proxy <arg.ptr>, in the context of the
+ * thread the item was posted to, as the filters may release thread-local
+ * variables.
+ */
+static union twork_arg flt_twork_deinit_per_thread(union twork_arg arg)
+{
+       flt_deinit_per_thread(arg.ptr);
+       return twk_u32(ERR_NONE);
+}
+
+/* Completion callback of the deferred per-thread work items of a proxy
+ * created at runtime, running on the thread that posted them: releases the
+ * reference the item held on the proxy, so the completion of the last item
+ * marks the completion of the per-thread callbacks of every thread.
+ */
+static void flt_twork_done(union twork_arg arg, union twork_arg ret)
+{
+       proxy_drop(arg.ptr);
+}
+
+/* Finalizes the filter instances of the proxy <px> created at runtime (for
+ * now only with the "add backend" CLI command, after the instances inherited
+ * from its defaults section were copied): the parse callback of every
+ * instance is called to produce its filter configuration, the
+ * filter-enable/filter-disable and filter-sequence directives are applied,
+ * and the three evaluation orders are flattened. This mirrors the pre-check
+ * stage of the startup, so flt_start_new_proxy() must be called before
+ * proxy_finalize(), while the threads are isolated, so that the filter
+ * analyzers are enabled if the proxy has filters.
+ * Returns a combination of ERR_* flags, ERR_NONE on success.
+ */
+int flt_start_new_proxy(struct proxy *px)
+{
+       int err_code = ERR_NONE;
+
+       /* The filter mode of a new proxy may depend on the globally requested
+        * one, apply it before finalizing the filter instances.
+        */
+       flt_apply_default_mode(px);
+
+       err_code |= flt_precheck_instances(px);
+       if (!(err_code & (ERR_ABORT|ERR_FATAL)))
+               err_code |= flt_enable_filters(px);
+       if (!(err_code & (ERR_ABORT|ERR_FATAL)))
+               err_code |= flt_apply_sequences(px);
+
+       if (err_code & (ERR_ABORT|ERR_FATAL)) {
+               ha_alert("Failed to finalize the filter instances of proxy '%s'.\n",
+                        px->id);
+               return err_code;
+       }
+
+       /* Produce the flat evaluation orders and release the configuration
+        * structures
+        */
+       flt_flatten_instances(px);
+       return ERR_NONE;
+}
+
+/* Initializes the filters of the proxy <px> created at runtime, mirroring the
+ * post-check and per-thread initialization stages of the startup:
+ *
+ * - the 'init' callbacks run on the main thread during the startup, before
+ *   the threads are started, so they run here on the thread performing the
+ *   creation, with all the other threads isolated;
+ *
+ * - the 'init_per_thread' callbacks run on every thread during the startup,
+ *   as the filters may initialize thread-local variables. The other threads
+ *   being parked by the thread isolation, each callback is deferred to its
+ *   thread with a twork item: the items are queued while the threads are
+ *   still parked and run as their first pending work when they resume. The
+ *   new proxy is not reachable by the switching rules until it is published
+ *   with "publish backend", which happens after the items completed; only
+ *   "force-be-switch" could select it in-between.
+ *
+ * Each item holds a reference on the proxy, so the proxy cannot be destroyed
+ * before its per-thread callbacks ran (e.g. by a concurrent "del backend").
+ * flt_init_new_proxy() must be called after flt_start_new_proxy() and the
+ * post-proxy-check callbacks, while the threads are isolated.
+ * Returns a combination of ERR_* flags, ERR_NONE on success.
+ */
+int flt_init_new_proxy(struct proxy *px)
+{
+       int err_code = ERR_NONE;
+       int t;
+
+       BUG_ON(!thread_isolated());
+
+       err_code |= flt_init(px);
+       if (err_code & (ERR_ABORT|ERR_FATAL)) {
+               ha_alert("Failed to initialize the filters of proxy '%s'.\n", px->id);
+               return err_code;
+       }
+
+       /* nothing to defer without filter */
+       if (LIST_ISEMPTY(&px->filter_instances) && LIST_ISEMPTY(&px->filter_configs))
+               return ERR_NONE;
+
+       for (t = 0; t < global.nbthread; t++) {
+               proxy_take(px);
+               if (twork_call_on(NULL, t, flt_twork_init_per_thread, twk_ptr(px),
+                                 flt_twork_done))
+                       continue;
+
+               /* the item could not be queued: release the reference taken
+                * for it and report the failure. The creation will be aborted,
+                * so also defer the deinitialization of the threads already
+                * notified, to balance their per-thread initialization.
+                */
+               proxy_drop(px);
+               err_code |= ERR_ALERT|ERR_FATAL;
+               ha_alert("Failed to defer the per-thread filter initialization of proxy '%s' for thread %u.\n",
+                        px->id, t);
+               while (t-- > 0) {
+                       proxy_take(px);
+                       if (!twork_call_on(NULL, t, flt_twork_deinit_per_thread,
+                                           twk_ptr(px), flt_twork_done))
+                               proxy_drop(px);
+               }
+               return err_code;
+       }
+       return ERR_NONE;
+}
+
+/* Deinitializes the per-thread filters of the proxy <px> created at runtime
+ * (for now only with the "del backend" CLI command), mirroring the
+ * per-thread deinitialization stage of the shutdown: the
+ * 'deinit_per_thread' callbacks are deferred to each thread with twork
+ * items, queued while the threads are still parked by the thread isolation
+ * of the deletion. The callbacks may release thread-local variables or
+ * per-thread resources (e.g. references in the per-thread Lua states), so
+ * they must complete before the proxy is destroyed: each item holds a
+ * reference on the proxy, and the caller only releases the creation
+ * reference, so the proxy memory is freed by the completion of the last
+ * item, once every thread ran its callbacks.
+ */
+void flt_deinit_new_proxy(struct proxy *px)
+{
+       int t;
+
+       BUG_ON(!thread_isolated());
+
+       /* nothing to defer without filter */
+       if (LIST_ISEMPTY(&px->filter_instances) && LIST_ISEMPTY(&px->filter_configs))
+               return;
+
+       for (t = 0; t < global.nbthread; t++) {
+               proxy_take(px);
+               if (twork_call_on(NULL, t, flt_twork_deinit_per_thread, twk_ptr(px),
+                                 flt_twork_done))
+                       continue;
+
+               /* the item could not be queued: release the reference taken
+                * for it. The per-thread resources of this thread, if any,
+                * leak with the proxy, which is about to be destroyed.
+                */
+               proxy_drop(px);
+               ha_alert("Failed to defer the per-thread filter deinitialization of proxy '%s' for thread %u.\n",
+                        px->id, t);
+       }
+}
+
 /* Dump classes in global registration order or in a channel's placement tree. */
 static void cli_dump_flt_classes(struct buffer *out, struct list *classes, unsigned int side, int *first)
 {
index c7a6688ad7a3ec07d6971baba4e98dd321c5aec1..9b410cd0111ca88a3c028b5d6d40c7571e8c8b98 100644 (file)
@@ -5137,6 +5137,15 @@ static int cli_parse_add_backend(char **args, char *payload, struct appctx *appc
        if (proxy_ref_defaults(px, defpx, &msg))
                goto err;
 
+       /* Finalize the filter instances inherited from <defpx>, before the
+        * proxy finalization so the filter analyzers are enabled if needed.
+        */
+       err_code |= flt_start_new_proxy(px);
+       if (err_code & (ERR_ABORT|ERR_FATAL)) {
+               memprintf(&msg, "failed to finalize the filter instances");
+               goto err;
+       }
+
        proxy_init_per_thr(px);
 
        if (proxy_finalize(px, &err_code))
@@ -5148,6 +5157,16 @@ static int cli_parse_add_backend(char **args, char *payload, struct appctx *appc
                        goto err;
        }
 
+       /* Initialize the filters on every thread, after the post-proxy-check
+        * callbacks which validate the instances, while the threads are still
+        * isolated.
+        */
+       err_code |= flt_init_new_proxy(px);
+       if (err_code & (ERR_ABORT|ERR_FATAL)) {
+               memprintf(&msg, "failed to initialize the filters");
+               goto err;
+       }
+
        px->flags |= PR_FL_BE_UNPUBLISHED;
 
        if (!stats_allocate_proxy_counters_internal(&px->extra_counters_be,
@@ -5314,6 +5333,11 @@ static int cli_parse_delete_backend(char **args, char *payload, struct appctx *a
 
        px->flags |= PR_FL_DELETED;
 
+       /* Deinitialize the per-thread filters in the context of every thread,
+        * before the proxy is destroyed, while the threads are still isolated.
+        */
+       flt_deinit_new_proxy(px);
+
        thread_release();
 
        ha_notice("Backend deleted.\n");