commit fd16f9d2cbd085e84bc903326d9434aff74ea890
parent d59f3045248ff848f3e3bb9698f1c4094fc9c8b8
Author: Christian Grothoff <christian@grothoff.org>
Date: Tue, 28 Jul 2026 23:13:46 +0200
add test for suspend_resume_epoll example
Diffstat:
3 files changed, 715 insertions(+), 1 deletion(-)
diff --git a/src/examples/.gitignore b/src/examples/.gitignore
@@ -40,3 +40,8 @@ minimal_example_empty
minimal_example_empty_tls
/digest_auth_example_adv
json_echo
+/suspend_resume_epoll
+/test_suspend_resume_epoll
+# Produced by the automake test harness
+*.log
+*.trs
diff --git a/src/examples/Makefile.am b/src/examples/Makefile.am
@@ -3,7 +3,7 @@ SUBDIRS = .
AM_CPPFLAGS = \
-I$(top_srcdir)/src/include \
- $(CPPFLAGS_ac) \
+ $(CPPFLAGS_ac) $(LIBCURL_CPPFLAGS) \
-DDATA_DIR=\"$(top_srcdir)/src/datadir/\"
AM_CFLAGS = $(CFLAGS_ac) @LIBGCRYPT_CFLAGS@
@@ -42,6 +42,12 @@ noinst_PROGRAMS = \
if MHD_HAVE_EPOLL
noinst_PROGRAMS += \
suspend_resume_epoll
+if HAVE_FORK_WAITPID
+if RUN_LIBCURL_TESTS
+check_PROGRAMS = test_suspend_resume_epoll
+TESTS = $(check_PROGRAMS)
+endif
+endif
endif
if HAVE_JANSSON
@@ -175,6 +181,11 @@ suspend_resume_epoll_CPPFLAGS = \
suspend_resume_epoll_LDADD = \
$(top_builddir)/src/microhttpd/libmicrohttpd.la
+test_suspend_resume_epoll_SOURCES = \
+ test_suspend_resume_epoll.c
+test_suspend_resume_epoll_LDADD = \
+ @LIBCURL@
+
benchmark_https_SOURCES = \
benchmark_https.c
benchmark_https_CPPFLAGS = \
diff --git a/src/examples/test_suspend_resume_epoll.c b/src/examples/test_suspend_resume_epoll.c
@@ -0,0 +1,698 @@
+/*
+ This file is part of libmicrohttpd
+ Copyright (C) 2026 Christian Grothoff (and other contributing authors)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+/**
+ * @file test_suspend_resume_epoll.c
+ * @brief Test for the suspend_resume_epoll example. The example itself
+ * is started as a child process and then driven over HTTP, so
+ * what is tested is exactly the code that ships as the example.
+ *
+ * The example parks every request for one second on a timerfd
+ * that lives in the application's own epoll set, next to the
+ * daemon's epoll FD. Checking that the right bytes come back is
+ * the easy part; the checks that matter are that the process
+ * burns no CPU while a connection is suspended, and that two
+ * requests are parked at the same time rather than one after the
+ * other. Both would still deliver identical bytes if suspending
+ * were broken.
+ * @author Christian Grothoff
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <curl/curl.h>
+#include <errno.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+/**
+ * How long the example parks a request, in milliseconds. This is the
+ * one-second timer hard-coded in suspend_resume_epoll.c.
+ */
+#define PARK_MS 1000
+
+/**
+ * Upper bound for any single request, in seconds.
+ */
+#define REQUEST_TIMEOUT 30
+
+/**
+ * How long to wait for the freshly started example to answer, in
+ * milliseconds.
+ */
+#define STARTUP_TIMEOUT_MS 5000
+
+/**
+ * How often to retry with a different port if the one we picked turned
+ * out to be taken after all.
+ */
+#define PORT_ATTEMPTS 5
+
+/**
+ * Largest response body we are prepared to keep.
+ */
+#define MAX_BODY 4096
+
+
+/**
+ * What one HTTP request collected.
+ */
+struct Fetch
+{
+ /**
+ * The body, as far as it was received.
+ */
+ char body[MAX_BODY];
+
+ /**
+ * Number of valid bytes in @e body.
+ */
+ size_t len;
+
+ /**
+ * Result of the transfer.
+ */
+ CURLcode res;
+};
+
+
+/**
+ * The example's process ID, or -1 once it has been reaped.
+ */
+static pid_t server_pid = -1;
+
+/**
+ * The port the example was told to bind.
+ */
+static unsigned int server_port;
+
+/**
+ * Number of checks that failed.
+ */
+static unsigned int failures;
+
+
+/**
+ * Report the outcome of one check.
+ *
+ * @param ok non-zero if the check passed
+ * @param what what was being checked
+ */
+static void
+check (int ok,
+ const char *what)
+{
+ if (! ok)
+ failures++;
+ fprintf (stderr,
+ "%s: %s\n",
+ ok ? "PASS" : "FAIL",
+ what);
+}
+
+
+/**
+ * @return the current value of the monotonic clock, in milliseconds
+ */
+static long
+now_ms (void)
+{
+ struct timespec ts;
+
+ if (0 != clock_gettime (CLOCK_MONOTONIC,
+ &ts))
+ return 0;
+ return (long) ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+}
+
+
+/**
+ * Read the CPU time consumed by process @a pid so far. Only
+ * implemented for Linux; everywhere else the test simply skips the
+ * check that uses it.
+ *
+ * @param pid the process to look at
+ * @param[out] ms where to store the sum of user and system time in
+ * milliseconds
+ * @return non-zero on success
+ */
+static int
+cpu_time_ms (pid_t pid,
+ long *ms)
+{
+#ifdef __linux__
+ char path[64];
+ char buf[1024];
+ char *p;
+ FILE *f;
+ size_t got;
+ unsigned long utime;
+ unsigned long stime;
+ long ticks;
+
+ snprintf (path,
+ sizeof (path),
+ "/proc/%ld/stat",
+ (long) pid);
+ f = fopen (path,
+ "r");
+ if (NULL == f)
+ return 0;
+ got = fread (buf,
+ 1,
+ sizeof (buf) - 1,
+ f);
+ fclose (f);
+ if (0 == got)
+ return 0;
+ buf[got] = '\0';
+ /* The second field is the executable name and may contain spaces and
+ parentheses, so start parsing behind its closing one. */
+ p = strrchr (buf,
+ ')');
+ if (NULL == p)
+ return 0;
+ if (2 != sscanf (p + 1,
+ " %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu",
+ &utime,
+ &stime))
+ return 0;
+ ticks = sysconf (_SC_CLK_TCK);
+ if (0 >= ticks)
+ return 0;
+ *ms = (long) ((utime + stime) * 1000 / (unsigned long) ticks);
+ return 1;
+#else
+ (void) pid;
+ (void) ms;
+ return 0;
+#endif
+}
+
+
+static size_t
+write_cb (void *ptr,
+ size_t size,
+ size_t nmemb,
+ void *cls)
+{
+ struct Fetch *f = cls;
+ size_t total = size * nmemb;
+
+ if (total > sizeof (f->body) - f->len - 1)
+ total = sizeof (f->body) - f->len - 1;
+ memcpy (&f->body[f->len],
+ ptr,
+ total);
+ f->len += total;
+ f->body[f->len] = '\0';
+ return size * nmemb;
+}
+
+
+/**
+ * Create an easy handle that will GET @a path from the example.
+ *
+ * @param path the path to request
+ * @param[out] f where to store what is received; cleared here
+ * @return the handle, or NULL on error
+ */
+static CURL *
+make_handle (const char *path,
+ struct Fetch *f)
+{
+ CURL *curl;
+ char url[128];
+
+ memset (f,
+ 0,
+ sizeof (struct Fetch));
+ f->res = CURLE_FAILED_INIT;
+ snprintf (url,
+ sizeof (url),
+ "http://127.0.0.1:%u%s",
+ server_port,
+ path);
+ curl = curl_easy_init ();
+ if (NULL == curl)
+ return NULL;
+ curl_easy_setopt (curl, CURLOPT_URL, url);
+ curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, &write_cb);
+ curl_easy_setopt (curl, CURLOPT_WRITEDATA, f);
+ curl_easy_setopt (curl, CURLOPT_TIMEOUT, (long) REQUEST_TIMEOUT);
+ curl_easy_setopt (curl, CURLOPT_NOSIGNAL, 1L);
+ return curl;
+}
+
+
+/**
+ * Perform one GET against the example.
+ *
+ * @param path the path to request
+ * @param[out] f where to store what was received
+ * @return milliseconds the request took
+ */
+static long
+fetch (const char *path,
+ struct Fetch *f)
+{
+ CURL *curl;
+ long start;
+
+ curl = make_handle (path,
+ f);
+ if (NULL == curl)
+ return 0;
+ start = now_ms ();
+ f->res = curl_easy_perform (curl);
+ curl_easy_cleanup (curl);
+ return now_ms () - start;
+}
+
+
+/**
+ * Perform two GETs against the example at the same time.
+ *
+ * @param path1 the path for the first request
+ * @param path2 the path for the second request
+ * @param[out] f1 where to store what the first request received
+ * @param[out] f2 where to store what the second request received
+ * @return milliseconds until both were done
+ */
+static long
+fetch_both (const char *path1,
+ const char *path2,
+ struct Fetch *f1,
+ struct Fetch *f2)
+{
+ CURLM *multi;
+ CURL *c1;
+ CURL *c2;
+ CURLMsg *msg;
+ int running;
+ int left;
+ long start;
+
+ c1 = make_handle (path1,
+ f1);
+ c2 = make_handle (path2,
+ f2);
+ multi = curl_multi_init ();
+ if ( (NULL == c1) ||
+ (NULL == c2) ||
+ (NULL == multi) )
+ {
+ if (NULL != c1)
+ curl_easy_cleanup (c1);
+ if (NULL != c2)
+ curl_easy_cleanup (c2);
+ if (NULL != multi)
+ curl_multi_cleanup (multi);
+ return 0;
+ }
+ curl_multi_add_handle (multi, c1);
+ curl_multi_add_handle (multi, c2);
+ start = now_ms ();
+ running = 1;
+ while (0 != running)
+ {
+ if (CURLM_OK != curl_multi_perform (multi,
+ &running))
+ break;
+ if (0 == running)
+ break;
+ if (CURLM_OK != curl_multi_wait (multi,
+ NULL,
+ 0,
+ 100,
+ NULL))
+ break;
+ if (now_ms () - start > REQUEST_TIMEOUT * 1000)
+ break;
+ }
+ while (NULL != (msg = curl_multi_info_read (multi,
+ &left)))
+ {
+ if (CURLMSG_DONE != msg->msg)
+ continue;
+ if (msg->easy_handle == c1)
+ f1->res = msg->data.result;
+ else if (msg->easy_handle == c2)
+ f2->res = msg->data.result;
+ }
+ curl_multi_remove_handle (multi, c1);
+ curl_multi_remove_handle (multi, c2);
+ curl_easy_cleanup (c1);
+ curl_easy_cleanup (c2);
+ curl_multi_cleanup (multi);
+ return now_ms () - start;
+}
+
+
+/**
+ * Ask the operating system for a port that is currently free. The
+ * example insists on being given a port number, so the test has to pick
+ * one; if the guess goes stale between here and the child's bind() the
+ * child exits at once and the caller simply tries again.
+ *
+ * @param[out] port where to store the port number
+ * @return non-zero on success
+ */
+static int
+pick_free_port (unsigned int *port)
+{
+ struct sockaddr_in addr;
+ socklen_t addrlen = sizeof (addr);
+ int sock;
+
+ sock = socket (AF_INET,
+ SOCK_STREAM,
+ 0);
+ if (-1 == sock)
+ return 0;
+ memset (&addr,
+ 0,
+ sizeof (addr));
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = htonl (INADDR_ANY);
+ addr.sin_port = 0;
+ if ( (0 != bind (sock,
+ (struct sockaddr *) &addr,
+ sizeof (addr))) ||
+ (0 != getsockname (sock,
+ (struct sockaddr *) &addr,
+ &addrlen)) )
+ {
+ close (sock);
+ return 0;
+ }
+ close (sock);
+ *port = ntohs (addr.sin_port);
+ return (0 != *port);
+}
+
+
+/**
+ * Start the example and wait until it answers.
+ *
+ * @return 1 on success, 0 if it could not be started, -1 if the binary
+ * could not be executed at all (in which case the test skips)
+ */
+static int
+start_server (void)
+{
+ unsigned int attempt;
+
+ for (attempt = 0; attempt < PORT_ATTEMPTS; attempt++)
+ {
+ char port_arg[16];
+ long deadline;
+
+ if (! pick_free_port (&server_port))
+ return 0;
+ snprintf (port_arg,
+ sizeof (port_arg),
+ "%u",
+ server_port);
+ server_pid = fork ();
+ if (-1 == server_pid)
+ return 0;
+ if (0 == server_pid)
+ {
+ execl ("./suspend_resume_epoll",
+ "suspend_resume_epoll",
+ port_arg,
+ (char *) NULL);
+ fprintf (stderr,
+ "Failed to exec ./suspend_resume_epoll: %s\n",
+ strerror (errno));
+ _exit (77);
+ }
+ deadline = now_ms () + STARTUP_TIMEOUT_MS;
+ while (now_ms () < deadline)
+ {
+ struct Fetch f;
+ int status;
+
+ if (server_pid == waitpid (server_pid,
+ &status,
+ WNOHANG))
+ {
+ /* The example gave up, most likely because the port was taken
+ after all. Unless it could not even be started, try again. */
+ if (WIFEXITED (status) && (77 == WEXITSTATUS (status)))
+ {
+ server_pid = -1;
+ return -1;
+ }
+ server_pid = -1;
+ break;
+ }
+ (void) fetch ("/startup",
+ &f);
+ if (CURLE_OK == f.res)
+ return 1;
+ usleep (100 * 1000);
+ }
+ if (-1 != server_pid)
+ {
+ kill (server_pid,
+ SIGKILL);
+ waitpid (server_pid,
+ NULL,
+ 0);
+ server_pid = -1;
+ return 0; /* it is running but not answering; retrying will not help */
+ }
+ }
+ return 0;
+}
+
+
+/**
+ * The example echoes the requested URL back, but only after the timer
+ * it armed has fired.
+ */
+static void
+test_echo (void)
+{
+ struct Fetch f;
+ long ms;
+
+ ms = fetch ("/hello/world",
+ &f);
+ check (CURLE_OK == f.res,
+ "request completed");
+ check (0 == strcmp (f.body,
+ "/hello/world"),
+ "example echoed the requested URL");
+ /* Only a lower bound is checked: a loaded machine may take longer,
+ but nothing can make the one-second timer fire early. */
+ fprintf (stderr,
+ "request was parked for %ld ms\n",
+ ms);
+ check (ms >= PARK_MS / 2,
+ "answer waited for the timer to fire");
+}
+
+
+/**
+ * While the connection is parked the process must be asleep in
+ * epoll_wait(). This is the check that tells a suspended connection
+ * apart from an access handler that is polled in a loop --- both return
+ * the same bytes after the same delay.
+ */
+static void
+test_idle_while_suspended (void)
+{
+ struct Fetch f;
+ long cpu_before;
+ long cpu_after;
+ long ms;
+
+ if (! cpu_time_ms (server_pid,
+ &cpu_before))
+ {
+ fprintf (stderr,
+ "SKIP: no way to read the server's CPU time on this "
+ "platform\n");
+ return;
+ }
+ ms = fetch ("/idle",
+ &f);
+ check (CURLE_OK == f.res,
+ "request completed");
+ if (! cpu_time_ms (server_pid,
+ &cpu_after))
+ {
+ fprintf (stderr,
+ "SKIP: could not read the server's CPU time\n");
+ return;
+ }
+ fprintf (stderr,
+ "server used %ld ms of CPU during %ld ms of waiting\n",
+ cpu_after - cpu_before,
+ ms);
+ /* "<=" rather than "<" so that a degenerate run in which nothing was
+ waited for at all is left for test_echo() to report, instead of
+ being blamed on CPU usage here. */
+ check ((cpu_after - cpu_before) * 4 <= ms,
+ "server stayed idle while the connection was suspended");
+}
+
+
+/**
+ * Suspending must not serialise requests: two connections parked at the
+ * same time have to come back after one timer period, not two. With a
+ * single-threaded daemon this only works because the connection is
+ * taken out of the event loop rather than blocking it.
+ */
+static void
+test_two_at_once (void)
+{
+ struct Fetch f1;
+ struct Fetch f2;
+ long ms;
+
+ ms = fetch_both ("/first",
+ "/second",
+ &f1,
+ &f2);
+ check ( (CURLE_OK == f1.res) &&
+ (CURLE_OK == f2.res),
+ "both concurrent requests completed");
+ check ( (0 == strcmp (f1.body,
+ "/first")) &&
+ (0 == strcmp (f2.body,
+ "/second")),
+ "each concurrent request got its own answer");
+ fprintf (stderr,
+ "two concurrent requests took %ld ms\n",
+ ms);
+ check (ms < 2 * PARK_MS - PARK_MS / 4,
+ "the two requests were parked at the same time");
+}
+
+
+/**
+ * A client that hangs up while its connection is parked must not take
+ * the server down, and must not leak the timer either.
+ */
+static void
+test_client_hangs_up (void)
+{
+ struct sockaddr_in addr;
+ struct Fetch f;
+ static const char *req = "GET /gone HTTP/1.1\r\nHost: localhost\r\n\r\n";
+ int sock;
+
+ sock = socket (AF_INET,
+ SOCK_STREAM,
+ 0);
+ if (-1 == sock)
+ {
+ check (0,
+ "could not create a socket");
+ return;
+ }
+ memset (&addr,
+ 0,
+ sizeof (addr));
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = htonl (INADDR_LOOPBACK);
+ addr.sin_port = htons ((uint16_t) server_port);
+ if (0 != connect (sock,
+ (struct sockaddr *) &addr,
+ sizeof (addr)))
+ {
+ close (sock);
+ check (0,
+ "could not connect to the example");
+ return;
+ }
+ if (-1 == send (sock,
+ req,
+ strlen (req),
+ 0))
+ {
+ close (sock);
+ check (0,
+ "could not send a request");
+ return;
+ }
+ /* Hang up well before the timer fires, so that the connection is
+ still suspended when the FIN arrives. */
+ usleep ((useconds_t) (PARK_MS / 4) * 1000);
+ close (sock);
+
+ (void) fetch ("/still/here",
+ &f);
+ check ( (CURLE_OK == f.res) &&
+ (0 == strcmp (f.body,
+ "/still/here")),
+ "server survived a client that hung up while suspended");
+}
+
+
+int
+main (void)
+{
+ int started;
+
+ if (0 != curl_global_init (CURL_GLOBAL_ALL))
+ return 77;
+ started = start_server ();
+ if (1 != started)
+ {
+ fprintf (stderr,
+ "Could not start ./suspend_resume_epoll\n");
+ curl_global_cleanup ();
+ return (-1 == started) ? 77 : 1;
+ }
+ fprintf (stderr,
+ "example listening on port %u\n",
+ server_port);
+
+ test_echo ();
+ test_idle_while_suspended ();
+ test_two_at_once ();
+ test_client_hangs_up ();
+
+ /* The example runs an endless loop by design, so there is nothing to
+ ask it to do but die. */
+ kill (server_pid,
+ SIGKILL);
+ waitpid (server_pid,
+ NULL,
+ 0);
+ server_pid = -1;
+ curl_global_cleanup ();
+ if (0 != failures)
+ fprintf (stderr,
+ "%u check(s) failed\n",
+ failures);
+ return (0 == failures) ? 0 : 1;
+}