blob: 01bd684afa0370dd1d9ee183bcec9cd53baea754 [file]
import os
import time
import pytest
from pyhttpd.depends import needs_dependency
from .env import (NOTIFY_RELOAD_VERSION, TransientService, error_log_since,
error_log_size, http_responds, systemd_version)
pytestmark = [
needs_dependency(
"systemd --user manager", TransientService.is_available(),
reason="no per-user systemd manager to run a transient service under",
detected="systemd-run --user is not usable here"),
# a version gate, not a missing dependency: keeps skipping when strict
pytest.mark.skipif(
systemd_version() < NOTIFY_RELOAD_VERSION,
reason=f"Type=notify-reload needs systemd {NOTIFY_RELOAD_VERSION}"),
]
class TestNotifyReload:
"""httpd as a Type=notify-reload unit.
Under Type=notify a reload is only whatever ExecReload= does, and
systemctl returns as soon as that command exits. For "httpd -k
graceful" that is as soon as the signal has been sent, long before the
new configuration is in use, and it reports success however the restart
turns out. Type=notify-reload holds the reload job open until the
service sends RELOADING=1 and then READY=1, which mod_systemd sends
from pre_config and post_config, so the result is reported once it is
known.
The unit here keeps ExecReload= and sets ReloadSignal=SIGCONT, which
httpd ignores. The manager runs ExecReload= first and sends
ReloadSignal= only once it has exited successfully, so:
- "httpd -k graceful" stays the thing which restarts the server, and
because it parses the new configuration in its own process before
signalling, a configuration which does not parse fails the reload
without the running server being signalled at all. A bare
ReloadSignal= has the running parent read the new configuration and
exit if it does not parse.
- the signal the manager then sends is redundant, so it is pointed at
one httpd does not act on. The default is SIGHUP, which httpd
takes as an *ungraceful* restart.
"""
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
assert env.apache_stop() == 0
yield
assert env.apache_stop() == 0
@pytest.fixture
def service(self, env) -> TransientService:
svc = TransientService(env, port=env.http_port2,
name=f'httpd-reload-{os.getpid()}',
service_type='notify-reload',
properties=['ReloadSignal=SIGCONT'])
yield svc
svc.stop()
def test_systemd_007_01_reload_waits_for_the_new_config(self, env, service):
"""systemctl reload returns only once the reloaded server is
serving: the port added to the configuration answers without the
test waiting for it, because READY=1 is sent from post_config, by
which point the listeners are open."""
assert service.start().returncode == 0
assert service.wait_active()
assert http_responds(env.http_port2)
service.rewrite_conf(extra=f'Listen {env.proxy_port}')
r = service.reload()
assert r.returncode == 0, f"reload failed: {r.stderr}"
assert http_responds(env.proxy_port, timeout=10.0), \
"reload returned before the newly configured port was served"
assert http_responds(env.http_port2)
def test_systemd_007_02_broken_config_fails_the_reload(self, env, service):
"""A configuration which does not parse fails the reload and leaves
the running server alone. ExecReload= reads it in a process of its
own and exits without signalling, and the manager sends no reload
signal of its own once that has failed, so the parent never reads
it."""
assert service.start().returncode == 0
assert service.wait_active()
pid = int(service.show('MainPID'))
service.rewrite_conf(extra='ThisDirectiveDoesNotExist on')
r = service.reload()
assert r.returncode != 0, \
"reload of an unparseable configuration reported success"
assert service.show('ActiveState') == 'active', \
"a failed reload took the unit out of active"
# The parent exits when a restart re-reads a configuration which
# does not parse, and its children go on serving for a while
# afterwards, so answering a request is not on its own proof that
# the server survived: check the process the manager tracks.
assert int(service.show('MainPID')) == pid, \
"the parent was restarted despite the configuration not parsing"
assert http_responds(env.http_port2)
def test_systemd_007_03_reload_restarts_gracefully_once(self, env, service):
"""One reload is one graceful restart, and no ungraceful one.
Leaving ReloadSignal= at its SIGHUP default is what this catches:
httpd restarts ungracefully on SIGHUP, dropping the connections a
reload is supposed to keep."""
if env.mpm_module not in ('mpm_event', 'mpm_worker'):
pytest.skip(f"{env.mpm_module} does not log the graceful restart")
assert service.start().returncode == 0
assert service.wait_active()
pos = error_log_size(env)
assert service.reload().returncode == 0
# The restart is logged before the configuration is re-read, so it
# is written by the time READY=1 ends the reload; give the
# redundant signal which follows a moment to land as well.
time.sleep(2)
log = error_log_since(env, pos)
assert log.count("Attempting to restart") == 0, \
f"the reload restarted the server ungracefully:\n{log}"
assert log.count("Doing graceful restart") == 1, \
f"expected one graceful restart, log said:\n{log}"
def test_systemd_007_04_reload_repeats(self, env, service):
"""Reloading twice works. The manager ignores a RELOADING=1 which
is not stamped later than the reload it asked for, so a server
which got MONOTONIC_USEC wrong could report the first reload and
then leave the second to time out."""
assert service.start().returncode == 0
assert service.wait_active()
pid = int(service.show('MainPID'))
for i in range(2):
r = service.reload()
assert r.returncode == 0, f"reload {i + 1} failed: {r.stderr}"
assert service.show('ActiveState') == 'active'
assert int(service.show('MainPID')) == pid, \
"a graceful restart replaced the parent process"
assert http_responds(env.http_port2)