Skip to content

Commit 9d6cc2f

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 58966a2 commit 9d6cc2f

1 file changed

Lines changed: 41 additions & 16 deletions

File tree

test/http_server_http1_tests.jl

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,31 +52,56 @@ 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+
return _read_until_quiet(
91+
sock;
92+
timeout_s = max(2.0, settle_s + 1.0),
93+
quiet_timeout_s = min(0.25, max(0.05, settle_s)),
94+
wait_for_first_byte = wait_for_first_byte,
95+
)
96+
catch err
97+
_is_first_byte_timeout(err) || rethrow(err)
98+
attempt < attempts || error("timed out waiting for first response byte ($(attempts) attempts)")
99+
@warn "raw probe got no first byte; re-dialing — known Windows IOCP wake strand (Reseau#107)" attempt port
100+
finally
101+
NC.close(sock)
70102
end
71-
return _read_until_quiet(
72-
sock;
73-
timeout_s = max(2.0, settle_s + 1.0),
74-
quiet_timeout_s = min(0.25, max(0.05, settle_s)),
75-
wait_for_first_byte = wait_for_first_byte,
76-
)
77-
finally
78-
NC.close(sock)
79103
end
104+
error("unreachable: raw probe retry loop exited")
80105
end
81106
end
82107

@@ -165,7 +190,7 @@ function _read_until_quiet(
165190
end
166191
end
167192
if wait_for_first_byte && !saw_bytes
168-
error("timed out waiting for first response byte")
193+
throw(_FirstByteTimeout())
169194
end
170195
return String(out)
171196
end

0 commit comments

Comments
 (0)