Skip to content

Commit 071bfc2

Browse files
rustyconoverclaude
andcommitted
fix(test): drain HTTP fixture server pipes to fix Windows ReadTimeout
run_example_http_server spawned the fixture worker with stdout/stderr as PIPEs that were never drained. The server logs a line per RPC; once the (small) Windows OS pipe buffer fills, the server blocks on write() to stderr, freezing waitress's single asyncore I/O loop — so every later request hangs with httpx.ReadTimeout. POSIX hid this behind its 64 KiB pipe buffers. Drain both pipes in background threads (stderr buffered so the exit-code error path still surfaces it). Verified on real Windows (Python 3.14, vgi-rpc 0.20.0): the resumable-scan file goes from a 63s timeout to 1.5s, 6 passed. With the root cause fixed, un-skip test_continue_on_fresh_client_skips_rebind on win32 — its "tracked for investigation" skip was this same bug — and drop the now-unused sys import. Also gitignore local scratch artifacts (test.json, client-tests/, docs/time-travel-init-plan.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4e3a583 commit 071bfc2

3 files changed

Lines changed: 33 additions & 9 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,8 @@ test-data/v.parquet
4242

4343
# Cloudflare Worker scaffolding
4444
cloudflare/vgi-storage/node_modules/
45+
46+
# Local scratch / experiment artifacts (not part of the package)
47+
/test.json
48+
/client-tests/
49+
/docs/time-travel-init-plan.md

tests/_http_fixtures.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import socket
1313
import subprocess
1414
import sys
15+
import threading
1516
import time
1617
from collections.abc import Iterator, Sequence
1718
from contextlib import ExitStack, contextmanager
19+
from io import StringIO
1820

1921

2022
def free_port() -> int:
@@ -56,6 +58,29 @@ def run_example_http_server(
5658
stderr=subprocess.PIPE,
5759
text=True,
5860
)
61+
62+
# Continuously drain stdout/stderr in background threads. The server logs
63+
# one or more lines per RPC; left undrained, a chatty run fills the OS pipe
64+
# buffer and the server blocks on write() to stderr — freezing waitress's
65+
# single I/O loop so every subsequent request hangs (ReadTimeout). Windows
66+
# pipe buffers are small enough to hit this within one test file; POSIX's
67+
# 64 KiB buffers usually hide it. Buffer stderr so the exit-code check below
68+
# can still surface it.
69+
captured_stderr = StringIO()
70+
71+
def _drain(pipe: object, sink: StringIO | None) -> None:
72+
assert pipe is not None
73+
for line in pipe: # type: ignore[attr-defined]
74+
if sink is not None:
75+
sink.write(line)
76+
77+
drain_threads = [
78+
threading.Thread(target=_drain, args=(proc.stdout, None), daemon=True),
79+
threading.Thread(target=_drain, args=(proc.stderr, captured_stderr), daemon=True),
80+
]
81+
for t in drain_threads:
82+
t.start()
83+
5984
try:
6085
yield
6186
finally:
@@ -65,13 +90,14 @@ def run_example_http_server(
6590
except subprocess.TimeoutExpired:
6691
proc.kill()
6792
proc.wait(timeout=5)
93+
for t in drain_threads:
94+
t.join(timeout=5)
6895

6996
# POSIX terminate() -> SIGTERM (-15); Windows terminate() is
7097
# TerminateProcess(handle, 1), so a cleanly-stopped worker reports 1.
7198
ok_codes = (0, 1) if sys.platform == "win32" else (0, -15)
7299
if proc.returncode not in ok_codes:
73-
stderr = proc.stderr.read() if proc.stderr is not None else ""
74-
raise RuntimeError(f"example HTTP worker exited with code {proc.returncode}: {stderr}")
100+
raise RuntimeError(f"example HTTP worker exited with code {proc.returncode}: {captured_stderr.getvalue()}")
75101

76102

77103
def start_http_worker(

tests/conformance/test_resumable_scan.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
from __future__ import annotations
1212

13-
import sys
1413
from collections.abc import Iterator
1514

1615
import pyarrow as pa
@@ -103,12 +102,6 @@ def test_resume_on_fresh_client_after_node_hop(http_base_url: str) -> None:
103102
assert rows == expected # contiguous, complete, no duplicates
104103

105104

106-
@pytest.mark.skipif(
107-
sys.platform == "win32",
108-
reason="deterministic ReadTimeout on Windows CI after the preceding "
109-
"resumable scans — suspected fixture-server worker degradation; "
110-
"tracked for investigation (passes on every POSIX platform)",
111-
)
112105
def test_continue_on_fresh_client_skips_rebind(http_base_url: str) -> None:
113106
"""``table_scan_continue`` resumes from the token alone — no bind/init round-trip.
114107

0 commit comments

Comments
 (0)