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 2021/09/23 08:56:34 UTC

svn commit: r1893542 [12/12] - in /httpd/httpd/branches/2.4.x: ./ test/modules/ test/modules/http2/ test/modules/http2/conf/ test/modules/http2/data/ test/modules/http2/htdocs/ test/modules/http2/htdocs/cgi/ test/modules/http2/htdocs/cgi/files/ test/mo...

Added: httpd/httpd/branches/2.4.x/test/modules/http2/test_712_buffering.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_712_buffering.py?rev=1893542&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_712_buffering.py (added)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_712_buffering.py Thu Sep 23 08:56:33 2021
@@ -0,0 +1,175 @@
+import datetime
+import re
+import sys
+import time
+import subprocess
+
+from datetime import timedelta
+from threading import Thread
+
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class CurlPiper:
+
+    def __init__(self, url: str):
+        self.url = url
+        self.proc = None
+        self.args = None
+        self.headerfile = None
+        self._stderr = []
+        self._stdout = []
+        self.stdout_thread = None
+        self.stderr_thread = None
+
+    def start(self, env):
+        self.args, self.headerfile = env.curl_complete_args(self.url, timeout=5, options=[
+            "-T", "-", "-X", "POST", "--trace-ascii", "%", "--trace-time"])
+        sys.stderr.write("starting: {0}\n".format(self.args))
+        self.proc = subprocess.Popen(self.args, stdin=subprocess.PIPE,
+                                     stdout=subprocess.PIPE,
+                                     stderr=subprocess.PIPE,
+                                     bufsize=0)
+
+        def read_output(fh, buffer):
+            while True:
+                chunk = fh.read()
+                if not chunk:
+                    break
+                buffer.append(chunk.decode())
+
+        # collect all stdout and stderr until we are done
+        # use separate threads to not block ourself
+        self._stderr = []
+        self._stdout = []
+        if self.proc.stderr:
+            self.stderr_thread = Thread(target=read_output, args=(self.proc.stderr, self._stderr))
+            self.stderr_thread.start()
+        if self.proc.stdout:
+            self.stdout_thread = Thread(target=read_output, args=(self.proc.stdout, self._stdout))
+            self.stdout_thread.start()
+        return self.proc
+
+    def send(self, data: str):
+        self.proc.stdin.write(data.encode())
+        self.proc.stdin.flush()
+
+    def close(self) -> ([str], [str]):
+        self.proc.stdin.close()
+        self.stdout_thread.join()
+        self.stderr_thread.join()
+        self._end()
+        return self._stdout, self._stderr
+
+    def _end(self):
+        if self.proc:
+            # noinspection PyBroadException
+            try:
+                if self.proc.stdin:
+                    # noinspection PyBroadException
+                    try:
+                        self.proc.stdin.close()
+                    except Exception:
+                        pass
+                if self.proc.stdout:
+                    self.proc.stdout.close()
+                if self.proc.stderr:
+                    self.proc.stderr.close()
+            except Exception:
+                self.proc.terminate()
+            finally:
+                self.stdout_thread = None
+                self.stderr_thread = None
+                self.proc = None
+
+    def stutter_check(self, env, chunks: [str], stutter: datetime.timedelta):
+        if not self.proc:
+            self.start(env)
+        for chunk in chunks:
+            self.send(chunk)
+            time.sleep(stutter.total_seconds())
+        recv_out, recv_err = self.close()
+        # assert we got everything back
+        assert "".join(chunks) == "".join(recv_out)
+        # now the tricky part: check *when* we got everything back
+        recv_times = []
+        for line in "".join(recv_err).split('\n'):
+            m = re.match(r'^\s*(\d+:\d+:\d+(\.\d+)?) <= Recv data, (\d+) bytes.*', line)
+            if m:
+                recv_times.append(datetime.time.fromisoformat(m.group(1)))
+        # received as many chunks as we sent
+        assert len(chunks) == len(recv_times), "received response not in {0} chunks, but {1}".format(
+            len(chunks), len(recv_times))
+
+        def microsecs(tdelta):
+            return ((tdelta.hour * 60 + tdelta.minute) * 60 + tdelta.second) * 1000000 + tdelta.microsecond
+
+        recv_deltas = []
+        last_mics = microsecs(recv_times[0])
+        for ts in recv_times[1:]:
+            mics = microsecs(ts)
+            delta_mics = mics - last_mics
+            if delta_mics < 0:
+                delta_mics += datetime.time(23, 59, 59, 999999)
+            recv_deltas.append(datetime.timedelta(microseconds=delta_mics))
+            last_mics = mics
+        stutter_td = datetime.timedelta(seconds=stutter.total_seconds() * 0.9)  # 10% leeway
+        # TODO: the first two chunks are often close together, it seems
+        # there still is a little buffering delay going on
+        for idx, td in enumerate(recv_deltas[1:]):
+            assert stutter_td < td, \
+                f"chunk {idx} arrived too early \n{recv_deltas}\nafter {td}\n{recv_err}"
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        env.setup_data_1k_1m()
+        conf = HttpdConf(env).add("H2OutputBuffering off")
+        conf.add_vhost_cgi(h2proxy_self=True).install()
+        assert env.apache_restart() == 0
+
+    @pytest.mark.skip(reason="this test shows unreliable jitter")
+    def test_712_01(self, env):
+        # test gRPC like requests that do not end, but give answers, see #207
+        #
+        # this test works like this:
+        # - use curl to POST data to the server /h2test/echo
+        # - feed curl the data in chunks, wait a bit between chunks
+        # - since some buffering on curl's stdout to Python is involved,
+        #   we will see the response data only at the end.
+        # - therefore, we enable tracing with timestamps in curl on stderr
+        #   and see when the response chunks arrive
+        # - if the server sends the incoming data chunks back right away,
+        #   as it should, we see receiving timestamps separated roughly by the
+        #   wait time between sends.
+        #
+        url = env.mkurl("https", "cgi", "/h2test/echo")
+        base_chunk = "0123456789"
+        chunks = ["chunk-{0:03d}-{1}\n".format(i, base_chunk) for i in range(5)]
+        stutter = timedelta(seconds=0.2)  # this is short, but works on my machine (tm)
+        piper = CurlPiper(url=url)
+        piper.stutter_check(env, chunks, stutter)
+
+    def test_712_02(self, env):
+        # same as 712_01 but via mod_proxy_http2
+        #
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/echo")
+        base_chunk = "0123456789"
+        chunks = ["chunk-{0:03d}-{1}\n".format(i, base_chunk) for i in range(3)]
+        stutter = timedelta(seconds=0.4)  # need a bit more delay since we have the extra connection
+        piper = CurlPiper(url=url)
+        piper.stutter_check(env, chunks, stutter)
+
+    def test_712_03(self, env):
+        # same as 712_02 but with smaller chunks
+        #
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/echo")
+        base_chunk = "0"
+        chunks = ["ck{0}-{1}\n".format(i, base_chunk) for i in range(3)]
+        stutter = timedelta(seconds=0.4)  # need a bit more delay since we have the extra connection
+        piper = CurlPiper(url=url)
+        piper.stutter_check(env, chunks, stutter)