From 0c6d8ee23fa5e3e10c743b914e8fb417e4fba8a0 Mon Sep 17 00:00:00 2001 From: Amaury Denoyelle Date: Wed, 30 Sep 2026 14:41:45 +0200 Subject: [PATCH] MEDIUM: proxy: implement purge backend 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 | 9 ++++ include/haproxy/proxy-t.h | 1 + include/haproxy/proxy.h | 12 ++++- src/cli.c | 2 +- src/proxy.c | 100 +++++++++++++++++++++++++++++++++++--- 5 files changed, 115 insertions(+), 9 deletions(-) diff --git a/doc/management.txt b/doc/management.txt index b4070ed72..607ee0270 100644 --- a/doc/management.txt +++ b/doc/management.txt @@ -2597,6 +2597,15 @@ publish 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 + 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 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 diff --git a/include/haproxy/proxy-t.h b/include/haproxy/proxy-t.h index 7b1b0148f..1063b2eec 100644 --- a/include/haproxy/proxy-t.h +++ b/include/haproxy/proxy-t.h @@ -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; diff --git a/include/haproxy/proxy.h b/include/haproxy/proxy.h index a6bf3b5c9..da09c46d3 100644 --- a/include/haproxy/proxy.h +++ b/include/haproxy/proxy.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -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) { diff --git a/src/cli.c b/src/cli.c index f61c7d506..128643b4a 100644 --- 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) { diff --git a/src/proxy.c b/src/proxy.c index 7312adb11..ad61f804b 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -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 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 can be deleted. +/* Test if the backend instance named can be deleted. If 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 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 .\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 : temporarily disable specific frontend", cli_parse_disable_frontend, NULL, NULL }, { { "enable", "frontend", NULL }, "enable frontend : re-enable specific frontend", cli_parse_enable_frontend, NULL, NULL }, { { "publish", "backend", NULL }, "publish backend : mark backend as ready for traffic", cli_parse_publish_backend, NULL, NULL }, + { { "purge", "backend", NULL }, "purge backend : remove a backend on background", cli_parse_purge_backend, NULL, NULL }, { { "set", "maxconn", "frontend", NULL }, "set maxconn frontend : change a frontend's maxconn setting", cli_parse_set_maxconn_frontend, NULL }, { { "show", "default-server", NULL }, "show default-server [] : 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 }, -- 2.47.3