You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by ic...@apache.org on 2022/04/15 10:59:22 UTC

svn commit: r1899885 - /httpd/httpd/trunk/test/modules/core/test_002_restarts.py

Author: icing
Date: Fri Apr 15 10:59:22 2022
New Revision: 1899885

URL: http://svn.apache.org/viewvc?rev=1899885&view=rev
Log:
  *) test: core stress test_core_002 enhanved to monitor dynamic child
     changes on load and graceful reload of the server.


Modified:
    httpd/httpd/trunk/test/modules/core/test_002_restarts.py

Modified: httpd/httpd/trunk/test/modules/core/test_002_restarts.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/core/test_002_restarts.py?rev=1899885&r1=1899884&r2=1899885&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/core/test_002_restarts.py (original)
+++ httpd/httpd/trunk/test/modules/core/test_002_restarts.py Fri Apr 15 10:59:22 2022
@@ -1,4 +1,8 @@
 import os
+import re
+import time
+from datetime import datetime, timedelta
+from threading import Thread
 
 import pytest
 
@@ -6,50 +10,141 @@ from .env import CoreTestEnv
 from pyhttpd.conf import HttpdConf
 
 
+class Loader:
+
+    def __init__(self, env, url: str, clients: int, req_per_client: int = 10):
+        self.env = env
+        self.url = url
+        self.clients = clients
+        self.req_per_client = req_per_client
+        self.result = None
+        self.total_request = 0
+        self._thread = None
+
+    def run(self):
+        self.total_requests = self.clients * self.req_per_client
+        conn_per_client = 5
+        args = [self.env.h2load, f"--connect-to=localhost:{self.env.https_port}",
+                "--h1",                                 # use only http/1.1
+                "-n", str(self.total_requests),              # total # of requests to make
+                "-c", str(conn_per_client * self.clients),   # total # of connections to make
+                "-r", str(self.clients),                     # connections at a time
+                "--rate-period", "2",                   # create conns every 2 sec
+                self.url,
+                ]
+        self.result = self.env.run(args)
+
+    def start(self):
+        self._thread = Thread(target=self.run)
+        self._thread.start()
+
+    def join(self):
+        self._thread.join()
+
+
+class ChildDynamics:
+
+    RE_DATE_TIME = re.compile(r'\[(?P<date_time>[^\]]+)\] .*')
+    RE_TIME_FRAC = re.compile(r'(?P<dt>.* \d\d:\d\d:\d\d)(?P<frac>.(?P<micros>.\d+)) (?P<year>\d+)')
+    RE_CHILD_CHANGE = re.compile(r'\[(?P<date_time>[^\]]+)\] '
+                                 r'\[mpm_event:\w+\]'
+                                 r' \[pid (?P<main_pid>\d+):tid \w+\] '
+                                 r'.* Child (?P<child_no>\d+) (?P<action>\w+): '
+                                 r'pid (?P<pid>\d+), gen (?P<generation>\d+), .*')
+
+    def __init__(self, env: CoreTestEnv):
+        self.env = env
+        self.changes = list()
+        self._start = None
+        for l in open(env.httpd_error_log.path):
+            m = self.RE_CHILD_CHANGE.match(l)
+            if m:
+                self.changes.append({
+                    'pid': int(m.group('pid')),
+                    'child_no': int(m.group('child_no')),
+                    'gen': int(m.group('generation')),
+                    'action': m.group('action'),
+                    'rtime' : self._rtime(m.group('date_time'))
+                })
+                continue
+            if self._start is None:
+                m = self.RE_DATE_TIME.match(l)
+                if m:
+                    self._rtime(m.group('date_time'))
+
+    def _rtime(self, s: str) -> timedelta:
+        micros = 0
+        m = self.RE_TIME_FRAC.match(s)
+        if m:
+            micros = int(m.group('micros'))
+            s = f"{m.group('dt')} {m.group('year')}"
+        d = datetime.strptime(s, '%a %b %d %H:%M:%S %Y') + timedelta(microseconds=micros)
+        if self._start is None:
+            self._start = d
+        delta = d - self._start
+        return f"{delta.seconds:+02d}.{delta.microseconds:06d}"
+
+
+
 @pytest.mark.skipif(condition='STRESS_TEST' not in os.environ,
                     reason="STRESS_TEST not set in env")
 @pytest.mark.skipif(condition=not CoreTestEnv().h2load_is_at_least('1.41.0'),
                     reason="h2load unavailable or misses --connect-to option")
 class TestRestarts:
 
-    @pytest.fixture(autouse=True, scope='class')
-    def _class_scope(self, env):
+    def test_core_002_01(self, env):
+        # Lets make a tight config that triggers dynamic child behaviour
         conf = HttpdConf(env, extras={
             'base': f"""
-StartServers            1
-ServerLimit             3
-ThreadLimit             4
-ThreadsPerChild         4
-MinSpareThreads         4
-MaxSpareThreads         6
-MaxRequestWorkers       12
-MaxConnectionsPerChild  0
+        StartServers            1
+        ServerLimit             3
+        ThreadLimit             4
+        ThreadsPerChild         4
+        MinSpareThreads         4
+        MaxSpareThreads         6
+        MaxRequestWorkers       12
+        MaxConnectionsPerChild  0
 
-LogLevel mpm_event:trace6
-        """,
+        LogLevel mpm_event:trace6
+                """,
         })
         conf.add_vhost_cgi()
         conf.install()
-        assert env.apache_restart() == 0
-
-    def test_core_002_01(self, env):
-        clients = 6
-        total_requests = clients * 10
-        conn_per_client = 5
-        url = env.mkurl("https", "cgi", "/delay.py")
-        args = [env.h2load, f"--connect-to=localhost:{env.https_port}",
-                "--h1",                                 # use only http/1.1
-                "-n", str(total_requests),              # total # of requests to make
-                "-c", str(conn_per_client * clients),   # total # of connections to make
-                "-r", str(clients),                     # connections at a time
-                "--rate-period", "2",                   # create conns every 2 sec
-                url,
-                ]
-        r = env.run(args)
-        assert 0 == r.exit_code
-        r = env.h2load_status(r)
-        assert r.results["h2load"]["requests"] == {
-            "total": total_requests, "started": total_requests,
-            "done": total_requests, "succeeded": total_requests
-        }, f"{r.stdout}"
 
+        # clear logs and start server, start load
+        env.httpd_error_log.clear_log()
+        assert env.apache_restart() == 0
+        # we should see a single child started
+        cd = ChildDynamics(env)
+        assert len(cd.changes) == 1, f"{cd.changes}"
+        assert cd.changes[0]['action'] == 'started'
+        # This loader simulates 6 clients, each making 10 requests.
+        # delay.py sleeps for 1sec, so this should run for about 10 seconds
+        loader = Loader(env=env, url=env.mkurl("https", "cgi", "/delay.py"),
+                        clients=6, req_per_client=10)
+        loader.start()
+        # Expect 2 more children to have been started after half time
+        time.sleep(5)
+        cd = ChildDynamics(env)
+        assert len(cd.changes) == 3, f"{cd.changes}"
+        assert len([x for x in cd.changes if x['action'] == 'started']) == 3, f"{cd.changes}"
+
+        # Trigger a server reload
+        assert env.apache_reload() == 0
+        # a graceful reload lets ongoing requests continue, but
+        # after a while all gen 0 children should have stopped
+        time.sleep(3)  # FIXME: this pbly depends on the runtime a lot, do we have expectations?
+        cd = ChildDynamics(env)
+        gen0 = [x for x in cd.changes if x['gen'] == 0]
+        assert len([x for x in gen0 if x['action'] == 'stopped']) == 3
+
+        # wait for the loader to finish and stop the server
+        loader.join()
+        env.apache_stop()
+
+        # Similar to before the reload, we expect 3 children to have
+        # been started and stopped again on server stop
+        cd = ChildDynamics(env)
+        gen1 = [x for x in cd.changes if x['gen'] == 1]
+        assert len([x for x in gen1 if x['action'] == 'started']) == 3
+        assert len([x for x in gen1 if x['action'] == 'stopped']) == 3