Add Bounded Ordinary HTTPS Idle Cleanup
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user