]> git.kaiwu.me - haproxy.git/commitdiff
MEDIUM: proxy: implement purge backend
authorAmaury Denoyelle <adenoyelle@haproxy.com>
Wed, 30 Sep 2026 12:41:45 +0000 (14:41 +0200)
committerAmaury Denoyelle <adenoyelle@haproxy.com>
Wed, 14 Oct 2026 09:33:41 +0000 (11:33 +0200)
Add a new CLI command "purge backend", similar to "purge server". Its
objective is to remove a proxy instance at runtime, or if not possible
due to a transient error, flag the proxy for background deletion.

To implement it, a new flag PR_FL_TO_PURGE is defined. It is tested
inside proxy purge task to periodically test backend deletion via
be_check_for_deletion() until the operation can be completed.

Also, be_check_for_deletion() has been extended to add a pr_del_mode as
parameter. When using purge, backend server list can still be non empty,
at the condition that every entries in it are flagged for background
deletion as well.

A backend deletion can be cancelled by using publish/unpublish commands.
This will simply remove PR_FL_TO_PURGE and log a message to report the
cancellation.

doc/management.txt
include/haproxy/proxy-t.h
include/haproxy/proxy.h
src/cli.c
src/proxy.c

index b4070ed72111a7c82b3593b8e3989fa066b9b820..607ee027043357aae253ea46a72dad3bea421005 100644 (file)
@@ -2597,6 +2597,15 @@ publish backend <backend>
   operation of "unpublish backend" command. This command is restricted and can
   only be issued on sockets configured for levels "operator" or "admin".
 
+purge backend <backend>
+  This is an alternative command to "del backend". Its main difference is that
+  if the backend cannot be immediately removed due to remaining connections, it
+  is flagged for background deletion instead of rejecting the command. This
+  requires that all remaining servers in it are already removed or at least
+  also flagged for background deletion (see "purge all servers"). A scheduled
+  backend deletion is cancelled if publish/unpublish operations are performed
+  again on it.
+
 purge all servers <backend>
   Removes or schedules deletion of all servers in a target backend. This is
   equivalent to using "purge server" on each entry. If a server cannot be
index 7b1b0148f6337781eda809487d514dd00bcbcb84..1063b2eec63f0c24aaaadca094ea3a936877fef1 100644 (file)
@@ -259,6 +259,7 @@ enum PR_SRV_STATE_FILE {
 #define PR_FL_DELETED            0x00000100  /* Proxy has been deleted and must be manipulated with care */
 #define PR_FL_NON_PURGEABLE      0x00000200  /* Proxy referenced by config elements which prevent its runtime removal. */
 #define PR_FL_FILTER_LEGACY      0x00000400  /* An explicit legacy "filter" directive was used */
+#define PR_FL_TO_PURGE           0x00000800  /* proxy will be deleted when unused - backend only */
 
 struct stream;
 
index a6bf3b5c9d12e2fa7b01b7baf8b9e001973a0954..da09c46d3aade842dcd56fcdf830da0ffb7a4197 100644 (file)
@@ -27,6 +27,7 @@
 #include <haproxy/api.h>
 #include <haproxy/applet-t.h>
 #include <haproxy/counters.h>
+#include <haproxy/errors.h>
 #include <haproxy/freq_ctr.h>
 #include <haproxy/list.h>
 #include <haproxy/listener-t.h>
@@ -103,7 +104,8 @@ void free_server_rules(struct list *srules);
 int proxy_init_per_thr(struct proxy *px);
 int proxy_finalize(struct proxy *px, int *err_code);
 
-int be_check_for_deletion(const char *bename, struct proxy **pb, const char **pm);
+int be_check_for_deletion(const char *bename, enum pr_del_mode del_mode,
+                          struct proxy **pb, const char **pm);
 void be_unregister(struct proxy *px);
 
 /*
@@ -139,6 +141,14 @@ static inline void proxy_index_id(struct proxy *px)
        ceb32_item_insert(&used_proxy_id, conf.uuid_node, uuid, px);
 }
 
+static inline void proxy_cancel_purge(struct proxy *px)
+{
+       if (px->flags & PR_FL_TO_PURGE) {
+               ha_notice("Backend '%s' purge cancelled.\n", px->id);
+               px->flags &= ~PR_FL_TO_PURGE;
+       }
+}
+
 /* this function initializes all timeouts for proxy p */
 static inline void proxy_reset_timeouts(struct proxy *proxy)
 {
index f61c7d50672090e7f96aed410b902b2c284415f0..128643b4a044afdb0663e7b1376228dc53239793 100644 (file)
--- a/src/cli.c
+++ b/src/cli.c
@@ -2561,7 +2561,7 @@ static int cli_io_handler_wait(struct appctx *appctx)
                        ret = srv_check_for_deletion(ctx->args[0], ctx->args[1], PR_DEL_MODE_SYNC, NULL, NULL, &ctx->msg);
                }
                else {
-                       ret = be_check_for_deletion(ctx->args[0], NULL, &ctx->msg);
+                       ret = be_check_for_deletion(ctx->args[0], 0, NULL, &ctx->msg);
                }
 
                if (ret < 0) {
index 7312adb114889c880c0d7ffda433076cb99fcbce..ad61f804b88b94abef0fa5dbb50ae008f93ff834 100644 (file)
@@ -531,6 +531,7 @@ static int proxy_purge_servers(struct proxy *px, int max_srv_delete)
                srv = LIST_NEXT(&px->servers_purge, struct server *, el_alt);
                if (!(srv->flags & SRV_F_TO_PURGE)) {
                        LIST_DEL_INIT(&srv->el_alt);
+                       proxy_cancel_purge(px);
                        continue;
                }
 
@@ -558,6 +559,7 @@ struct task *proxy_process_purge(struct task *t, void *context, unsigned int sta
        struct proxy *px = context;
        /* constant for max number of server delete before yield */
        const int max_srv_delete = 50;
+       int ret = 0;
 
        /* Must not dereference <px> before entering thread isolation and
         * checking task state, as proxy_drop() performed outside isolation
@@ -580,8 +582,22 @@ struct task *proxy_process_purge(struct task *t, void *context, unsigned int sta
                }
        }
 
+       if (LIST_ISEMPTY(&px->servers_purge) && (px->flags & PR_FL_TO_PURGE)) {
+               /* TODO unnecessary proxy lookup */
+               ret = be_check_for_deletion(px->id, PR_DEL_MODE_SYNC, NULL, NULL);
+               if (ret < 0) {
+                       proxy_cancel_purge(px);
+               }
+               else if (ret > 0) {
+                       ha_notice("Backend '%s' purged.\n", px->id);
+                       be_unregister(px);
+                       proxy_drop(px);
+                       px = NULL;
+               }
+       }
+
        /* Set next task execution or cancel it if no more work to do. */
-       if (!LIST_ISEMPTY(&px->servers_purge))
+       if (px && (!LIST_ISEMPTY(&px->servers_purge) || (px->flags & PR_FL_TO_PURGE)))
                t->expire = tick_add(now_ms, MS_TO_TICKS(1000));
        else
                t->expire = TICK_ETERNITY;
@@ -5361,7 +5377,10 @@ static int cli_parse_add_backend(char **args, char *payload, struct appctx *appc
        return 1;
 }
 
-/* Test if the backend instance named <bename> can be deleted.
+/* Test if the backend instance named <bename> can be deleted. If <del_mode> is
+ * set to synchronous operation, the backend's server list must already be empty.
+ * In asynchronous deletion mode, the remaining servers must be also flagged
+ * for background deletion.
  *
  * Returns a positive integer if backend can be deleted. Else, 0 is returned if
  * backend should be deletable after some delay. A negative value indicates
@@ -5371,9 +5390,11 @@ static int cli_parse_add_backend(char **args, char *payload, struct appctx *appc
  * is found. If <pm> is not NULL, it will be used on error to point to the
  * description failure.
  */
-int be_check_for_deletion(const char *bename, struct proxy **pb, const char **pm)
+int be_check_for_deletion(const char *bename, enum pr_del_mode del_mode,
+                          struct proxy **pb, const char **pm)
 {
        struct proxy *be = NULL;
+       struct server *srv;
        const char *msg = NULL;
        int ret;
 
@@ -5411,8 +5432,18 @@ int be_check_for_deletion(const char *bename, struct proxy **pb, const char **pm
        }
 
        if (!LIST_ISEMPTY(&be->servers)) {
-               msg = "Only a backend without server can be deleted.";
-               goto out;
+               if (del_mode == PR_DEL_MODE_SYNC) {
+                       msg = "Only a backend without server can be deleted.";
+                       goto out;
+               }
+
+               /* To allow background deletion of a proxy, all its servers have to be also flagged. */
+               list_for_each_entry(srv, &be->servers, el_px) {
+                       if (!(srv->flags & SRV_F_TO_PURGE)) {
+                               msg = "At least one server not flagged for purge.";
+                               goto out;
+                       }
+               }
        }
 
        /* Second, conditions that may change over time */
@@ -5461,6 +5492,11 @@ void be_unregister(struct proxy *px)
        LIST_DELETE(&px->el);
 
        px->flags |= PR_FL_DELETED;
+       /* Mark purge as completed. Necessary when proxy is deleted via a
+        * concurrent "del backend", or if proxy remains after proxy_drop() to
+        * ensure purge task won't run again.
+        */
+       px->flags &= ~PR_FL_TO_PURGE;
 
        /* Deinitialize the per-thread filters in the context of every thread,
         * before the proxy is destroyed, while the threads are still isolated.
@@ -5491,7 +5527,7 @@ static int cli_parse_delete_backend(char **args, char *payload, struct appctx *a
        thread_isolate_full();
 
        be_name = args[2];
-       ret = be_check_for_deletion(be_name, &px, &msg);
+       ret = be_check_for_deletion(be_name, PR_DEL_MODE_SYNC, &px, &msg);
        if (ret <= 0) {
                cli_err(appctx, msg);
                goto out;
@@ -5591,6 +5627,7 @@ static int cli_parse_publish_backend(char **args, char *payload, struct appctx *
 
        thread_isolate();
        px->flags &= ~PR_FL_BE_UNPUBLISHED;
+       proxy_cancel_purge(px);
 
        /* backend publish cannot fail anymore, time to publish the PROXY_PUB event */
        _proxy_event_hdl_prepare(&e_cb_data, px);
@@ -5602,6 +5639,54 @@ static int cli_parse_publish_backend(char **args, char *payload, struct appctx *
        return cli_umsg(appctx, LOG_INFO);
 }
 
+/* Handler for "purge backend". Runs under thread isolation. Always returns 1. */
+static int cli_parse_purge_backend(char **args, char *payload, struct appctx *appctx, void *private)
+{
+       struct proxy *px;
+       const char *msg;
+       char *be_name;
+       int ret;
+
+       usermsgs_clr("CLI");
+
+       if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
+               return 1;
+
+       if (*args[3]) {
+               cli_err(appctx, "Usage: purge backend <name>.\n");
+               return 1;
+       }
+
+       thread_isolate_full();
+
+       be_name = args[2];
+       ret = be_check_for_deletion(be_name, PR_DEL_MODE_ASYNC, &px, &msg);
+       if (ret < 0) {
+               /* Unremovable backend, cancel purge. */
+               cli_err(appctx, msg);
+               goto out;
+       }
+
+       if (!ret) {
+               px->flags |= PR_FL_TO_PURGE;
+               if (!tick_isset(px->purge_task->expire))
+                       px->purge_task->expire = tick_add(now_ms, 1000);
+               task_queue(px->purge_task);
+               ha_notice("Backend '%s' flagged for deletion.\n", px->id);
+               cli_msg(appctx, LOG_INFO, "Backend scheduled for purge.\n");
+       }
+       else {
+               be_unregister(px);
+               ha_notice("Backend '%s' deleted.\n", px->id);
+               cli_msg(appctx, LOG_INFO, "Backend deleted.\n");
+               proxy_drop(px);
+       }
+
+ out:
+       thread_release();
+       return 1;
+}
+
 static int cli_parse_unpublish_backend(char **args, char *payload, struct appctx *appctx, void *private)
 {
        struct event_hdl_cb_data_proxy e_cb_data;
@@ -5621,11 +5706,11 @@ static int cli_parse_unpublish_backend(char **args, char *payload, struct appctx
 
        thread_isolate();
        px->flags |= PR_FL_BE_UNPUBLISHED;
+       proxy_cancel_purge(px);
 
        /* backend unpublish cannot fail anymore, time to publish the PROXY_UNPUB event */
        _proxy_event_hdl_prepare(&e_cb_data, px);
        _proxy_event_hdl_publish(EVENT_HDL_SUB_PROXY_UNPUB, e_cb_data, px);
-
        thread_release();
 
        ha_notice("Backend unpublished.\n");
@@ -5865,6 +5950,7 @@ static struct cli_kw_list cli_kws = {{ },{
        { { "disable", "frontend",  NULL },                 "disable frontend <frontend>             : temporarily disable specific frontend",                          cli_parse_disable_frontend, NULL, NULL },
        { { "enable", "frontend",  NULL },                  "enable frontend <frontend>              : re-enable specific frontend",                                    cli_parse_enable_frontend, NULL, NULL },
        { { "publish", "backend",  NULL },                  "publish backend <backend>               : mark backend as ready for traffic",                              cli_parse_publish_backend, NULL, NULL },
+       { { "purge", "backend",  NULL },                    "purge backend <backend>                 : remove a backend on background",                                 cli_parse_purge_backend, NULL, NULL },
        { { "set", "maxconn", "frontend",  NULL },          "set maxconn frontend <frontend> <value> : change a frontend's maxconn setting",                            cli_parse_set_maxconn_frontend, NULL },
        { { "show", "default-server", NULL },               "show default-server [<backend>]         : list default-server instances in all or a single backend",       cli_parse_show_default_server, cli_io_handler_show_default_server, cli_io_release_show_default_server, },
        { { "show", "defaults", NULL },                     "show defaults                           : list all proxies defaults sections",                             NULL, cli_io_handler_show_defaults },