Count the clients of each listener

A listener counts its clients, whichever of its services they came to,
old or new: those connected now, raised when a client is spawned, and
those done with, raised as one leaves its service. As the listener
outlives a reload, so do the counts.

The services page of the admin interface shows them for each service as
listenerclients and listenerprocessed; services sharing a socket show
the same counts. The takeover case checks that the count goes on
through three reloads and that no client is left counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vladimir Dubrovin 2026-10-15 14:52:06 +03:00
parent d83a48fe18
commit 113c9eaebe
4 changed files with 73 additions and 1 deletions

View File

@ -530,6 +530,14 @@ static void * ef_server_auth(struct node * node){
return "uknown"; return "uknown";
} }
static void * ef_server_listenerclients(struct node * node){
return listenerclients((struct srvparam *)node->value);
}
static void * ef_server_listenerprocessed(struct node * node){
return listenerprocessed((struct srvparam *)node->value);
}
static void * ef_server_childcount(struct node * node){ static void * ef_server_childcount(struct node * node){
return &((struct srvparam *)node->value) -> childcount; return &((struct srvparam *)node->value) -> childcount;
} }
@ -886,6 +894,8 @@ static struct property prop_server[] = {
{"nonprintable", ef_server_nonprintable, TYPE_STRING, "non printable characters"}, {"nonprintable", ef_server_nonprintable, TYPE_STRING, "non printable characters"},
{"replacement", ef_server_replacement, TYPE_CHAR, "replacement character"}, {"replacement", ef_server_replacement, TYPE_CHAR, "replacement character"},
{"childcount", ef_server_childcount, TYPE_INTEGER, "number of servers connected"}, {"childcount", ef_server_childcount, TYPE_INTEGER, "number of servers connected"},
{"listenerclients", ef_server_listenerclients, TYPE_UNSIGNED64, "clients connected to the listening socket, of its services old and new"},
{"listenerprocessed", ef_server_listenerprocessed, TYPE_UNSIGNED64, "clients of the listening socket done with"},
{"maxchild", ef_server_maxchild, TYPE_INTEGER, "max concurrent connections"}, {"maxchild", ef_server_maxchild, TYPE_INTEGER, "max concurrent connections"},
{"backlog", ef_server_backlog, TYPE_INTEGER, "listen backlog"}, {"backlog", ef_server_backlog, TYPE_INTEGER, "listen backlog"},
{"needuser", ef_server_needuser, TYPE_INTEGER, "require user authentication"}, {"needuser", ef_server_needuser, TYPE_INTEGER, "require user authentication"},

View File

@ -404,6 +404,8 @@ int reattach(struct clientparam *param, int auth);
/* reattach() where configurations are reloaded, NULL elsewhere */ /* reattach() where configurations are reloaded, NULL elsewhere */
extern int (*reattachfunc)(struct clientparam *param, int auth); extern int (*reattachfunc)(struct clientparam *param, int auth);
int clientauth(struct clientparam *param); int clientauth(struct clientparam *param);
uint64_t *listenerclients(struct srvparam *srv);
uint64_t *listenerprocessed(struct srvparam *srv);
struct passwords * copypwl (struct passwords *pwl); struct passwords * copypwl (struct passwords *pwl);
void freepwl(struct passwords *pw); void freepwl(struct passwords *pw);
int copyfilter(struct filter *, struct srvparam *srv); int copyfilter(struct filter *, struct srvparam *srv);

View File

@ -903,7 +903,37 @@ struct listener {
/* held while the first of its services opens the socket, binding /* held while the first of its services opens the socket, binding
which may take long: the others then use what it opened */ which may take long: the others then use what it opened */
_3proxy_mutex_t open_mutex; _3proxy_mutex_t open_mutex;
/* its clients, whichever of its services, old or new, they came to:
those connected now, and those done with */
uint64_t clients, processed;
_3proxy_mutex_t count_mutex;
}; };
/* A client of srv comes (1) or goes (-1). */
static void listenercount(struct srvparam *srv, int delta){
struct listener *l = srv->listener;
if(!l) return;
_3proxy_mutex_lock(&l->count_mutex);
if(delta > 0) l->clients++;
else {
l->clients--;
l->processed++;
}
_3proxy_mutex_unlock(&l->count_mutex);
}
static uint64_t nocount = 0;
/* The counters of a running service's listener, for the admin pages; the
listener lasts as long as the service. */
uint64_t *listenerclients(struct srvparam *srv){
return srv->listener? &srv->listener->clients : &nocount;
}
uint64_t *listenerprocessed(struct srvparam *srv){
return srv->listener? &srv->listener->processed : &nocount;
}
static struct listener *listeners = NULL; static struct listener *listeners = NULL;
_3proxy_mutex_t listener_mutex; _3proxy_mutex_t listener_mutex;
@ -1350,6 +1380,9 @@ static int srvspawn(struct srvstart *s, SOCKET client_sock){
} }
else { else {
srv->childcount++; srv->childcount++;
#ifndef STDMAIN
listenercount(srv, 1);
#endif
if(!reused){ if(!reused){
newparam->workerid = (uint64_t)thread; newparam->workerid = (uint64_t)thread;
_3proxy_mutex_lock(&tpool.mutex); _3proxy_mutex_lock(&tpool.mutex);
@ -1719,7 +1752,10 @@ void mainclaim(void *service){
free(newl); free(newl);
newl = NULL; newl = NULL;
} }
else _3proxy_mutex_init(&newl->open_mutex); else {
_3proxy_mutex_init(&newl->open_mutex);
_3proxy_mutex_init(&newl->count_mutex);
}
} }
_3proxy_mutex_lock(&listener_mutex); _3proxy_mutex_lock(&listener_mutex);
for(l = listeners; l; l = l->next){ for(l = listeners; l; l = l->next){
@ -1745,6 +1781,7 @@ void mainclaim(void *service){
_3proxy_mutex_unlock(&listener_mutex); _3proxy_mutex_unlock(&listener_mutex);
if(newl){ if(newl){
_3proxy_mutex_destroy(&newl->open_mutex); _3proxy_mutex_destroy(&newl->open_mutex);
_3proxy_mutex_destroy(&newl->count_mutex);
free(newl->cbdest); free(newl->cbdest);
free(newl->netns); free(newl->netns);
free(newl->device); free(newl->device);
@ -1793,6 +1830,7 @@ static void listenerrelease(struct srvparam *srv){
_3proxy_mutex_unlock(&listener_mutex); _3proxy_mutex_unlock(&listener_mutex);
if(!last) return; if(!last) return;
_3proxy_mutex_destroy(&l->open_mutex); _3proxy_mutex_destroy(&l->open_mutex);
_3proxy_mutex_destroy(&l->count_mutex);
if(l->netns) free(l->netns); if(l->netns) free(l->netns);
if(l->device) free(l->device); if(l->device) free(l->device);
if(l->cbdest) free(l->cbdest); if(l->cbdest) free(l->cbdest);
@ -2134,6 +2172,9 @@ void releaseparam(struct clientparam * param) {
param->next->prev = param->prev; param->next->prev = param->prev;
} }
(param->srv->childcount)--; (param->srv->childcount)--;
#ifndef STDMAIN
listenercount(param->srv, -1);
#endif
/* under the mutex too: srvfree() may destroy the semaphore as /* under the mutex too: srvfree() may destroy the semaphore as
soon as the mutex shows the last child gone */ soon as the mutex shows the last child gone */
_3proxy_sem_unlock(param->srv->childslots); _3proxy_sem_unlock(param->srv->childslots);

View File

@ -2,6 +2,7 @@
the same address and port takes over the old service's socket, so clients the same address and port takes over the old service's socket, so clients
connecting all the while are never refused nor reset.""" connecting all the while are never refused nor reset."""
import re
import socket import socket
import threading import threading
import time import time
@ -67,6 +68,24 @@ def run(t):
t.eq(0, done["failed"], "no request is refused or reset by a reload " t.eq(0, done["failed"], "no request is refused or reset by a reload "
f"(first errors: {done['errors']})") f"(first errors: {done['errors']})")
# the listener counts its clients, whichever service took them: through
# the reloads the proxy's socket is one, its count goes on
def listener_counts(service):
"""The listener counters of the running service of that type: its
item has nested ones, acl and clients, before its counters."""
page = t.http(f"http://127.0.0.1:{adm}/S").text
m = re.search(r"type of the service/client</description><value><!\[CDATA\[" + service +
r"\]\].*?<name>listenerclients</name>.*?CDATA\[(\d+)\]"
r".*?<name>listenerprocessed</name>.*?CDATA\[(\d+)\]", page, re.S)
return (int(m.group(1)), int(m.group(2))) if m else (None, None)
clients, processed = listener_counts("PROXY")
t.eq(0, clients, "no client is connected to the proxy's listener after the run")
total = done["ok"] + done["failed"]
t.ok(f"its count of clients done with goes on through the reloads ({processed} of {total})") \
if processed is not None and processed >= total else \
t.fail("its count of clients done with goes on through the reloads", f"at least {total}", processed)
# --- UDP: the socket is handed over too, and a session of the old # --- UDP: the socket is handed over too, and a session of the old
# service goes on through it, its datagrams forwarded by the new one --- # service goes on through it, its datagrams forwarded by the new one ---
echo = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) echo = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)