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";
}
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){
return &((struct srvparam *)node->value) -> childcount;
}
@ -886,6 +894,8 @@ static struct property prop_server[] = {
{"nonprintable", ef_server_nonprintable, TYPE_STRING, "non printable characters"},
{"replacement", ef_server_replacement, TYPE_CHAR, "replacement character"},
{"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"},
{"backlog", ef_server_backlog, TYPE_INTEGER, "listen backlog"},
{"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 */
extern int (*reattachfunc)(struct clientparam *param, int auth);
int clientauth(struct clientparam *param);
uint64_t *listenerclients(struct srvparam *srv);
uint64_t *listenerprocessed(struct srvparam *srv);
struct passwords * copypwl (struct passwords *pwl);
void freepwl(struct passwords *pw);
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
which may take long: the others then use what it opened */
_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;
_3proxy_mutex_t listener_mutex;
@ -1350,6 +1380,9 @@ static int srvspawn(struct srvstart *s, SOCKET client_sock){
}
else {
srv->childcount++;
#ifndef STDMAIN
listenercount(srv, 1);
#endif
if(!reused){
newparam->workerid = (uint64_t)thread;
_3proxy_mutex_lock(&tpool.mutex);
@ -1719,7 +1752,10 @@ void mainclaim(void *service){
free(newl);
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);
for(l = listeners; l; l = l->next){
@ -1745,6 +1781,7 @@ void mainclaim(void *service){
_3proxy_mutex_unlock(&listener_mutex);
if(newl){
_3proxy_mutex_destroy(&newl->open_mutex);
_3proxy_mutex_destroy(&newl->count_mutex);
free(newl->cbdest);
free(newl->netns);
free(newl->device);
@ -1793,6 +1830,7 @@ static void listenerrelease(struct srvparam *srv){
_3proxy_mutex_unlock(&listener_mutex);
if(!last) return;
_3proxy_mutex_destroy(&l->open_mutex);
_3proxy_mutex_destroy(&l->count_mutex);
if(l->netns) free(l->netns);
if(l->device) free(l->device);
if(l->cbdest) free(l->cbdest);
@ -2134,6 +2172,9 @@ void releaseparam(struct clientparam * param) {
param->next->prev = param->prev;
}
(param->srv->childcount)--;
#ifndef STDMAIN
listenercount(param->srv, -1);
#endif
/* under the mutex too: srvfree() may destroy the semaphore as
soon as the mutex shows the last child gone */
_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
connecting all the while are never refused nor reset."""
import re
import socket
import threading
import time
@ -67,6 +68,24 @@ def run(t):
t.eq(0, done["failed"], "no request is refused or reset by a reload "
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
# service goes on through it, its datagrams forwarded by the new one ---
echo = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)