|
5 | 5 | import time |
6 | 6 |
|
7 | 7 | import pytest |
| 8 | +import requests |
| 9 | +from requests.exceptions import Timeout |
8 | 10 |
|
9 | 11 | from pytest_httpserver import HTTPServer |
10 | 12 |
|
@@ -58,9 +60,9 @@ def test_slow_start_server_waits_for_ready(): |
58 | 60 | server = SlowStartServer(host="localhost", port=0) |
59 | 61 | server.expect_request("/").respond_with_data("ok") |
60 | 62 |
|
61 | | - start_time = time.time() |
| 63 | + start_time = time.monotonic() |
62 | 64 | server.start() |
63 | | - elapsed = time.time() - start_time |
| 65 | + elapsed = time.monotonic() - start_time |
64 | 66 |
|
65 | 67 | try: |
66 | 68 | # Should have waited at least 0.5 seconds |
@@ -112,3 +114,75 @@ def test_warns_when_ready_event_not_set(): |
112 | 114 | raise AssertionError("Server did not accept connections within 1 second") |
113 | 115 | finally: |
114 | 116 | server.stop() |
| 117 | + |
| 118 | + |
| 119 | +class SlowServeServer(HTTPServer): |
| 120 | + """A server that delays serve_forever() but does not set ready event early. |
| 121 | +
|
| 122 | + This simulates the scenario where: |
| 123 | + - bind() and listen() complete (TCP connections queue in backlog) |
| 124 | + - But serve_forever() hasn't started yet (no HTTP responses) |
| 125 | + """ |
| 126 | + |
| 127 | + def thread_target(self): |
| 128 | + assert self.server is not None |
| 129 | + # Delay before serve_forever - connections will queue but not be processed |
| 130 | + time.sleep(3.0) |
| 131 | + self._server_ready_event.set() |
| 132 | + self.server.serve_forever() |
| 133 | + |
| 134 | + |
| 135 | +def test_http_request_fails_before_serve_forever_without_wait(): |
| 136 | + """ |
| 137 | + Demonstrate the race condition: TCP connects but HTTP times out. |
| 138 | +
|
| 139 | + This test shows why waiting for server readiness matters: |
| 140 | + - After start(), TCP connections succeed (queued in backlog) |
| 141 | + - But HTTP requests timeout because serve_forever() hasn't started |
| 142 | + - With short client timeouts (common in production), this causes failures |
| 143 | + """ |
| 144 | + # Use startup_timeout=0 to NOT wait for ready event (old behavior) |
| 145 | + server = SlowServeServer(host="localhost", port=0, startup_timeout=0.0) |
| 146 | + server.expect_request("/ping").respond_with_data("pong") |
| 147 | + |
| 148 | + with pytest.warns(UserWarning, match="ready event was not set"): |
| 149 | + server.start() |
| 150 | + |
| 151 | + try: |
| 152 | + # TCP connection succeeds (proves Zsolt's point about backlog) |
| 153 | + sock = socket.create_connection((server.host, server.port), timeout=1) |
| 154 | + sock.close() |
| 155 | + |
| 156 | + # But HTTP request with short timeout fails! |
| 157 | + # This is the actual problem in containerized environments |
| 158 | + with pytest.raises(Timeout): |
| 159 | + requests.get(server.url_for("/ping"), timeout=(0.5, 0.5)) |
| 160 | + finally: |
| 161 | + server.stop() |
| 162 | + |
| 163 | + |
| 164 | +def test_http_request_succeeds_when_waiting_for_ready(): |
| 165 | + """ |
| 166 | + Demonstrate that waiting for ready event fixes the race condition. |
| 167 | +
|
| 168 | + With startup_timeout enabled (default), start() waits until |
| 169 | + serve_forever() begins, so HTTP requests succeed immediately. |
| 170 | + """ |
| 171 | + # Use default startup_timeout to wait for ready event |
| 172 | + server = SlowServeServer(host="localhost", port=0) # default startup_timeout=10.0 |
| 173 | + server.expect_request("/ping").respond_with_data("pong") |
| 174 | + |
| 175 | + start_time = time.monotonic() |
| 176 | + server.start() |
| 177 | + elapsed = time.monotonic() - start_time |
| 178 | + |
| 179 | + try: |
| 180 | + # Should have waited for the slow startup |
| 181 | + assert elapsed >= 3.0, f"Expected to wait >= 3.0s, but only waited {elapsed}s" |
| 182 | + |
| 183 | + # HTTP request succeeds because serve_forever() has started |
| 184 | + response = requests.get(server.url_for("/ping"), timeout=(0.5, 0.5)) |
| 185 | + assert response.status_code == 200 |
| 186 | + assert response.text == "pong" |
| 187 | + finally: |
| 188 | + server.stop() |
0 commit comments