| import time |
| |
| import pytest |
| |
| from pyhttpd.conf import HttpdConf |
| |
| |
| class TestSystemdNotify: |
| """The service notifications mod_systemd sends over $NOTIFY_SOCKET |
| across the server lifecycle.""" |
| |
| @pytest.fixture(autouse=True, scope='class') |
| def _class_scope(self, env): |
| conf = HttpdConf(env) |
| conf.add_vhost_test1() |
| conf.install() |
| # Each test drives startup itself, so leave the server down. |
| assert env.apache_stop() == 0 |
| |
| @staticmethod |
| def index_of(messages, match): |
| for i, msg in enumerate(messages): |
| if match(msg): |
| return i |
| return -1 |
| |
| def test_systemd_001_01_startup(self, env): |
| """Startup reports configuration reading, then readiness, then that |
| the MPM is serving.""" |
| env.notify.clear() |
| assert env.apache_restart() == 0 |
| assert env.notify.wait_for_status(r'^Reading configuration\.\.\.$'), \ |
| f"no reload notification, got {env.notify.statuses()}" |
| assert env.notify.wait_for_status(r'^Configuration loaded\.$'), \ |
| f"no ready notification, got {env.notify.statuses()}" |
| assert env.notify.wait_for_status(r'^Processing requests\.\.\.$'), \ |
| f"no pre_mpm notification, got {env.notify.statuses()}" |
| |
| def test_systemd_001_02_reloading_before_ready(self, env): |
| """RELOADING=1 is sent while the configuration is read, and READY=1 |
| only once it has been.""" |
| env.notify.clear() |
| assert env.apache_restart() == 0 |
| assert env.notify.wait_for_status(r'^Configuration loaded\.$') |
| msgs = env.notify.messages |
| reloading = self.index_of( |
| msgs, lambda m: m.get('RELOADING') == '1' |
| and m.get('STATUS') == 'Reading configuration...') |
| ready = self.index_of( |
| msgs, lambda m: m.get('READY') == '1' |
| and m.get('STATUS') == 'Configuration loaded.') |
| assert reloading >= 0 and ready >= 0 |
| assert reloading < ready, \ |
| "READY=1 was reported before the configuration was read" |
| |
| def test_systemd_001_03_mainpid(self, env): |
| """MAINPID is the pid of the parent process, the one httpd records |
| in its pid file.""" |
| env.notify.clear() |
| assert env.apache_restart() == 0 |
| msg = env.notify.wait_for_key('MAINPID') |
| assert msg, f"no MAINPID reported, got {env.notify.messages}" |
| assert msg.get('READY') == '1' |
| assert msg.get('STATUS') == 'Processing requests...' |
| assert int(msg['MAINPID']) == env.read_pid_file() |
| |
| def test_systemd_001_04_reload(self, env): |
| """A graceful restart reports reading the configuration and then |
| being ready again.""" |
| assert env.apache_restart() == 0 |
| pid = env.read_pid_file() |
| env.notify.clear() |
| assert env.apache_reload() == 0 |
| assert env.notify.wait_for_status(r'^Reading configuration\.\.\.$'), \ |
| f"no reload notification, got {env.notify.statuses()}" |
| assert env.notify.wait_for_status(r'^Configuration loaded\.$'), \ |
| f"no ready notification, got {env.notify.statuses()}" |
| # The parent survives a graceful restart, and the MPM is not started |
| # over, so the pid systemd tracks neither changes nor is re-reported. |
| assert env.read_pid_file() == pid |
| for msg in env.notify.messages: |
| assert 'MAINPID' not in msg or int(msg['MAINPID']) == pid |
| |
| def test_systemd_001_05_hard_restart(self, env): |
| """An ungraceful restart starts the MPM over, and re-reports the |
| main pid, which is still that of the surviving parent.""" |
| assert env.apache_restart() == 0 |
| pid = env.read_pid_file() |
| env.notify.clear() |
| assert env.apache_hard_restart() == 0 |
| assert env.notify.wait_for_status(r'^Configuration loaded\.$'), \ |
| f"no ready notification, got {env.notify.statuses()}" |
| msg = env.notify.wait_for_key('MAINPID') |
| assert msg, f"no MAINPID after restart, got {env.notify.messages}" |
| assert int(msg['MAINPID']) == pid |
| assert env.read_pid_file() == pid |
| |
| def test_systemd_001_06_notify_socket_kept(self, env): |
| """mod_systemd leaves NOTIFY_SOCKET in the environment, so a second |
| start after a stop is reported just like the first.""" |
| assert env.apache_restart() == 0 |
| assert env.apache_stop() == 0 |
| env.notify.clear() |
| assert env.apache_restart() == 0 |
| assert env.notify.wait_for_status(r'^Configuration loaded\.$') |
| |
| def test_systemd_001_07_stopping(self, env): |
| """Shutdown is announced before the process goes away.""" |
| assert env.apache_restart() == 0 |
| env.notify.clear() |
| assert env.apache_stop() == 0 |
| # The server is already gone, so anything it sent has arrived. |
| msg = env.notify.wait_for_key('STOPPING', timeout=1) |
| assert msg, f"no STOPPING=1 on shutdown, got {env.notify.messages}" |
| assert msg['STOPPING'] == '1' |
| assert msg.get('STATUS') == 'Shutting down.' |
| |
| def test_systemd_001_08_reloading_monotonic(self, env): |
| assert env.apache_restart() == 0 |
| env.notify.clear() |
| assert env.apache_reload() == 0 |
| msg = env.notify.wait_for(lambda m: m.get('RELOADING') == '1') |
| assert msg, "no RELOADING=1 on graceful restart" |
| assert 'MONOTONIC_USEC' in msg, \ |
| f"RELOADING=1 sent without MONOTONIC_USEC: {msg}" |
| # systemd compares this against its own reading of the same clock. |
| assert 0 < int(msg['MONOTONIC_USEC']) <= time.clock_gettime_ns( |
| time.CLOCK_MONOTONIC) // 1000 |
| |
| def test_systemd_001_09_watchdog(self, env): |
| """A server started with $WATCHDOG_USEC reports that it is alive. |
| |
| $WATCHDOG_PID is left unset, which sd_watchdog_enabled(3) takes to |
| mean any process may report: it has to be, since apachectl starts a |
| parent whose pid nothing knew in advance. The keep-alive arrives |
| with the startup notifications rather than only from the monitor |
| hook ten seconds later, which is what makes this cheap to check. |
| test_006_watchdog.py covers the rest of the protocol. |
| """ |
| assert env.apache_stop() == 0 |
| # Long enough that mod_systemd does not object to it (AH10622). |
| env.set_httpd_env('WATCHDOG_USEC', str(60 * 1000000)) |
| try: |
| env.notify.clear() |
| assert env.apache_restart() == 0 |
| msg = env.notify.wait_for_key('WATCHDOG', timeout=4) |
| assert msg, f"no watchdog keepalive was sent, " \ |
| f"got {env.notify.messages}" |
| assert msg['WATCHDOG'] == '1' |
| finally: |
| env.set_httpd_env('WATCHDOG_USEC', None) |
| assert env.apache_restart() == 0 |