test_19_shutdown.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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 difflib
  28. import filecmp
  29. import logging
  30. import os
  31. import re
  32. from datetime import timedelta
  33. import pytest
  34. from testenv import Env, CurlClient, LocalClient
  35. log = logging.getLogger(__name__)
  36. class TestShutdown:
  37. @pytest.fixture(autouse=True, scope='class')
  38. def _class_scope(self, env, httpd, nghttpx):
  39. if env.have_h3():
  40. nghttpx.start_if_needed()
  41. httpd.clear_extra_configs()
  42. httpd.reload()
  43. @pytest.fixture(autouse=True, scope='class')
  44. def _class_scope(self, env, httpd):
  45. indir = httpd.docs_dir
  46. env.make_data_file(indir=indir, fname="data-10k", fsize=10*1024)
  47. env.make_data_file(indir=indir, fname="data-100k", fsize=100*1024)
  48. env.make_data_file(indir=indir, fname="data-1m", fsize=1024*1024)
  49. # check with `tcpdump` that we see curl TCP RST packets
  50. @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available")
  51. @pytest.mark.parametrize("proto", ['http/1.1'])
  52. def test_19_01_check_tcp_rst(self, env: Env, httpd, repeat, proto):
  53. if env.ci_run:
  54. pytest.skip("seems not to work in CI")
  55. curl = CurlClient(env=env)
  56. url = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-1]'
  57. r = curl.http_download(urls=[url], alpn_proto=proto, with_tcpdump=True, extra_args=[
  58. '--parallel'
  59. ])
  60. r.check_response(http_status=200, count=2)
  61. assert r.tcpdump
  62. assert len(r.tcpdump.stats) != 0, f'Expected TCP RSTs packets: {r.tcpdump.stderr}'
  63. # check with `tcpdump` that we do NOT see TCP RST when CURL_GRACEFUL_SHUTDOWN set
  64. @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available")
  65. @pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
  66. def test_19_02_check_shutdown(self, env: Env, httpd, repeat, proto):
  67. if not env.curl_is_debug():
  68. pytest.skip('only works for curl debug builds')
  69. curl = CurlClient(env=env, run_env={
  70. 'CURL_GRACEFUL_SHUTDOWN': '2000',
  71. 'CURL_DEBUG': 'ssl'
  72. })
  73. url = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-1]'
  74. r = curl.http_download(urls=[url], alpn_proto=proto, with_tcpdump=True, extra_args=[
  75. '--parallel'
  76. ])
  77. r.check_response(http_status=200, count=2)
  78. assert r.tcpdump
  79. assert len(r.tcpdump.stats) == 0, f'Unexpected TCP RSTs packets'
  80. # run downloads where the server closes the connection after each request
  81. @pytest.mark.parametrize("proto", ['http/1.1'])
  82. def test_19_03_shutdown_by_server(self, env: Env, httpd, repeat, proto):
  83. if not env.curl_is_debug():
  84. pytest.skip('only works for curl debug builds')
  85. count = 10
  86. curl = CurlClient(env=env, run_env={
  87. 'CURL_GRACEFUL_SHUTDOWN': '2000',
  88. 'CURL_DEBUG': 'ssl'
  89. })
  90. url = f'https://{env.authority_for(env.domain1, proto)}/curltest/tweak/?'\
  91. f'id=[0-{count-1}]&with_cl&close'
  92. r = curl.http_download(urls=[url], alpn_proto=proto)
  93. r.check_response(http_status=200, count=count)
  94. shutdowns = [l for l in r.trace_lines if re.match(r'.*CCACHE\] shutdown #\d+, done=1', l)]
  95. assert len(shutdowns) == count, f'{shutdowns}'
  96. # run downloads with CURLOPT_FORBID_REUSE set, meaning *we* close
  97. # the connection after each request
  98. @pytest.mark.parametrize("proto", ['http/1.1'])
  99. def test_19_04_shutdown_by_curl(self, env: Env, httpd, proto, repeat):
  100. if not env.curl_is_debug():
  101. pytest.skip('only works for curl debug builds')
  102. count = 10
  103. docname = 'data.json'
  104. url = f'https://localhost:{env.https_port}/{docname}'
  105. client = LocalClient(name='hx-download', env=env, run_env={
  106. 'CURL_GRACEFUL_SHUTDOWN': '2000',
  107. 'CURL_DEBUG': 'ssl'
  108. })
  109. if not client.exists():
  110. pytest.skip(f'example client not built: {client.name}')
  111. r = client.run(args=[
  112. '-n', f'{count}', '-f', '-V', proto, url
  113. ])
  114. r.check_exit_code(0)
  115. shutdowns = [l for l in r.trace_lines if re.match(r'.*CCACHE\] shutdown #\d+, done=1', l)]
  116. assert len(shutdowns) == count, f'{shutdowns}'
  117. # run event-based downloads with CURLOPT_FORBID_REUSE set, meaning *we* close
  118. # the connection after each request
  119. @pytest.mark.parametrize("proto", ['http/1.1'])
  120. def test_19_05_event_shutdown_by_server(self, env: Env, httpd, proto, repeat):
  121. if not env.curl_is_debug():
  122. pytest.skip('only works for curl debug builds')
  123. count = 10
  124. curl = CurlClient(env=env, run_env={
  125. # forbid connection reuse to trigger shutdowns after transfer
  126. 'CURL_FORBID_REUSE': '1',
  127. # make socket receives block 50% of the time to delay shutdown
  128. 'CURL_DBG_SOCK_RBLOCK': '50',
  129. 'CURL_DEBUG': 'ssl'
  130. })
  131. url = f'https://{env.authority_for(env.domain1, proto)}/curltest/tweak/?'\
  132. f'id=[0-{count-1}]&with_cl&'
  133. r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[
  134. '--test-event'
  135. ])
  136. r.check_response(http_status=200, count=count)
  137. # check that we closed all connections
  138. closings = [l for l in r.trace_lines if re.match(r'.*CCACHE\] closing #\d+', l)]
  139. assert len(closings) == count, f'{closings}'
  140. # check that all connection sockets were removed from event
  141. removes = [l for l in r.trace_lines if re.match(r'.*socket cb: socket \d+ REMOVED', l)]
  142. assert len(removes) == count, f'{removes}'
  143. # check graceful shutdown on multiplexed http
  144. @pytest.mark.parametrize("proto", ['h2', 'h3'])
  145. def test_19_06_check_shutdown(self, env: Env, httpd, nghttpx, repeat, proto):
  146. if proto == 'h3' and not env.have_h3():
  147. pytest.skip("h3 not supported")
  148. if not env.curl_is_debug():
  149. pytest.skip('only works for curl debug builds')
  150. curl = CurlClient(env=env, run_env={
  151. 'CURL_GRACEFUL_SHUTDOWN': '2000',
  152. 'CURL_DEBUG': 'all'
  153. })
  154. url = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-1]'
  155. r = curl.http_download(urls=[url], alpn_proto=proto, with_tcpdump=True, extra_args=[
  156. '--parallel'
  157. ])
  158. r.check_response(http_status=200, count=2)
  159. # check connection cache closings
  160. shutdowns = [l for l in r.trace_lines if re.match(r'.*CCACHE\] shutdown #\d+, done=1', l)]
  161. assert len(shutdowns) == 1, f'{shutdowns}'