Skip to content

Commit a718a79

Browse files
quinnjclaude
andcommitted
test(server): retry raw probes that get no first byte on Windows
The raw HTTP/1 probe helper fails with "timed out waiting for first response byte" when the known Windows IOCP wake strand (JuliaServices/Reseau.jl#107) leaves the probe's parked reader unwoken — the same canary test has now struck four unrelated PRs. Until the Reseau fix lands, re-dial fresh and retry (3 attempts, Windows only; other platforms stay strict) with a loud warning so the strand remains visible in logs. Safe for all current callers: none assert on server-side invocation counts. The first-byte timeout is now a typed exception so the retry can unwrap it through the nested watchdog wrappers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3771505 commit a718a79

1 file changed

Lines changed: 44 additions & 17 deletions

File tree

test/http_server_http1_tests.jl

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,32 +52,59 @@ end
5252
# indefinitely. Wrap the whole exchange in `_run_with_timeout`, a task-level
5353
# watchdog that does not depend on socket deadlines, so a stuck read fails the
5454
# test in seconds instead of hanging the job until CI's wall-clock limit.
55+
# Raised when the raw probe saw no response bytes at all within its budget.
56+
# On Windows this is the known IOCP wake strand (JuliaServices/Reseau.jl#107):
57+
# the parked reader is intermittently never woken even though bytes arrive.
58+
struct _FirstByteTimeout <: Exception end
59+
60+
# `_read_until_quiet` runs under its own `_run_with_timeout` watchdog, so the
61+
# timeout surfaces here wrapped in (possibly nested) TaskFailedException.
62+
function _is_first_byte_timeout(err)::Bool
63+
err isa _FirstByteTimeout && return true
64+
err isa TaskFailedException || return false
65+
inner = err.task.exception
66+
return inner !== nothing && _is_first_byte_timeout(inner)
67+
end
68+
5569
function _raw_http_request(
5670
port::Integer,
5771
request::AbstractString;
5872
settle_s::Float64 = 0.5,
5973
close_write::Bool = true,
6074
wait_for_first_byte::Bool = true,
75+
# Re-dial fresh and retry when no first byte arrives. Windows-only by
76+
# default so other platforms stay strict; safe for these probes because no
77+
# caller asserts on server-side invocation counts.
78+
attempts::Int = Sys.iswindows() ? 3 : 1,
6179
)::String
62-
return _run_with_timeout(; timeout_s = max(8.0, settle_s + 4.0), label = "raw http request (port $(port))") do
63-
sock = ND.connect("tcp", "127.0.0.1:$(Int(port))")
64-
try
65-
write(sock, Vector{UInt8}(codeunits(String(request))))
66-
if close_write
67-
HT.@try_ignore begin
68-
NC.closewrite(sock)
80+
return _run_with_timeout(; timeout_s = max(8.0, settle_s + 4.0) * attempts, label = "raw http request (port $(port))") do
81+
for attempt in 1:attempts
82+
sock = ND.connect("tcp", "127.0.0.1:$(Int(port))")
83+
try
84+
write(sock, Vector{UInt8}(codeunits(String(request))))
85+
if close_write
86+
HT.@try_ignore begin
87+
NC.closewrite(sock)
88+
end
6989
end
90+
# master hardening kept: longer first-byte budget on Windows,
91+
# combined with the re-dial retry below
92+
first_byte_timeout_s = max(Sys.iswindows() ? 5.0 : 2.0, settle_s + 1.0)
93+
return _read_until_quiet(
94+
sock;
95+
timeout_s = first_byte_timeout_s,
96+
quiet_timeout_s = min(0.25, max(0.05, settle_s)),
97+
wait_for_first_byte = wait_for_first_byte,
98+
)
99+
catch err
100+
_is_first_byte_timeout(err) || rethrow(err)
101+
attempt < attempts || error("timed out waiting for first response byte ($(attempts) attempts)")
102+
@warn "raw probe got no first byte; re-dialing — known Windows IOCP wake strand (Reseau#107)" attempt port
103+
finally
104+
NC.close(sock)
70105
end
71-
first_byte_timeout_s = max(Sys.iswindows() ? 5.0 : 2.0, settle_s + 1.0)
72-
return _read_until_quiet(
73-
sock;
74-
timeout_s = first_byte_timeout_s,
75-
quiet_timeout_s = min(0.25, max(0.05, settle_s)),
76-
wait_for_first_byte = wait_for_first_byte,
77-
)
78-
finally
79-
NC.close(sock)
80106
end
107+
error("unreachable: raw probe retry loop exited")
81108
end
82109
end
83110

@@ -166,7 +193,7 @@ function _read_until_quiet(
166193
end
167194
end
168195
if wait_for_first_byte && !saw_bytes
169-
error("timed out waiting for first response byte")
196+
throw(_FirstByteTimeout())
170197
end
171198
return String(out)
172199
end

0 commit comments

Comments
 (0)