From c23586aee58fa3c2235651df2054079b7c54be9d Mon Sep 17 00:00:00 2001
From: Abhijeet Rastogi <abhijeet.1989@gmail.com>
Date: Mon, 8 Apr 2024 15:30:44 -0700
Subject: [PATCH] MINOR: cli: add option to modify close-spread-time

close-spread-time is only set during config parse stage, and then there
is no API available today to modify it.
If there is a requirement to speed-up the soft-stop for HAproxy, it is
not possible to dynamically lower this value before initiating
soft-stop. This CLI feature now provides ability to modify the
close-spread-time value dynamically, after HAProxy process has already
started.

A new `notice` message is also added to showcase the current value of
close-spread-time when it is non-zero.

New reg-tests dir `cli` is created as there is currently no appropriate
director where this test falls in.
---
 reg-tests/cli/cli_set_close_spread_time.vtc |  55 ++++++++++
 src/cli.c                                   | 112 ++++++++++++++++----
 src/proxy.c                                 |   2 +
 3 files changed, 146 insertions(+), 23 deletions(-)
 create mode 100644 reg-tests/cli/cli_set_close_spread_time.vtc

diff --git a/reg-tests/cli/cli_set_close_spread_time.vtc b/reg-tests/cli/cli_set_close_spread_time.vtc
new file mode 100644
index 000000000..d8c407371
--- /dev/null
+++ b/reg-tests/cli/cli_set_close_spread_time.vtc
@@ -0,0 +1,55 @@
+varnishtest "Set close-spread-time via CLI"
+
+feature ignore_unknown_macro
+
+# for "set close-spread-time <time>"
+# for "get close-spread-time"
+#REGTEST_TYPE=devel
+
+# Do nothing. Is there only to create s1_* macros
+server s1 {
+} -start
+
+haproxy h1 -conf {
+    global
+        close-spread-time 10s
+
+    defaults
+        mode http
+        timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+
+    frontend myfrontend
+        bind "fd@${my_fe}"
+        default_backend test
+
+    backend test
+        server www1 ${s1_addr}:${s1_port}
+} -start
+
+haproxy h1 -cli {
+    # Starts with 10s
+    send "get global close-spread-time"
+    expect ~ "close-spread-time=10000ms"
+
+    send "set global close-spread-time 1s"
+    expect ~ ""
+    send "get global close-spread-time"
+    expect ~ "close-spread-time=1000ms"
+
+    # Disabling close-spread-time is possible
+    send "set global close-spread-time 0"
+    expect ~ ""
+    send "get global close-spread-time"
+    expect ~ "close-spread-time=0"
+
+    # Negative value is error
+    send "set global close-spread-time -1"
+    expect ~ "Invalid"
+    send "set global close-spread-time 25d"
+    expect ~ "Timer overflow"
+    send "get global close-spread-time"
+    expect ~ "close-spread-time=0"
+} -wait
+
diff --git a/src/cli.c b/src/cli.c
index 02cb06843..c03020ecc 100644
--- a/src/cli.c
+++ b/src/cli.c
@@ -1716,6 +1716,70 @@ static int cli_parse_show_fd(char **args, char *payload, struct appctx *appctx,
 	return 0;
 }
 
+/* parse a "get close_spread_time" CLI request. It always returns 1. */
+static int cli_parse_get_close_spread_time(char **args, char *payload, struct appctx *appctx, void *private)
+{
+	char *output = NULL;
+
+	if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
+		return 1;
+
+	memprintf(&output, "close-spread-time=%dms\n", global.close_spread_time);
+
+	cli_dynmsg(appctx, LOG_INFO, output);
+
+	return 1;
+}
+
+
+/* parse a "set close_spread_time" CLI request. It always returns 1. */
+static int cli_parse_set_close_spread_time(char **args, char *payload, struct appctx *appctx, void *private)
+{
+	const char *res;
+	unsigned int time;
+
+	if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
+		return 1;
+
+	if (!*args[3])
+		return cli_err(appctx, "Expects an integer value or infinite.\n");
+
+	if (strcmp(args[3], "infinite") == 0) {
+		thread_isolate();
+		global.tune.options |= GTUNE_DISABLE_ACTIVE_CLOSE;
+		global.close_spread_time = TICK_ETERNITY;
+		thread_release();
+
+		return 1;
+	} else if (strcmp(args[3], "0") == 0) {
+		thread_isolate();
+		global.tune.options &= ~GTUNE_DISABLE_ACTIVE_CLOSE;
+		global.close_spread_time = 0;
+		thread_release();
+
+		return 1;
+	}
+
+	res = parse_time_err(args[3], &time, TIME_UNIT_MS);
+	if (res == PARSE_TIME_OVER) {
+		return cli_err(appctx, "Timer overflow, maximum possible value 2147483647");
+	}
+	else if (res == PARSE_TIME_UNDER) {
+		return cli_err(appctx, "Timer underflow, min is too small");
+	}
+	else if (res) {
+		return cli_err(appctx, "Invalid close-spread-time value.\n");
+	}
+
+	thread_isolate();
+	global.tune.options &= ~GTUNE_DISABLE_ACTIVE_CLOSE;
+	global.close_spread_time = time;
+	thread_release();
+
+	return 1;
+}
+
+
 /* parse a "set timeout" CLI request. It always returns 1. */
 static int cli_parse_set_timeout(char **args, char *payload, struct appctx *appctx, void *private)
 {
@@ -3566,29 +3630,31 @@ static struct applet mcli_applet = {
 
 /* register cli keywords */
 static struct cli_kw_list cli_kws = {{ },{
-	{ { "help", NULL },                      NULL,                                                                                                cli_parse_simple, NULL, NULL, NULL, ACCESS_MASTER },
-	{ { "prompt", NULL },                    NULL,                                                                                                cli_parse_simple, NULL, NULL, NULL, ACCESS_MASTER },
-	{ { "quit", NULL },                      NULL,                                                                                                cli_parse_simple, NULL, NULL, NULL, ACCESS_MASTER },
-	{ { "_getsocks", NULL },                 NULL,                                                                                                _getsocks, NULL },
-	{ { "expert-mode", NULL },               NULL,                                                                                                cli_parse_expert_experimental_mode, NULL, NULL, NULL, ACCESS_MASTER }, // not listed
-	{ { "experimental-mode", NULL },         NULL,                                                                                                cli_parse_expert_experimental_mode, NULL, NULL, NULL, ACCESS_MASTER }, // not listed
-	{ { "mcli-debug-mode", NULL },         NULL,                                                                                                  cli_parse_expert_experimental_mode, NULL, NULL, NULL, ACCESS_MASTER_ONLY }, // not listed
-	{ { "set", "anon", "on" },               "set anon on [value]                     : activate the anonymized mode",                            cli_parse_set_anon, NULL, NULL },
-	{ { "set", "anon", "off" },              "set anon off                            : deactivate the anonymized mode",                          cli_parse_set_anon, NULL, NULL },
-	{ { "set", "anon", "global-key", NULL }, "set anon global-key <value>             : change the global anonymizing key",                       cli_parse_set_global_key, NULL, NULL },
-	{ { "set", "maxconn", "global",  NULL }, "set maxconn global <value>              : change the per-process maxconn setting",                  cli_parse_set_maxconn_global, NULL },
-	{ { "set", "rate-limit", NULL },         "set rate-limit <setting> <value>        : change a rate limiting value",                            cli_parse_set_ratelimit, NULL },
-	{ { "set", "severity-output",  NULL },   "set severity-output [none|number|string]: set presence of severity level in feedback information",  cli_parse_set_severity_output, NULL, NULL },
-	{ { "set", "timeout",  NULL },           "set timeout [cli] <delay>               : change a timeout setting",                                cli_parse_set_timeout, NULL, NULL },
-	{ { "show", "anon", NULL },              "show anon                               : display the current state of anonymized mode",            cli_parse_show_anon, NULL },
-	{ { "show", "env",  NULL },              "show env [var]                          : dump environment variables known to the process",         cli_parse_show_env, cli_io_handler_show_env, NULL },
-	{ { "show", "cli", "sockets",  NULL },   "show cli sockets                        : dump list of cli sockets",                                cli_parse_default, cli_io_handler_show_cli_sock, NULL, NULL, ACCESS_MASTER },
-	{ { "show", "cli", "level", NULL },      "show cli level                          : display the level of the current CLI session",            cli_parse_show_lvl, NULL, NULL, NULL, ACCESS_MASTER},
-	{ { "show", "fd", NULL },                "show fd [-!plcfbsd]* [num]              : dump list of file descriptors in use or a specific one",  cli_parse_show_fd, cli_io_handler_show_fd, NULL },
-	{ { "show", "version", NULL },           "show version                            : show version of the current process",                     cli_parse_show_version, NULL, NULL, NULL, ACCESS_MASTER },
-	{ { "operator", NULL },                  "operator                                : lower the level of the current CLI session to operator",  cli_parse_set_lvl, NULL, NULL, NULL, ACCESS_MASTER},
-	{ { "user", NULL },                      "user                                    : lower the level of the current CLI session to user",      cli_parse_set_lvl, NULL, NULL, NULL, ACCESS_MASTER},
-	{ { "wait", NULL },                      "wait {-h|<delay_ms>} cond [args...]     : wait the specified delay or condition (-h to see list)",  cli_parse_wait, cli_io_handler_wait, cli_release_wait, NULL },
+	{ { "help", NULL },                                NULL,                                                                                                cli_parse_simple, NULL, NULL, NULL, ACCESS_MASTER },
+	{ { "prompt", NULL },                              NULL,                                                                                                cli_parse_simple, NULL, NULL, NULL, ACCESS_MASTER },
+	{ { "quit", NULL },                                NULL,                                                                                                cli_parse_simple, NULL, NULL, NULL, ACCESS_MASTER },
+	{ { "_getsocks", NULL },                           NULL,                                                                                                _getsocks, NULL },
+	{ { "expert-mode", NULL },                         NULL,                                                                                                cli_parse_expert_experimental_mode, NULL, NULL, NULL, ACCESS_MASTER }, // not listed
+	{ { "experimental-mode", NULL },                   NULL,                                                                                                cli_parse_expert_experimental_mode, NULL, NULL, NULL, ACCESS_MASTER }, // not listed
+	{ { "mcli-debug-mode", NULL },                     NULL,                                                                                                cli_parse_expert_experimental_mode, NULL, NULL, NULL, ACCESS_MASTER_ONLY }, // not listed
+	{ { "set", "anon", "on" },                         "set anon on [value]                     : activate the anonymized mode",                            cli_parse_set_anon, NULL, NULL },
+	{ { "set", "anon", "off" },                        "set anon off                            : deactivate the anonymized mode",                          cli_parse_set_anon, NULL, NULL },
+	{ { "set", "anon", "global-key", NULL },           "set anon global-key <value>             : change the global anonymizing key",                       cli_parse_set_global_key, NULL, NULL },
+	{ { "set", "global", "close-spread-time",  NULL }, "set close-spread-time <time>            : change the close-spread-time setting",                    cli_parse_set_close_spread_time, NULL, NULL },
+	{ { "get", "global", "close-spread-time",  NULL }, "get close-spread-time                   : get the close-spread-time setting",                       cli_parse_get_close_spread_time, NULL, NULL },
+	{ { "set", "maxconn", "global",  NULL },           "set maxconn global <value>              : change the per-process maxconn setting",                  cli_parse_set_maxconn_global, NULL },
+	{ { "set", "rate-limit", NULL },                   "set rate-limit <setting> <value>        : change a rate limiting value",                            cli_parse_set_ratelimit, NULL },
+	{ { "set", "severity-output",  NULL },             "set severity-output [none|number|string]: set presence of severity level in feedback information",  cli_parse_set_severity_output, NULL, NULL },
+	{ { "set", "timeout",  NULL },                     "set timeout [cli] <delay>               : change a timeout setting",                                cli_parse_set_timeout, NULL, NULL },
+	{ { "show", "anon", NULL },                        "show anon                               : display the current state of anonymized mode",            cli_parse_show_anon, NULL },
+	{ { "show", "env",  NULL },                        "show env [var]                          : dump environment variables known to the process",         cli_parse_show_env, cli_io_handler_show_env, NULL },
+	{ { "show", "cli", "sockets",  NULL },             "show cli sockets                        : dump list of cli sockets",                                cli_parse_default, cli_io_handler_show_cli_sock, NULL, NULL, ACCESS_MASTER },
+	{ { "show", "cli", "level", NULL },                "show cli level                          : display the level of the current CLI session",            cli_parse_show_lvl, NULL, NULL, NULL, ACCESS_MASTER},
+	{ { "show", "fd", NULL },                          "show fd [-!plcfbsd]* [num]              : dump list of file descriptors in use or a specific one",  cli_parse_show_fd, cli_io_handler_show_fd, NULL },
+	{ { "show", "version", NULL },                     "show version                            : show version of the current process",                     cli_parse_show_version, NULL, NULL, NULL, ACCESS_MASTER },
+	{ { "operator", NULL },                            "operator                                : lower the level of the current CLI session to operator",  cli_parse_set_lvl, NULL, NULL, NULL, ACCESS_MASTER},
+	{ { "user", NULL },                                "user                                    : lower the level of the current CLI session to user",      cli_parse_set_lvl, NULL, NULL, NULL, ACCESS_MASTER},
+	{ { "wait", NULL },                                "wait {-h|<delay_ms>} cond [args...]     : wait the specified delay or condition (-h to see list)",  cli_parse_wait, cli_io_handler_wait, cli_release_wait, NULL },
 	{{},}
 }};
 
diff --git a/src/proxy.c b/src/proxy.c
index 92c5df143..2fe320ae9 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -2151,6 +2151,8 @@ static void do_soft_stop_now()
 
 	if (tick_isset(global.close_spread_time)) {
 		global.close_spread_end = tick_add(now_ms, global.close_spread_time);
+		ha_notice("close-spread-time is currently set to %dms\n",
+			   global.close_spread_time);
 	}
 
 	/* schedule a hard-stop after a delay if needed */
-- 
2.34.1

