diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index c35f84611ec..35b3f4a96a2 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -310,6 +310,19 @@ jobs: PYHTTPD_TARGETS=modules/arch PYTEST_ARGS=--only=pyhttpd # ------------------------------------------------------------------------- + # The pytest suites again under prefork, whose idle and shutdown + # handling is a separate implementation from event's. Worker is + # close enough to event not to be worth a third job. + - name: Python pytest test suites, prefork + config: --enable-mods-shared=reallyall --enable-mpms-shared=all --with-mpm=prefork --enable-systemd=shared + pkgs: nghttp2-client libsystemd-dev + env: | + MPM=prefork + NO_TEST_FRAMEWORK=1 + TEST_PYTEST=1 + PYHTTPD_TARGETS=modules/arch modules/core + PYTEST_ARGS=--only=pyhttpd + # ------------------------------------------------------------------------- ### TODO if: *condition_not_24x ### TODO: Fails because :i386 packages are not being found. # - name: i386 Shared MPMs, most modules, maintainer-mode w/-Werror diff --git a/changes-entries/idle-termination-timeout.txt b/changes-entries/idle-termination-timeout.txt new file mode 100644 index 00000000000..5f451e01451 --- /dev/null +++ b/changes-entries/idle-termination-timeout.txt @@ -0,0 +1,5 @@ + *) mpm_event, mpm_prefork, mpm_worker: Add IdleTerminationTimeout + directive, which terminates the server once it has had no open + connections and served no requests for the given number of seconds. + Github #529. + [Alex/AT <85214814+AlexAT users.noreply.github.com>] diff --git a/docs/log-message-tags/intended-duplicates b/docs/log-message-tags/intended-duplicates index 6886322e87f..f991ffab44e 100644 --- a/docs/log-message-tags/intended-duplicates +++ b/docs/log-message-tags/intended-duplicates @@ -8,3 +8,4 @@ 00812 # mod_socache_dbm.c 00814 # mod_socache_dbm.c 00815 # mod_socache_dbm.c +10620 # "idle timeout reached" in the prefork, worker and event MPMs diff --git a/docs/manual/mod/mpm_common.xml b/docs/manual/mod/mpm_common.xml index 2493a3bda78..7524a3ad7c3 100644 --- a/docs/manual/mod/mpm_common.xml +++ b/docs/manual/mod/mpm_common.xml @@ -134,6 +134,47 @@ will exit. + +IdleTerminationTimeout +Specify a timeout after which an idle server will terminate +itself. +IdleTerminationTimeout seconds +Disabled, the server never terminates itself +server config +eventworker +prefork +Available in Apache HTTP Server 2.5.1 and later + + +

The IdleTerminationTimeout directive + specifies how many seconds the server should remain idle before + shutting itself down. The server is only considered idle while it + has no open connections and has served no requests; idle workers + alone are not enough, since a connection can be open without + occupying a worker, and a request can start and finish between two + checks. Setting this value to zero means that the server terminates + as soon as it is found to be idle.

+ +

The argument must be a non-negative number of seconds; any other + value is a fatal configuration error. The directive may only be used + in the global server configuration. If it is not used at all, the + server never terminates itself.

+ +

The shutdown is not graceful: nothing is in flight when the + timeout expires, so there is nothing left to drain and the server + exits immediately. Instead of the usual caught SIGTERM, + shutting down message, a notice is logged reading idle + timeout reached, shutting down.

+ +

This is primarily useful with systemd socket activation, where + the listening sockets belong to the service manager rather than to + the server: an idle server can exit and leave the sockets with + systemd, which starts it again when the next connection arrives.

+
+mod_systemd +GracefulShutdownTimeout +
+ PidFile File where the server records the process ID diff --git a/server/mpm/event/event.c b/server/mpm/event/event.c index 385532d03b4..b9d46ced542 100644 --- a/server/mpm/event/event.c +++ b/server/mpm/event/event.c @@ -199,6 +199,10 @@ static fd_queue_info_t *worker_queue_info; static apr_thread_mutex_t *timeout_mutex; +static int idle_termination_timeout = -1; /* never terminate by default */ +static apr_time_t idle_termination_since; /* when the server went quiet */ +static unsigned long idle_termination_accesses; /* requests counted then */ + module AP_MODULE_DECLARE_DATA mpm_event_module; /* forward declare */ @@ -444,6 +448,8 @@ typedef struct event_retained_data { */ int *idle_spawn_rate; int hold_off_on_exponential_spawning; + + int idle_timeout; /* did we time out? */ } event_retained_data; static event_retained_data *retained; @@ -836,6 +842,16 @@ static void just_die(int sig) static int child_fatal; +/* The parent and mod_status see this count only through the scoreboard, + * and the listener can sleep for a whole keepalive timeout without + * passing through its queue maintenance, so publish it where it changes + * rather than there. */ +static void publish_connection_count(void) +{ + ap_scoreboard_image->parent[ap_child_slot].connections = + apr_atomic_read32(&connection_count); +} + static apr_status_t decrement_connection_count(void *cs_) { int is_last_connection; @@ -859,6 +875,7 @@ static apr_status_t decrement_connection_count(void *cs_) * now accept new connections. */ is_last_connection = !apr_atomic_dec32(&connection_count); + publish_connection_count(); if (listener_is_wakeable && ((is_last_connection && listener_may_exit) || should_enable_listensocks())) { @@ -1067,6 +1084,7 @@ static void process_socket(apr_thread_t *thd, apr_pool_t * p, apr_socket_t * soc return; } apr_atomic_inc32(&connection_count); + publish_connection_count(); apr_pool_cleanup_register(c->pool, cs, decrement_connection_count, apr_pool_cleanup_null); ap_set_module_config(c->conn_config, &mpm_event_module, cs); @@ -2392,6 +2410,17 @@ static void * APR_THREAD_FUNC listener_thread(apr_thread_t * thd, void *dummy) apr_thread_mutex_unlock(timeout_mutex); ps->keep_alive = 0; } + else { + /* No queue maintenance was due, but the counts above are what + * the parent and mod_status see, and a connection can sit in a + * queue for as long as its timeout without either. Publish + * them on every pass instead of only when a queue expires. */ + ps->wait_io = apr_atomic_read32(waitio_q->total); + ps->write_completion = apr_atomic_read32(write_completion_q->total); + ps->keep_alive = apr_atomic_read32(keepalive_q->total); + ps->lingering_close = apr_atomic_read32(&lingering_count); + ps->suspended = apr_atomic_read32(&suspended_count); + } /* If there are some lingering closes to defer (to a worker), schedule * them now. We might wakeup a worker spuriously if another one empties @@ -3237,11 +3266,45 @@ static void startup_children(int number_to_start) } } +/* Whether the server has nothing whatever to do. An idle worker count is + * not enough on its own: this MPM hands a connection back to the listener + * between requests, so open connections occupy no worker, and a request + * which starts and finishes between two calls here leaves every worker + * idle at both of them. */ +static int server_is_idle(int workers_busy) +{ + unsigned long accesses = 0; + apr_uint32_t connections = 0; + int i, j; + + for (i = 0; i < server_limit; i++) { + process_score *ps = ap_get_scoreboard_process(i); + + if (ps->pid == 0) { + continue; + } + connections += ps->connections; + for (j = 0; j < thread_limit; j++) { + accesses += ap_scoreboard_image->servers[i][j].access_count; + } + } + + if (workers_busy || connections + || accesses != idle_termination_accesses) { + idle_termination_accesses = accesses; + idle_termination_since = 0; + return 0; + } + + return 1; +} + static void perform_idle_server_maintenance(int child_bucket, int *max_daemon_used) { int num_buckets = retained->mpm->num_buckets; int idle_thread_count = 0; + int total_thread_count = 0; process_score *ps; int free_length = 0; int free_slots[MAX_SPAWN_RATE]; @@ -3292,6 +3355,7 @@ static void perform_idle_server_maintenance(int child_bucket, if (status >= SERVER_READY && status < SERVER_GRACEFUL) { ++child_threads_active; } + ++total_thread_count; } active_thread_count += child_threads_active; if (child_threads_active == threads_per_child) { @@ -3338,6 +3402,20 @@ static void perform_idle_server_maintenance(int child_bucket, && retained->total_daemons <= retained->max_daemon_used && retained->max_daemon_used <= server_limit); + if (idle_termination_timeout >= 0 + && server_is_idle(idle_thread_count != total_thread_count)) { + if (!idle_termination_since) { + idle_termination_since = apr_time_now(); + } + if (apr_time_now() - idle_termination_since + >= apr_time_from_sec(idle_termination_timeout)) { + /* terminate immediately: nothing is going on to wait for */ + retained->mpm->shutdown_pending = 1; + retained->mpm->is_ungraceful = 1; + retained->idle_timeout = 1; + } + } + if (idle_thread_count > max_spare_threads / num_buckets) { /* * Child processes that we ask to shut down won't die immediately @@ -3783,8 +3861,16 @@ static int event_run(apr_pool_t * _pconf, apr_pool_t * plog, server_rec * s) if (!child_fatal) { /* cleanup pid file on normal shutdown */ ap_remove_pid(pconf, ap_pid_fname); - ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, - ap_server_conf, APLOGNO(00491) "caught SIGTERM, shutting down"); + + /* log message depends on if we are terminating by signal or by idle timeout */ + if (!retained->idle_timeout) { + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, APLOGNO(00491) + "caught SIGTERM, shutting down"); + } + else { + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, APLOGNO(10620) + "idle timeout reached, shutting down"); + } } return DONE; @@ -4026,6 +4112,9 @@ static int event_pre_config(apr_pool_t * pconf, apr_pool_t * plog, ap_listen_pre_config(); ap_daemons_to_start = DEFAULT_START_DAEMON; + idle_termination_timeout = -1; + idle_termination_since = 0; + idle_termination_accesses = 0; min_spare_threads = DEFAULT_MIN_FREE_DAEMON * DEFAULT_THREADS_PER_CHILD; max_spare_threads = DEFAULT_MAX_FREE_DAEMON * DEFAULT_THREADS_PER_CHILD; server_limit = DEFAULT_SERVER_LIMIT; @@ -4481,6 +4570,25 @@ static const char *set_worker_factor(cmd_parms * cmd, void *dummy, return NULL; } +static const char *set_idle_termination_timeout(cmd_parms *cmd, void *dummy, const char *arg) +{ + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + char *end; + long secs; + + if (err != NULL) { + return err; + } + + secs = strtol(arg, &end, 10); + if (*arg == '\0' || *end != '\0' || secs < 0 || secs > APR_INT32_MAX) { + return "IdleTerminationTimeout must be a non-negative number of " + "seconds"; + } + + idle_termination_timeout = (int)secs; + return NULL; +} static const command_rec event_cmds[] = { LISTEN_COMMANDS, @@ -4504,6 +4612,8 @@ static const command_rec event_cmds[] = { AP_INIT_TAKE1("AsyncRequestWorkerFactor", set_worker_factor, NULL, RSRC_CONF, "How many additional connects will be accepted per idle " "worker thread"), + AP_INIT_TAKE1("IdleTerminationTimeout", set_idle_termination_timeout, NULL, RSRC_CONF, + "Number of seconds to terminate in when the server is idle"), AP_GRACEFUL_SHUTDOWN_TIMEOUT_COMMAND, {NULL} }; diff --git a/server/mpm/prefork/prefork.c b/server/mpm/prefork/prefork.c index 37dc2dda4dd..93c7d10ba45 100644 --- a/server/mpm/prefork/prefork.c +++ b/server/mpm/prefork/prefork.c @@ -95,6 +95,10 @@ static int ap_daemons_max_free=0; static int ap_daemons_limit=0; /* MaxRequestWorkers */ static int server_limit = 0; +static int idle_termination_timeout = -1; /* never terminate by default */ +static apr_time_t idle_termination_since; /* when the server went quiet */ +static unsigned long idle_termination_accesses; /* requests counted then */ + typedef struct prefork_child_bucket { ap_pod_t *pod; ap_listen_rec *listeners; @@ -131,6 +135,8 @@ typedef struct prefork_retained_data { #define MAX_SPAWN_RATE (32) #endif int hold_off_on_exponential_spawning; + + int idle_timeout; /* did we time out? */ } prefork_retained_data; static prefork_retained_data *retained; @@ -819,6 +825,38 @@ static void startup_children(int number_to_start) } } +/* Whether the server has nothing whatever to do. Idle workers are not + * enough to go on: a request which starts and finishes between two calls + * here leaves every worker idle at both of them, and an async MPM holds + * open connections without occupying a worker at all. */ +static int server_is_idle(int workers_busy) +{ + unsigned long accesses = 0; + apr_uint32_t connections = 0; + int i, j; + + for (i = 0; i < server_limit; i++) { + process_score *ps = ap_get_scoreboard_process(i); + + if (ps->pid == 0) { + continue; + } + connections += ps->connections; + for (j = 0; j < 1; j++) { + accesses += ap_scoreboard_image->servers[i][j].access_count; + } + } + + if (workers_busy || connections + || accesses != idle_termination_accesses) { + idle_termination_accesses = accesses; + idle_termination_since = 0; + return 0; + } + + return 1; +} + static void perform_idle_server_maintenance(apr_pool_t *p) { int i; @@ -866,6 +904,21 @@ static void perform_idle_server_maintenance(apr_pool_t *p) } } retained->max_daemons_limit = last_non_dead + 1; + + if (idle_termination_timeout >= 0 + && server_is_idle(idle_count != total_non_dead)) { + if (!idle_termination_since) { + idle_termination_since = apr_time_now(); + } + if (apr_time_now() - idle_termination_since + >= apr_time_from_sec(idle_termination_timeout)) { + /* terminate immediately: nothing is going on to wait for */ + retained->mpm->shutdown_pending = 1; + retained->mpm->is_ungraceful = 1; + retained->idle_timeout = 1; + } + } + if (idle_count > ap_daemons_max_free) { static int bucket_kill_child_record = -1; /* kill off one child... we use the pod because that'll cause it to @@ -1206,8 +1259,16 @@ static int prefork_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s) /* cleanup pid file on normal shutdown */ ap_remove_pid(pconf, ap_pid_fname); - ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, APLOGNO(00169) - "caught SIGTERM, shutting down"); + + /* log message depends on if we are terminating by signal or by idle timeout */ + if (!retained->idle_timeout) { + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, APLOGNO(00169) + "caught SIGTERM, shutting down"); + } + else { + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, APLOGNO(10620) + "idle timeout reached, shutting down"); + } return DONE; } @@ -1346,6 +1407,7 @@ static int prefork_pre_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp retained->mpm = ap_unixd_mpm_get_retained_data(); retained->mpm->baton = retained; retained->idle_spawn_rate = 1; + retained->idle_timeout = 0; } else if (retained->mpm->baton != retained) { /* If the MPM changes on restart, be ungraceful */ @@ -1374,6 +1436,9 @@ static int prefork_pre_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp ap_listen_pre_config(); ap_daemons_to_start = DEFAULT_START_DAEMON; + idle_termination_timeout = -1; + idle_termination_since = 0; + idle_termination_accesses = 0; ap_daemons_min_free = DEFAULT_MIN_FREE_DAEMON; ap_daemons_max_free = DEFAULT_MAX_FREE_DAEMON; server_limit = DEFAULT_SERVER_LIMIT; @@ -1575,6 +1640,26 @@ static const char *set_server_limit (cmd_parms *cmd, void *dummy, const char *ar return NULL; } +static const char *set_idle_termination_timeout(cmd_parms *cmd, void *dummy, const char *arg) +{ + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + char *end; + long secs; + + if (err != NULL) { + return err; + } + + secs = strtol(arg, &end, 10); + if (*arg == '\0' || *end != '\0' || secs < 0 || secs > APR_INT32_MAX) { + return "IdleTerminationTimeout must be a non-negative number of " + "seconds"; + } + + idle_termination_timeout = (int)secs; + return NULL; +} + static const command_rec prefork_cmds[] = { LISTEN_COMMANDS, AP_INIT_TAKE1("StartServers", set_daemons_to_start, NULL, RSRC_CONF, @@ -1589,6 +1674,8 @@ AP_INIT_TAKE1("MaxRequestWorkers", set_max_clients, NULL, RSRC_CONF, "Maximum number of children alive at the same time"), AP_INIT_TAKE1("ServerLimit", set_server_limit, NULL, RSRC_CONF, "Maximum value of MaxRequestWorkers for this run of Apache"), +AP_INIT_TAKE1("IdleTerminationTimeout", set_idle_termination_timeout, NULL, RSRC_CONF, + "Number of seconds to terminate in when the server is idle"), AP_GRACEFUL_SHUTDOWN_TIMEOUT_COMMAND, { NULL } }; diff --git a/server/mpm/worker/worker.c b/server/mpm/worker/worker.c index 42b81a8ed1b..135ac0299db 100644 --- a/server/mpm/worker/worker.c +++ b/server/mpm/worker/worker.c @@ -138,6 +138,10 @@ static fd_queue_t *worker_queue; static fd_queue_info_t *worker_queue_info; static apr_pollset_t *worker_pollset; +static int idle_termination_timeout = -1; /* never terminate by default */ +static apr_time_t idle_termination_since; /* when the server went quiet */ +static unsigned long idle_termination_accesses; /* requests counted then */ + typedef struct worker_child_bucket { ap_pod_t *pod; ap_listen_rec *listeners; @@ -175,6 +179,8 @@ typedef struct worker_retained_data { */ int *idle_spawn_rate; int hold_off_on_exponential_spawning; + + int idle_timeout; /* did we time out? */ } worker_retained_data; static worker_retained_data *retained; @@ -1424,26 +1430,52 @@ static void startup_children(int number_to_start) } } +/* Whether the server has nothing whatever to do. Idle workers are not + * enough to go on: a request which starts and finishes between two calls + * here leaves every worker idle at both of them, and an async MPM holds + * open connections without occupying a worker at all. */ +static int server_is_idle(int workers_busy) +{ + unsigned long accesses = 0; + apr_uint32_t connections = 0; + int i, j; + + for (i = 0; i < server_limit; i++) { + process_score *ps = ap_get_scoreboard_process(i); + + if (ps->pid == 0) { + continue; + } + connections += ps->connections; + for (j = 0; j < thread_limit; j++) { + accesses += ap_scoreboard_image->servers[i][j].access_count; + } + } + + if (workers_busy || connections + || accesses != idle_termination_accesses) { + idle_termination_accesses = accesses; + idle_termination_since = 0; + return 0; + } + + return 1; +} + static void perform_idle_server_maintenance(int child_bucket) { int num_buckets = retained->mpm->num_buckets; - int idle_thread_count; + int idle_thread_count = 0; + int total_thread_count = 0; process_score *ps; - int free_length; + int free_length = 0; int totally_free_length = 0; int free_slots[MAX_SPAWN_RATE]; - int last_non_dead; - int total_non_dead; + int last_non_dead = -1; + int total_non_dead = 0; int active_thread_count = 0; int i, j; - /* initialize the free_list */ - free_length = 0; - - idle_thread_count = 0; - last_non_dead = -1; - total_non_dead = 0; - for (i = 0; i < ap_daemons_limit; ++i) { /* Initialization to satisfy the compiler. It doesn't know * that threads_per_child is always > 0 */ @@ -1491,6 +1523,7 @@ static void perform_idle_server_maintenance(int child_bucket) if (status >= SERVER_READY && status < SERVER_GRACEFUL) { ++child_threads_active; } + ++total_thread_count; } } active_thread_count += child_threads_active; @@ -1558,6 +1591,20 @@ static void perform_idle_server_maintenance(int child_bucket) } } + if (idle_termination_timeout >= 0 + && server_is_idle(idle_thread_count != total_thread_count)) { + if (!idle_termination_since) { + idle_termination_since = apr_time_now(); + } + if (apr_time_now() - idle_termination_since + >= apr_time_from_sec(idle_termination_timeout)) { + /* terminate immediately: nothing is going on to wait for */ + retained->mpm->shutdown_pending = 1; + retained->mpm->is_ungraceful = 1; + retained->idle_timeout = 1; + } + } + if (idle_thread_count > max_spare_threads / num_buckets) { /* Kill off one child */ ap_mpm_podx_signal(retained->buckets[child_bucket].pod, @@ -1983,8 +2030,16 @@ static int worker_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s) if (!child_fatal) { /* cleanup pid file on normal shutdown */ ap_remove_pid(pconf, ap_pid_fname); - ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, - ap_server_conf, APLOGNO(00295) "caught SIGTERM, shutting down"); + + /* log message depends on if we are terminating by signal or by idle timeout */ + if (!retained->idle_timeout) { + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, APLOGNO(00295) + "caught SIGTERM, shutting down"); + } + else { + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, APLOGNO(10620) + "idle timeout reached, shutting down"); + } } return DONE; } @@ -2150,6 +2205,9 @@ static int worker_pre_config(apr_pool_t *pconf, apr_pool_t *plog, ap_listen_pre_config(); ap_daemons_to_start = DEFAULT_START_DAEMON; + idle_termination_timeout = -1; + idle_termination_since = 0; + idle_termination_accesses = 0; min_spare_threads = DEFAULT_MIN_FREE_DAEMON * DEFAULT_THREADS_PER_CHILD; max_spare_threads = DEFAULT_MAX_FREE_DAEMON * DEFAULT_THREADS_PER_CHILD; server_limit = DEFAULT_SERVER_LIMIT; @@ -2484,6 +2542,26 @@ static const char *set_thread_limit (cmd_parms *cmd, void *dummy, const char *ar return NULL; } +static const char *set_idle_termination_timeout(cmd_parms *cmd, void *dummy, const char *arg) +{ + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + char *end; + long secs; + + if (err != NULL) { + return err; + } + + secs = strtol(arg, &end, 10); + if (*arg == '\0' || *end != '\0' || secs < 0 || secs > APR_INT32_MAX) { + return "IdleTerminationTimeout must be a non-negative number of " + "seconds"; + } + + idle_termination_timeout = (int)secs; + return NULL; +} + static const command_rec worker_cmds[] = { LISTEN_COMMANDS, AP_INIT_TAKE1("StartServers", set_daemons_to_start, NULL, RSRC_CONF, @@ -2502,6 +2580,8 @@ AP_INIT_TAKE1("ServerLimit", set_server_limit, NULL, RSRC_CONF, "Maximum number of child processes for this run of Apache"), AP_INIT_TAKE1("ThreadLimit", set_thread_limit, NULL, RSRC_CONF, "Maximum number of worker threads per child process for this run of Apache - Upper limit for ThreadsPerChild"), +AP_INIT_TAKE1("IdleTerminationTimeout", set_idle_termination_timeout, NULL, RSRC_CONF, + "Number of seconds to terminate in when the server is idle"), AP_GRACEFUL_SHUTDOWN_TIMEOUT_COMMAND, { NULL } }; diff --git a/test/modules/arch/conftest.py b/test/modules/arch/conftest.py new file mode 100644 index 00000000000..2d866b50493 --- /dev/null +++ b/test/modules/arch/conftest.py @@ -0,0 +1,5 @@ +import sys + +# Subdirectories here are named for the platform whose interfaces they +# test, and are not collected anywhere else. +collect_ignore = [] if sys.platform.startswith('linux') else ['linux'] diff --git a/test/modules/arch/linux/README b/test/modules/arch/linux/README index 4bdd7af91d1..c79b4c7c42a 100644 --- a/test/modules/arch/linux/README +++ b/test/modules/arch/linux/README @@ -88,10 +88,11 @@ libsystemd httpd itself is linked against. skipped where systemd is older than 253, which is where Type=notify-reload arrived. -The whole package is skipped unless mod_systemd was built, which needs -configure --enable-systemd. A static module is enough for everything -except the test which has to leave mod_systemd out of the configuration; -that one needs --enable-systemd=shared. +These tests are collected on Linux only, which the conftest.py in the +directory above arranges. The package is then skipped unless mod_systemd +was built, which needs configure --enable-systemd. A static module is +enough for everything except the test which has to leave mod_systemd out +of the configuration; that one needs --enable-systemd=shared. Running the systemd suites where there is no user session --------------------------------------------------------- diff --git a/test/modules/core/test_007_idle_termination.py b/test/modules/core/test_007_idle_termination.py new file mode 100644 index 00000000000..b28bdb3ca18 --- /dev/null +++ b/test/modules/core/test_007_idle_termination.py @@ -0,0 +1,133 @@ +import os +import re +import socket +import time + +import pytest + +from pyhttpd.conf import HttpdConf + + +# The directive is implemented by these MPMs only; another one rejects it +# as an unknown command rather than ignoring it. +MPMS_WITH_IDLE_TERMINATION = ['mpm_prefork', 'mpm_worker', 'mpm_event'] + + +class TestIdleTermination: + """IdleTerminationTimeout: the server terminates itself once no worker + has anything to do, so that a socket-activated instance gives its + listening socket back to the service manager instead of sitting idle. + + Liveness here is checked by watching the process, never by making a + request: a request is exactly what resets the timer under test. + """ + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + if env.mpm_module not in MPMS_WITH_IDLE_TERMINATION: + pytest.skip(f"{env.mpm_module} has no IdleTerminationTimeout") + yield + conf = HttpdConf(env) + conf.add_vhost_test1() + conf.install() + assert env.apache_restart() == 0 + + def start(self, env, extra=''): + """Install a configuration, start the server, and return its pid.""" + conf = HttpdConf(env, extras={'base': extra}) + conf.add_vhost_test1() + conf.install() + assert env.apache_restart() == 0 + pid = env.read_pid_file() + assert pid, "no pid file after start" + return pid + + @staticmethod + def is_running(pid): + try: + os.kill(pid, 0) + except OSError: + return False + return True + + @classmethod + def wait_for_exit(cls, pid, timeout): + end = time.time() + timeout + while time.time() < end: + if not cls.is_running(pid): + return True + time.sleep(0.2) + return False + + def test_core_007_01_terminates_when_idle(self, env): + pid = self.start(env, 'IdleTerminationTimeout 2') + assert self.wait_for_exit(pid, 15), \ + "the server was still running well after its idle timeout" + assert env.httpd_error_log.scan_recent( + re.compile(r'.*idle timeout reached, shutting down.*'), timeout=5), \ + "no log message explaining the shutdown" + + def test_core_007_02_stays_up_by_default(self, env): + """Without the directive the server never terminates itself.""" + pid = self.start(env) + assert not self.wait_for_exit(pid, 6), \ + "the server terminated with no IdleTerminationTimeout configured" + + def test_core_007_03_requests_reset_the_timer(self, env): + """A server which is being used does not time out.""" + pid = self.start(env, 'IdleTerminationTimeout 3') + for _ in range(6): + r = env.curl_get(env.mkurl("http", "test1", "/")) + assert r.response['status'] == 200, "the server went away early" + time.sleep(1) + assert self.is_running(pid), \ + "the server terminated while it was still serving requests" + # Left alone, it goes away. + assert self.wait_for_exit(pid, 15) + + def test_core_007_04_zero_terminates_at_once(self, env): + """A zero timeout means the first idle moment is enough.""" + pid = self.start(env, 'IdleTerminationTimeout 0') + assert self.wait_for_exit(pid, 10) + + def test_core_007_05_open_connection_holds_it_up(self, env): + """A client which is connected but quiet keeps the server alive. + + KeepAliveTimeout has to outlast the wait below, or the server + closes the connection itself and is then genuinely idle. + """ + pid = self.start(env, 'IdleTerminationTimeout 2\nKeepAliveTimeout 30') + with socket.create_connection((env.http_addr, env.http_port), 5) as c: + c.sendall(b'GET / HTTP/1.1\r\nHost: test1.' + + env.http_tld.encode() + b'\r\n\r\n') + assert c.recv(64).startswith(b'HTTP/1.1 200') + assert not self.wait_for_exit(pid, 8), \ + "the server terminated with a connection still open" + + def test_core_007_06_rejects_a_bad_value(self, env): + conf = HttpdConf(env, extras={'base': 'IdleTerminationTimeout burble'}) + conf.add_vhost_test1() + conf.install() + rv = env.apache_restart() + env.httpd_error_log.ignore_recent() + assert rv != 0, "a non-numeric timeout was accepted" + + def test_core_007_07_forgotten_on_reload(self, env): + """Removing the directive and reloading stops the behaviour. + + A graceful reload keeps the same process, which is the point: a + fresh one would start from the compiled-in default whether or not + the MPM forgets the old value. + """ + pid = self.start(env, 'IdleTerminationTimeout 20') + # Rewritten in place rather than through HttpdConf, which stops the + # server before installing and so would lose the process. + conf_file = os.path.join(env.server_conf_dir, 'test.conf') + with open(conf_file) as fd: + kept = [l for l in fd if 'IdleTerminationTimeout' not in l] + with open(conf_file, 'w') as fd: + fd.writelines(kept) + assert env.apache_reload() == 0 + assert env.read_pid_file() == pid, "the reload replaced the process" + assert not self.wait_for_exit(pid, 25), \ + "the server still timed out after the directive was removed" diff --git a/test/pyhttpd/env.py b/test/pyhttpd/env.py index cba8cd4eda9..7ee56fc4c1d 100644 --- a/test/pyhttpd/env.py +++ b/test/pyhttpd/env.py @@ -742,7 +742,10 @@ def is_live(self, url: str = None, timeout: timedelta = None): while datetime.now() < try_until: # noinspection PyBroadException try: - r = self.curl_get(url, insecure=True) + # --max-time, or a server which accepts a connection and + # then never answers hangs the probe rather than failing it. + r = self.curl_get(url, insecure=True, + options=['--max-time', '5']) if r.exit_code == 0: return True time.sleep(.1) @@ -767,7 +770,7 @@ def is_dead(self, url: str = None, timeout: timedelta = None): while datetime.now() < try_until: # noinspection PyBroadException try: - r = self.curl_get(url) + r = self.curl_get(url, options=['--max-time', '5']) if r.exit_code != 0: return True time.sleep(.1)