Add Bounded Ordinary HTTPS Idle Cleanup

This commit is contained in:
2026-09-08 18:33:33 +02:00
parent f6263042ff
commit 82f21d6116
18 changed files with 884 additions and 13 deletions
+329
View File
@@ -0,0 +1,329 @@
/* SPDX-License-Identifier: GPL-3.0-only */
static struct sock_db sockets[WEB_HTTPD_IDLE_SOCKETS];
static struct httpd_data server;
static int peers[WEB_HTTPD_IDLE_SOCKETS];
static esp_tls_t tls_identity; /* Deliberately reuse this identity and descriptors. */
static int64_t handler_delay, purge_delay;
static size_t leftover;
static bool request_fail, purge_fail, upgrade;
static unsigned purges, responses;
static void pump(void)
{
assert(!lock_depth && !server.hd_req_aux.sd);
if (!queued_work) return;
void (*work)(void *) = queued_work;
void *arg = queued_arg;
queued_work = NULL; queued_arg = NULL;
bool previous = owner; owner = true; work(arg); owner = previous;
}
static esp_err_t httpd_queue_work(httpd_handle_t hd, void (*work)(void *), void *arg)
{
assert(!lock_depth && hd == &server && !queued_work);
++queue_calls;
if (queue_error != ESP_OK) return queue_error;
queued_work = work; queued_arg = arg;
if (inline_work) pump();
if (queue_hook) queue_hook();
return ESP_OK;
}
static int pending(httpd_handle_t hd, int fd)
{
assert(owner && !lock_depth && hd == &server);
return tls_pending[fd];
}
static void tick(int64_t at)
{
now_us = at;
bool previous = owner; owner = false; timer_callback(NULL); owner = previous;
pump();
}
static void drop(unsigned i)
{
if (sockets[i].fd >= 0) {
owner = true;
tls_identity.fd = sockets[i].fd;
esp_https_server_user_cb_arg_t arg = {.tls = &tls_identity, .user_cb_state = HTTPD_SSL_USER_CB_SESS_CLOSE};
web_httpd_idle_tls(&arg);
close(sockets[i].fd); sockets[i].fd = -1;
}
if (peers[i] >= 0) { close(peers[i]); peers[i] = -1; }
}
static void connect_slot(unsigned i, int reuse_fd)
{
int pair[2]; assert(socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == 0);
if (reuse_fd >= 0 && pair[0] != reuse_fd) {
assert(pair[1] != reuse_fd && dup2(pair[0], reuse_fd) == reuse_fd);
close(pair[0]); pair[0] = reuse_fd;
}
assert(pair[0] < FD_SETSIZE);
sockets[i] = (struct sock_db){.fd = pair[0], .pending_fn = pending};
peers[i] = pair[1]; tls_identity = (esp_tls_t){.fd = pair[0]};
owner = true;
esp_https_server_user_cb_arg_t arg = {.tls = &tls_identity, .user_cb_state = HTTPD_SSL_USER_CB_SESS_CREATE};
web_httpd_idle_tls(&arg);
}
static void reset(void)
{
/* Each test models a fresh process; restart tests below do NOT reset state. */
memset(s_rows, 0, sizeof(s_rows));
s_server = s_timer = NULL; s_generation = 0;
s_accepting = s_queued = s_submitting = false;
server = (struct httpd_data){.config.max_open_sockets = 6, .hd_td.handle = (void *)1, .hd_sd = sockets};
memset(sockets, 0, sizeof(sockets));
for (unsigned i = 0; i < 6; ++i) sockets[i].fd = peers[i] = -1;
memset(tls_pending, 0, sizeof(tls_pending));
owner = true; now_us = 0; lock_depth = shutdown_calls = select_calls = 0;
shutdown_error = select_error = 0;
timer_create_error = timer_start_error = queue_error = ESP_OK;
timer_creates = timer_starts = timer_deletes = queue_calls = 0;
timer_callback = queued_work = NULL; queued_arg = NULL; queue_hook = delay_hook = NULL;
inline_work = request_fail = purge_fail = upgrade = false;
handler_delay = purge_delay = 0; leftover = purges = responses = 0;
}
static void start(void)
{
assert(web_httpd_idle_prepare() == ESP_OK);
assert(web_httpd_idle_attach(&server) == ESP_OK);
}
static void stop(void)
{
owner = false; assert(web_httpd_idle_detach(&server) == ESP_OK);
/* Model successful HTTPD stop: join work, close sessions, destroy queue. */
pump();
for (unsigned i = 0; i < 6; ++i) drop(i);
queued_work = NULL; queued_arg = NULL;
web_httpd_idle_stopped(&server);
assert(!s_server && !s_queued && !s_submitting);
}
static esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd)
{
assert(owner && hd == &server && !hd->hd_req_aux.sd);
hd->hd_req_aux.sd = sd;
hd->hd_req_aux.remaining_len = leftover;
hd->hd_req = (httpd_req_t){.handle = hd, .aux = &hd->hd_req_aux};
/* Parser/handler/send run synchronously; timers can queue but cannot run
* work on HTTPD until this and the SDK's leftover-body purge return. */
timer_callback(NULL);
now_us += handler_delay;
unsigned before = shutdown_calls;
web_httpd_idle_sweep(hd, s_rows, now_us); /* defensive active-owner guard */
assert(shutdown_calls == before);
if (request_fail) { httpd_req_cleanup(&hd->hd_req); return ESP_FAIL; }
++responses;
if (upgrade) sd->ws_handshake_done = true;
return ESP_OK;
}
static int httpd_req_recv(httpd_req_t *req, char *data, size_t size)
{
assert(owner && req->aux && req->aux->sd && size);
++purges; now_us += purge_delay;
timer_callback(NULL);
if (purge_fail) return -1;
memset(data, 0, size); req->aux->remaining_len -= size;
return (int)size;
}
static void httpd_req_cleanup(httpd_req_t *req)
{
req->aux->sd = NULL; req->aux = NULL; req->handle = NULL;
}
static void complete(unsigned i)
{
owner = true;
if (httpd_sess_process(&server, &sockets[i]) != ESP_OK) drop(i);
pump();
}
static void inline_submit_hook(void)
{
unsigned before = queue_calls;
assert(!s_queued && s_submitting);
idle_timer(NULL); assert(queue_calls == before);
}
static void stop_during_submit(void)
{
assert(s_submitting && s_queued);
assert(web_httpd_idle_detach(&server) == ESP_ERR_TIMEOUT);
assert(s_server == &server && !s_accepting && s_submitting);
assert(web_httpd_idle_prepare() == ESP_ERR_INVALID_STATE);
}
int main(void)
{
reset(); timer_create_error = ESP_ERR_NO_MEM;
assert(web_httpd_idle_prepare() == ESP_ERR_NO_MEM && !s_timer && !s_server);
timer_create_error = ESP_OK; timer_start_error = ESP_FAIL;
assert(web_httpd_idle_prepare() == ESP_FAIL && !s_timer && timer_deletes == 1);
timer_start_error = ESP_OK; start();
assert(timer_creates == 3 && timer_starts == 2);
stop(); start(); assert(timer_creates == 3); stop();
puts("PASS timer failure cleanup/retry and one persistent timer across restart");
reset(); start();
for (unsigned i = 0; i < 6; ++i) connect_slot(i, -1);
sockets[4].ws_handshake_done = sockets[5].ws_handshake_done = true;
tick(0); tick(14999999); assert(!shutdown_calls);
tick(15000000); assert(shutdown_calls == 4);
tick(16000000); assert(shutdown_calls == 4);
for (unsigned i = 0; i < 4; ++i) {
char byte; assert(recv(peers[i], &byte, 1, MSG_DONTWAIT) == 0);
/* HTTPD's next read owns TLS/free/slot retirement, not the sweep. */
assert(sockets[i].fd >= 0); drop(i);
}
connect_slot(0, -1); sockets[0].ws_handshake_done = true; /* newly admitted admin */
tick(60000000); assert(shutdown_calls == 4); stop();
puts("PASS six full slots: only four expired ordinary sockets shut down; two serial and new admin WS survive");
reset(); start(); connect_slot(0, -1); tick(0);
for (unsigned i = 1; i <= 12; ++i) { now_us = (int64_t)i * 5000000; complete(0); }
assert(responses == 12 && !shutdown_calls && s_rows[0].idle_since_us == now_us);
tick(now_us + 15000000); assert(shutdown_calls == 1); stop();
puts("PASS actual SDK completion marker refreshes five-second ordinary polling without TLS churn");
reset(); start(); connect_slot(0, -1); tick(0);
assert(send(peers[0], "G", 1, 0) == 1); tick(15000000); assert(!shutdown_calls);
char byte; assert(recv(sockets[0].fd, &byte, 1, 0) == 1);
now_us = 16000000; complete(0); tick(30000000); assert(!shutdown_calls);
tick(31000000); assert(shutdown_calls == 1); stop();
puts("PASS control-before-data ordering: readable incomplete next request is not expired");
for (unsigned mode = 0; mode < 4; ++mode) {
reset(); start(); connect_slot(0, -1); tick(0);
if (mode == 0) sockets[0].pending_len = 1;
if (mode == 1) tls_pending[sockets[0].fd] = 1;
if (mode == 2) tls_pending[sockets[0].fd] = -1;
if (mode == 3) select_error = 1;
tick(15000000); tick(60000000); assert(!shutdown_calls);
sockets[0].pending_len = 0; tls_pending[sockets[0].fd] = select_error = 0;
tick(74999999); assert(!shutdown_calls); tick(75000000); assert(shutdown_calls == 1); stop();
}
puts("PASS HTTPD pipeline/TLS buffered input and TLS/select errors conservatively restart idle window");
reset(); start(); connect_slot(0, -1); tick(0);
handler_delay = 40000000; purge_delay = 10000000; leftover = 64;
now_us = 14000000; complete(0);
assert(now_us == 74000000 && purges == 2 && responses == 1 && !shutdown_calls);
assert(s_rows[0].completed == 1 && s_rows[0].idle_since_us == now_us);
tick(88999999); assert(!shutdown_calls); tick(89000000); assert(shutdown_calls == 1); stop();
puts("PASS slow parser/handler/response plus real SDK leftover purge: queued probe observes completion only afterwards");
for (unsigned mode = 0; mode < 2; ++mode) {
reset(); start(); connect_slot(0, -1); tick(0);
request_fail = mode == 0; purge_fail = mode == 1; leftover = 64;
handler_delay = 60000000; complete(0);
assert(sockets[0].fd == -1 && server.lru_counter == 0 && !shutdown_calls); stop();
}
puts("PASS request/purge failure deletion precedes work and never publishes successful completion");
reset(); start(); connect_slot(0, -1); tick(0); now_us = 14000000;
upgrade = true; complete(0); tick(60000000);
assert(sockets[0].ws_handshake_done && !s_rows[0].observed && !shutdown_calls); stop();
reset(); start(); connect_slot(0, -1); tick(0);
sockets[0].for_async_req = true; tick(60000000); assert(!shutdown_calls && !s_rows[0].observed);
sockets[0].for_async_req = false; tick(61000000); tick(75999999); assert(!shutdown_calls);
tick(76000000); assert(shutdown_calls == 1); stop();
puts("PASS admission upgrade classification before idle publication and async response exemption");
reset(); start(); connect_slot(0, -1); tick(0);
int fd = sockets[0].fd; drop(0); now_us = 14000000; connect_slot(0, fd);
/* Both old and new lru=0, same fd, TLS pointer and sock_db address. */
tick(15000000); assert(!shutdown_calls && s_rows[0].idle_since_us == 15000000);
tick(30000000); assert(shutdown_calls == 1);
drop(0); connect_slot(0, fd); sockets[0].ws_handshake_done = true;
tick(90000000); assert(shutdown_calls == 1); stop();
puts("PASS identical fd/TLS/sock_db/LRU reuse resets identity; no late shutdown targets replacement WS");
reset(); start(); connect_slot(0, -1); tick(0);
shutdown_error = 1; tick(15000000); assert(shutdown_calls == 1 && !s_rows[0].shutdown_sent);
shutdown_error = 0; tick(16000000); assert(shutdown_calls == 2 && s_rows[0].shutdown_sent);
tick(17000000); assert(shutdown_calls == 2); stop();
puts("PASS failed shutdown retries, successful shutdown is latched until normal owner deletion");
reset(); start(); connect_slot(0, -1); tick(0);
queue_error = ESP_FAIL; tick(15000000); tick(16000000);
assert(!s_queued && !s_submitting && !shutdown_calls);
queue_error = ESP_OK; tick(17000000); assert(shutdown_calls == 1); stop();
puts("PASS queue failures release reservations and later probe enforces unchanged deadline");
reset(); start(); connect_slot(0, -1);
idle_timer(NULL); void *old_arg = queued_arg;
for (unsigned i = 0; i < 100; ++i) idle_timer(NULL);
assert(queue_calls == 1 && s_queued); pump();
inline_work = true; queue_hook = inline_submit_hook;
idle_timer(NULL); assert(queue_calls == 2 && !s_submitting && !s_queued);
queue_hook = NULL; inline_work = false; stop();
puts("PASS at most one probe; callback-before-submit-return cannot clear or submit a newer probe");
reset(); start(); connect_slot(0, -1); tick(0);
now_us = 20000000; queue_hook = stop_during_submit;
idle_timer(NULL); queue_hook = NULL;
assert(!s_submitting && s_queued && !s_accepting && !shutdown_calls);
assert(web_httpd_idle_detach(&server) == ESP_OK);
/* Failed SSL stop: MUST NOT call stopped, prepare/restart remains blocked. */
assert(web_httpd_idle_prepare() == ESP_ERR_INVALID_STATE);
assert(web_httpd_idle_attach(&server) == ESP_ERR_INVALID_STATE);
pump(); assert(!s_queued && !shutdown_calls && s_server == &server);
unsigned calls = queue_calls; tick(60000000); assert(queue_calls == calls);
stop(); start(); assert(s_accepting); stop();
puts("PASS in-flight submit fence timeout forbids stop; failed stop disables probes but retains ownership until retry");
reset(); start(); connect_slot(0, -1); tick(0); idle_timer(NULL); old_arg = queued_arg;
assert(web_httpd_idle_detach(&server) == ESP_OK && s_queued);
/* Successful SDK stop may discard, rather than run, the queued work. */
queued_work = NULL; queued_arg = NULL; drop(0); web_httpd_idle_stopped(&server);
assert(!s_queued); start(); connect_slot(0, -1); idle_timer(NULL);
owner = true; idle_work(old_arg);
assert(s_queued && queued_work && !shutdown_calls); pump(); stop();
puts("PASS successful stop retires discarded queue; stale generation cannot act on reused server or clear new probe");
reset(); start(); connect_slot(0, -1); tick(0);
tls_identity.get_error = true;
esp_https_server_user_cb_arg_t arg = {.tls = &tls_identity, .user_cb_state = HTTPD_SSL_USER_CB_SESS_CREATE};
web_httpd_idle_tls(&arg); assert(!s_rows[0].observed);
tick(15000000); assert(!shutdown_calls); stop();
puts("PASS unavailable TLS identity fails conservatively, independent of diagnostics");
reset(); start(); connect_slot(0, -1); tick(0); now_us = 60000000;
owner = false; web_httpd_idle_sweep(&server, s_rows, now_us); assert(!shutdown_calls);
owner = true; server.config.max_open_sockets = 7;
web_httpd_idle_sweep(&server, s_rows, now_us); assert(!shutdown_calls);
server.config.max_open_sockets = 6; sockets[0].ws_close = true;
tick(now_us); assert(!shutdown_calls); stop();
s_generation = UINTPTR_MAX;
assert(web_httpd_idle_prepare() == ESP_ERR_INVALID_STATE);
assert(web_httpd_idle_attach(&server) == ESP_ERR_INVALID_STATE);
puts("PASS owner/capacity/closing-WS guards and nonwrapping restart generation");
reset(); start(); idle_timer(NULL);
/* TLS establishment occupies HTTPD synchronously; only timer task runs. */
for (unsigned i = 0; i < 5; ++i) { now_us += 1000000; idle_timer(NULL); }
assert(queue_calls == 1 && !shutdown_calls && !s_rows[0].observed);
connect_slot(0, -1); pump();
assert(s_rows[0].idle_since_us == 5000000);
tick(19999999); assert(!shutdown_calls); tick(20000000); assert(shutdown_calls == 1); stop();
puts("PASS serialized TLS handshake delays probe; post-TLS unused connection gets a fresh full idle window");
reset(); start(); connect_slot(0, -1); tick(0); idle_timer(NULL);
/* Nonblocking IDF control transport is UDP: success is not an execution
* acknowledgement. Never time out the reservation and accumulate probes. */
queued_work = NULL; queued_arg = NULL; calls = queue_calls;
tick(60000000); tick(120000000);
assert(s_queued && queue_calls == calls && !shutdown_calls);
stop(); start(); connect_slot(0, -1); tick(120000000);
tick(135000000); assert(shutdown_calls == 1); stop();
puts("PASS accepted-but-lost UDP work remains bounded/fail-safe; successful stop/restart restores probing");
puts("18 idle lifecycle groups passed (real socket IO, installed SDK completion/purge, deterministic TLS/scheduler)");
return 0;
}