quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

test_19_shutdown.py (9151B)


      1 #!/usr/bin/env python3
      2 # -*- coding: utf-8 -*-
      3 #***************************************************************************
      4 #                                  _   _ ____  _
      5 #  Project                     ___| | | |  _ \| |
      6 #                             / __| | | | |_) | |
      7 #                            | (__| |_| |  _ <| |___
      8 #                             \___|\___/|_| \_\_____|
      9 #
     10 # Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
     11 #
     12 # This software is licensed as described in the file COPYING, which
     13 # you should have received as part of this distribution. The terms
     14 # are also available at https://curl.se/docs/copyright.html.
     15 #
     16 # You may opt to use, copy, modify, merge, publish, distribute and/or sell
     17 # copies of the Software, and permit persons to whom the Software is
     18 # furnished to do so, under the terms of the COPYING file.
     19 #
     20 # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
     21 # KIND, either express or implied.
     22 #
     23 # SPDX-License-Identifier: curl
     24 #
     25 ###########################################################################
     26 #
     27 import logging
     28 import os
     29 import re
     30 import pytest
     31 
     32 from testenv import Env, CurlClient, LocalClient
     33 
     34 
     35 log = logging.getLogger(__name__)
     36 
     37 
     38 class TestShutdown:
     39 
     40     @pytest.fixture(autouse=True, scope='class')
     41     def _class_scope(self, env, httpd):
     42         indir = httpd.docs_dir
     43         env.make_data_file(indir=indir, fname="data-10k", fsize=10*1024)
     44         env.make_data_file(indir=indir, fname="data-100k", fsize=100*1024)
     45         env.make_data_file(indir=indir, fname="data-1m", fsize=1024*1024)
     46 
     47     # check with `tcpdump` that we see curl TCP RST packets
     48     @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available")
     49     @pytest.mark.parametrize("proto", ['http/1.1'])
     50     def test_19_01_check_tcp_rst(self, env: Env, httpd, proto):
     51         if env.ci_run:
     52             pytest.skip("seems not to work in CI")
     53         # timing critical, disable trace overrides
     54         run_env = os.environ.copy()
     55         if 'CURL_DEBUG' in run_env:
     56             del run_env['CURL_DEBUG']
     57         curl = CurlClient(env=env, run_env=run_env)
     58         port = env.port_for(alpn_proto=proto)
     59         url = f'https://{env.domain1}:{port}/data.json?[0-1]'
     60         r = curl.http_download(urls=[url], alpn_proto=proto, with_tcpdump=True, extra_args=[
     61             '--parallel'
     62         ])
     63         r.check_response(http_status=200, count=2)
     64         assert r.tcpdump
     65         assert len(r.tcpdump.get_rsts(ports=[port])) != 0, f'Expected TCP RSTs packets: {r.tcpdump.stderr}'
     66 
     67     # check with `tcpdump` that we do NOT see TCP RST when CURL_GRACEFUL_SHUTDOWN set
     68     @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available")
     69     @pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
     70     def test_19_02_check_shutdown(self, env: Env, httpd, proto):
     71         if not env.curl_is_debug():
     72             pytest.skip('only works for curl debug builds')
     73         run_env = os.environ.copy()
     74         run_env.update({
     75             'CURL_GRACEFUL_SHUTDOWN': '2000',
     76             'CURL_DEBUG': 'ssl,tcp,lib-ids,multi'
     77         })
     78         curl = CurlClient(env=env, run_env=run_env)
     79         port = env.port_for(alpn_proto=proto)
     80         url = f'https://{env.domain1}:{port}/data.json?[0-1]'
     81         r = curl.http_download(urls=[url], alpn_proto=proto, with_tcpdump=True, extra_args=[
     82             '--parallel'
     83         ])
     84         r.check_response(http_status=200, count=2)
     85         assert r.tcpdump
     86         assert len(r.tcpdump.get_rsts(ports=[port])) == 0, 'Unexpected TCP RST packets'
     87 
     88     # run downloads where the server closes the connection after each request
     89     @pytest.mark.parametrize("proto", ['http/1.1'])
     90     def test_19_03_shutdown_by_server(self, env: Env, httpd, proto):
     91         if not env.curl_is_debug():
     92             pytest.skip('only works for curl debug builds')
     93         count = 10
     94         curl = CurlClient(env=env, run_env={
     95             'CURL_GRACEFUL_SHUTDOWN': '2000',
     96             'CURL_DEBUG': 'ssl,multi'
     97         })
     98         url = f'https://{env.authority_for(env.domain1, proto)}/curltest/tweak/?'\
     99             f'id=[0-{count-1}]&with_cl&close'
    100         r = curl.http_download(urls=[url], alpn_proto=proto)
    101         r.check_response(http_status=200, count=count)
    102         shutdowns = [line for line in r.trace_lines
    103                      if re.match(r'.*\[SHUTDOWN] shutdown, done=1', line)]
    104         assert len(shutdowns) == count, f'{shutdowns}'
    105 
    106     # run downloads with CURLOPT_FORBID_REUSE set, meaning *we* close
    107     # the connection after each request
    108     @pytest.mark.parametrize("proto", ['http/1.1'])
    109     def test_19_04_shutdown_by_curl(self, env: Env, httpd, proto):
    110         if not env.curl_is_debug():
    111             pytest.skip('only works for curl debug builds')
    112         count = 10
    113         docname = 'data.json'
    114         url = f'https://localhost:{env.https_port}/{docname}'
    115         client = LocalClient(name='hx_download', env=env, run_env={
    116             'CURL_GRACEFUL_SHUTDOWN': '2000',
    117             'CURL_DEBUG': 'ssl,multi'
    118         })
    119         if not client.exists():
    120             pytest.skip(f'example client not built: {client.name}')
    121         r = client.run(args=[
    122              '-n', f'{count}', '-f', '-V', proto, url
    123         ])
    124         r.check_exit_code(0)
    125         shutdowns = [line for line in r.trace_lines
    126                      if re.match(r'.*SHUTDOWN] shutdown, done=1', line)]
    127         assert len(shutdowns) == count, f'{shutdowns}'
    128 
    129     # run event-based downloads with CURLOPT_FORBID_REUSE set, meaning *we* close
    130     # the connection after each request
    131     @pytest.mark.parametrize("proto", ['http/1.1'])
    132     def test_19_05_event_shutdown_by_server(self, env: Env, httpd, proto):
    133         if not env.curl_is_debug():
    134             pytest.skip('only works for curl debug builds')
    135         count = 10
    136         run_env = os.environ.copy()
    137         # forbid connection reuse to trigger shutdowns after transfer
    138         run_env['CURL_FORBID_REUSE'] = '1'
    139         # make socket receives block 50% of the time to delay shutdown
    140         run_env['CURL_DBG_SOCK_RBLOCK'] = '50'
    141         run_env['CURL_DEBUG'] = 'ssl,multi,lib-ids'
    142         curl = CurlClient(env=env, run_env=run_env)
    143         url = f'https://{env.authority_for(env.domain1, proto)}/curltest/tweak/?'\
    144             f'id=[0-{count-1}]&with_cl&'
    145         r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[
    146             '--test-event'
    147         ])
    148         r.check_response(http_status=200, count=count)
    149         # check that we closed all connections
    150         closings = [line for line in r.trace_lines
    151                     if re.match(r'.*SHUTDOWN] (force )?closing', line)]
    152         assert len(closings) == count, f'{closings}'
    153         # check that all connection sockets were removed from event
    154         removes = [line for line in r.trace_lines
    155                    if re.match(r'.*socket cb: socket \d+ REMOVED', line)]
    156         assert len(removes) == count, f'{removes}'
    157 
    158     # check graceful shutdown on multiplexed http
    159     @pytest.mark.parametrize("proto", ['h2', 'h3'])
    160     def test_19_06_check_shutdown(self, env: Env, httpd, nghttpx, proto):
    161         if proto == 'h3' and not env.have_h3():
    162             pytest.skip("h3 not supported")
    163         if not env.curl_is_debug():
    164             pytest.skip('only works for curl debug builds')
    165         curl = CurlClient(env=env, run_env={
    166             'CURL_GRACEFUL_SHUTDOWN': '2000',
    167             'CURL_DEBUG': 'all'
    168         })
    169         url = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-1]'
    170         r = curl.http_download(urls=[url], alpn_proto=proto, with_tcpdump=True, extra_args=[
    171             '--parallel'
    172         ])
    173         r.check_response(http_status=200, count=2)
    174         # check connection cache closings
    175         shutdowns = [line for line in r.trace_lines
    176                      if re.match(r'.*SHUTDOWN] shutdown, done=1', line)]
    177         assert len(shutdowns) == 1, f'{shutdowns}'
    178 
    179     # run connection pressure, many small transfers, not reusing connections,
    180     # limited total
    181     @pytest.mark.parametrize("proto", ['http/1.1'])
    182     def test_19_07_shutdown_by_curl(self, env: Env, httpd, proto):
    183         if not env.curl_is_debug():
    184             pytest.skip('only works for curl debug builds')
    185         count = 500
    186         docname = 'data.json'
    187         url = f'https://localhost:{env.https_port}/{docname}'
    188         client = LocalClient(name='hx_download', env=env, run_env={
    189             'CURL_GRACEFUL_SHUTDOWN': '2000',
    190             'CURL_DEBUG': 'ssl,multi'
    191         })
    192         if not client.exists():
    193             pytest.skip(f'example client not built: {client.name}')
    194         r = client.run(args=[
    195              '-n', f'{count}',  # that many transfers
    196              '-f',  # forbid conn reuse
    197              '-m', '10',  # max parallel
    198              '-T', '5',  # max total conns at a time
    199              '-V', proto,
    200              url
    201         ])
    202         r.check_exit_code(0)
    203         shutdowns = [line for line in r.trace_lines
    204                      if re.match(r'.*SHUTDOWN] shutdown, done=1', line)]
    205         # we see less clean shutdowns as total limit forces early closes
    206         assert len(shutdowns) < count, f'{shutdowns}'