Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions doc/management.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,30 @@ clear counters all
server. This has the same effect as restarting. This command is restricted
and can only be issued on sockets configured for level "admin".

clear counters server <backend>/<server> [force]
Reset the statistics counters of a single server. This command is useful when
a server slot is being reused to represent a different logical entity (e.g.
a different Kubernetes pod occupying the same slot after a rename via
"set server <b>/<s> name") and per-entity counter attribution is required.
It behaves like "clear counters all" but scoped to one server rather than the
whole process.

The reset covers both accumulated counters (bytes, sessions, requests,
errors, response codes) and max values, as well as the extra counters
registered by modules (e.g. mux stats) for that server. As for the plain
"clear counters", only clearable modules are reset. It does not affect the
server's runtime state (address, weight, admin state) or health checks.

When the server's counters are stored in a shared-memory stats file (see
"shm-stats-file"), clearing them breaks the monotonicity that monitoring
tools consuming the shared stats rely on, and the reset is visible to every
process attached to the same object. For this reason the command refuses to
clear such a server unless the "force" keyword is appended.

Because it clears accumulated counters, this command is restricted and can
only be issued on sockets configured for level "admin", like "clear counters
all".

clear acl [@<ver>] <acl>
Remove all entries from the acl <acl>. <acl> is the #<id> or the <name>
returned by "show acl". Note that if the reference <acl> is a name and is
Expand Down
10 changes: 10 additions & 0 deletions include/haproxy/counters-t.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ struct fe_counters_shared {

/* counters used by listeners and frontends */
struct fe_counters {
/* shared must stay the first member: counters_fe_reset() zeroes
* everything after it in one memset, so any new field added below is
* reset automatically. It holds the shared.tg pointer array which the
* hot path dereferences without a NULL check and must never be zeroed.
*/
struct fe_counters_shared shared; /* shared counters */
unsigned int conn_max; /* max # of active sessions */

Expand Down Expand Up @@ -165,6 +170,11 @@ struct be_counters_shared {

/* counters used by servers and backends */
struct be_counters {
/* shared must stay the first member: counters_be_reset() zeroes
* everything after it in one memset, so any new field added below is
* reset automatically. It holds the shared.tg pointer array which the
* hot path dereferences without a NULL check and must never be zeroed.
*/
struct be_counters_shared shared; /* shared counters */
unsigned int conn_max; /* max # of active sessions */

Expand Down
18 changes: 18 additions & 0 deletions include/haproxy/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ int counters_be_shared_prepare(struct be_counters_shared *counters, const struct
void counters_fe_shared_drop(struct fe_counters_shared *counters);
void counters_be_shared_drop(struct be_counters_shared *counters);

/* Safe counter reset: zero all counter fields while preserving the shared.tg
* pointer array and per-tgroup allocations (which the hot path dereferences
* without a NULL check). The shared.flags field is also preserved because
* COUNTERS_SHARED_F_LOCAL is set once at init and reflects allocation
* ownership, not counter state.
*/
void counters_fe_reset(struct fe_counters *counters);
void counters_be_reset(struct be_counters *counters);

/* Reset only the max/peak fields of <counters>, leaving cumulative counters
* intact. This is what the plain "clear counters" command does (as opposed to
* "clear counters all" which performs a full reset via counters_*_reset()).
* The max fields are interleaved with live cumulative fields, so they are
* cleared individually rather than with a blanket memset.
*/
void counters_fe_reset_max(struct fe_counters *counters);
void counters_be_reset_max(struct be_counters *counters);

/* time oriented helper: get last time (relative to current time) on a given
* <scounter> array, for <elem> member (one member per thread group) which is
* assumed to be unsigned long type.
Expand Down
1 change: 1 addition & 0 deletions include/haproxy/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ int srv_set_addr_via_libc(struct server *srv, int *err_code);
int srv_postinit(struct server *srv);
int srv_init_addr(void);
struct server *cli_find_server(struct appctx *appctx, char *arg);
int cli_clear_counters_server(struct appctx *appctx, char *arg, int force);
struct server *new_server(struct proxy *proxy);
void srv_take(struct server *srv);
struct server *srv_drop(struct server *srv);
Expand Down
2 changes: 2 additions & 0 deletions include/haproxy/stats-proxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
struct buffer;
struct htx;
struct stconn;
struct server;

int stats_dump_proxies(struct stconn *sc, struct buffer *buf, struct htx *htx);

void proxy_stats_clear_counters(int clrall, struct list *stat_modules);
void srv_stats_clear_extra_counters(struct server *sv);

#endif /* _HAPROXY_STATS_PROXY_H */
75 changes: 75 additions & 0 deletions reg-tests/stats/clear_counters_all_survives.vtc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
varnishtest "'clear counters all' preserves the shared.tg pointer"

# Regression test for a segfault triggered by 'clear counters all' after the
# switch to per-thread-group shared counters. A blanket memset on the outer
# counters struct zeroed sv->counters.shared.tg, a pointer that every hot
# path dereferences with no NULL check, so the next request after the clear
# would crash. With counters_be_reset() / counters_fe_reset() the pointer
# is preserved and only the pointed-at counter contents are cleared.
#
# This test drives traffic, clears counters, then drives more traffic. If
# the fix regresses, the second traffic burst crashes the HAProxy process
# and vtest reports the connection as failed.

feature ignore_unknown_macro

#REGTEST_TYPE=devel

server s1 {
rxreq
txresp
} -start

server s1_more {
rxreq
txresp
} -start

haproxy h1 -conf {
global
.if feature(THREAD)
thread-groups 1
.endif

defaults
mode http
timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
timeout server "${HAPROXY_TEST_TIMEOUT-5s}"

frontend fe
bind "fd@${feS}"
default_backend be

backend be
server s1 ${s1_addr}:${s1_port}
} -start

# First burst of traffic to accumulate counters on the server.
client c1 -connect ${h1_feS_sock} {
txreq
rxresp
expect resp.status == 200
} -run

# Clear all counters via the CLI. Before the fix, this would null
# shared.tg and cause the *next* request to crash the process.
haproxy h1 -cli {
send "clear counters all"
expect ~ ".*"
}

# Rebind backend to a fresh varnishtest server since the first one
# consumed its single rxreq/txresp.
haproxy h1 -cli {
send "set server be/s1 addr ${s1_more_addr} port ${s1_more_port}"
expect ~ ".*"
}

# Second burst of traffic. If the process crashed on the counter clear,
# the connection is refused and vtest fails.
client c2 -connect ${h1_feS_sock} {
txreq
rxresp
expect resp.status == 200
} -run
89 changes: 89 additions & 0 deletions reg-tests/stats/clear_counters_server.vtc
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
varnishtest "Clear a single server's counters via CLI"

feature ignore_unknown_macro

#REGTEST_TYPE=devel

# echo server providing s1_addr / s1_port macros; serves two requests
server s1 {
rxreq
txresp
} -repeat 2 -start

server s1_extra {
rxreq
txresp
} -start

haproxy h1 -conf {
global
.if feature(THREAD)
thread-groups 1
.endif

defaults
mode http
timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
timeout server "${HAPROXY_TEST_TIMEOUT-5s}"

frontend fe
bind "fd@${feS}"
default_backend be

backend be
server s1 ${s1_addr}:${s1_port}
server s2 ${s1_extra_addr}:${s1_extra_port} backup
} -start

# Drive two requests; both go to s1 (s2 is backup, only used if s1 is down)
# so s1 accumulates two sessions while s2 stays at zero.
client c1 -connect ${h1_feS_sock} {
txreq
rxresp
expect resp.status == 200
} -run

client c2 -connect ${h1_feS_sock} {
txreq
rxresp
expect resp.status == 200
} -run

haproxy h1 -cli {
# ---- error cases ----

# unknown backend
send "clear counters server nosuch/s1"
expect ~ "No such backend"

# unknown server
send "clear counters server be/nosuch"
expect ~ "No such server"

# ---- success case ----

# precondition: s1 handled both requests (stot, CSV field 8) while the
# backup s2 stayed at zero. Both server rows are matched from a single
# response (one expect per send; the CLI connection closes afterwards).
send "show stat be 4 -1"
expect ~ "be,s1,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,2,[^\n]*\nbe,s2,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,0,"

# clear only s1's counters
send "clear counters server be/s1"
expect ~ "Server counters cleared."

# s1's cumulative sessions are now back to 0; s2 is untouched (still 0),
# proving the reset is scoped to the named server.
send "show stat be 4 -1"
expect ~ "be,s1,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,0,[^\n]*\nbe,s2,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,0,"

# 'force' is accepted and is a no-op override for process-local counters
# (there is no shared-memory stats file here), so it still succeeds.
send "clear counters server be/s2 force"
expect ~ "Server counters cleared."

# sanity: the command (with its 'force' option) is discoverable via help
send "help"
expect ~ "clear counters .*server.*force"
}
107 changes: 107 additions & 0 deletions src/counters.c
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,110 @@ int counters_be_shared_prepare(struct be_counters_shared *shared, const struct g
{
return _counters_shared_prepare((struct counters_shared *)shared, guid, 1, errmsg);
}

/* Reset the contents of the per-thread-group shared counters attached to
* <shared>, while preserving the shared.tg pointer array and the per-tgroup
* allocations themselves. This is the safe way to reset counters at runtime:
* the hot path dereferences shared.tg[tgid-1] with no NULL check, so nulling
* that pointer (as a blanket memset on the outer struct would do) segfaults on
* the next request. The shared.flags field is also preserved because
* COUNTERS_SHARED_F_LOCAL is set once at init and reflects allocation
* ownership, not counter state.
*
* In shared-memory (SHM stats file) mode, zeroing the per-tgroup structs
* affects every process attached to the same object. This is the intended
* "reset everywhere" semantic of clear counters.
*/
static void _counters_shared_reset(struct counters_shared *shared, size_t tg_size)
{
int it;

if (!shared || !shared->tg)
return;

for (it = 0; it < global.nbtgroups; it++) {
if (shared->tg[it])
memset(shared->tg[it], 0, tg_size);
}
}

/* Reset all counter fields of <counters> while preserving the shared.tg
* pointer array and the per-tgroup allocations. See _counters_shared_reset()
* for the pointer-lifetime rationale. Both the per-tgroup accumulated
* counters (bytes, sessions, requests, errors, response codes, ...) and the
* local max/rate counters are cleared.
*/
void counters_fe_reset(struct fe_counters *counters)
{
if (!counters)
return;

_counters_shared_reset((struct counters_shared *)&counters->shared,
sizeof(struct fe_counters_shared_tg));

/* Zero all local (non-shared) fields. Note this deliberately is NOT a
* plain memset() over the whole struct: shared is the first member and
* holds the shared.tg pointer array that the hot path dereferences
* without a NULL check, so blanket-zeroing it would segfault the next
* request. We skip exactly the shared sub-struct and clear the rest;
* every field after it is numeric, so any field added later is reset
* automatically without touching this code.
*/
memset((char *)counters + sizeof(counters->shared), 0,
sizeof(*counters) - sizeof(counters->shared));
}

/* Reset all counter fields of <counters>. See counters_fe_reset(). */
void counters_be_reset(struct be_counters *counters)
{
if (!counters)
return;

_counters_shared_reset((struct counters_shared *)&counters->shared,
sizeof(struct be_counters_shared_tg));

memset((char *)counters + sizeof(counters->shared), 0,
sizeof(*counters) - sizeof(counters->shared));
}

/* Reset only the max/peak gauges of a frontend/listener <counters> struct,
* leaving cumulative counters intact. Used by the plain "clear counters"
* command (as opposed to "clear counters all" which resets everything via
* counters_fe_reset()).
*/
void counters_fe_reset_max(struct fe_counters *counters)
{
if (!counters)
return;

counters->conn_max = 0;
counters->cps_max = 0;
counters->sps_max = 0;
counters->p.http.rps_max = 0;
}

/* Reset only the max/peak gauges of a backend/server <counters> struct,
* leaving cumulative counters intact. See counters_fe_reset_max().
*
* Note: this clears the union of all max gauges present in the struct. The
* previous inline code cleared slightly different subsets for backends and
* servers (backends left cur_sess_max untouched, servers left conn_max /
* cps_max / rps_max untouched); those were latent gaps, so plain "clear
* counters" now consistently resets every peak gauge for both.
*/
void counters_be_reset_max(struct be_counters *counters)
{
if (!counters)
return;

counters->conn_max = 0;
counters->cps_max = 0;
counters->sps_max = 0;
counters->nbpend_max = 0;
counters->cur_sess_max = 0;
counters->qtime_max = 0;
counters->ctime_max = 0;
counters->dtime_max = 0;
counters->ttime_max = 0;
counters->p.http.rps_max = 0;
}
Loading
Loading