Skip to content

Commit 04a21cf

Browse files
authored
Harden flaky transport tests (#1323)
1 parent 50b121c commit 04a21cf

2 files changed

Lines changed: 61 additions & 22 deletions

File tree

test/http_client_transport_tests.jl

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,8 @@ end
802802
address = ND.join_host_port("127.0.0.1", Int(laddr.port))
803803
close_count = Ref(0)
804804
stage = Ref(1)
805+
second_chunk_started = Channel{Nothing}(1)
806+
release_second_chunk = Channel{Nothing}(1)
805807
callback_body = HT.CallbackBody(
806808
dst -> begin
807809
if stage[] == 1
@@ -811,7 +813,8 @@ end
811813
return length(bytes)
812814
end
813815
if stage[] == 2
814-
sleep(1.0)
816+
isready(second_chunk_started) || put!(second_chunk_started, nothing)
817+
take!(release_second_chunk)
815818
bytes = collect(codeunits("world"))
816819
copyto!(dst, 1, bytes, 1, length(bytes))
817820
stage[] = 3
@@ -837,15 +840,26 @@ end
837840
transport = HT.Transport(max_idle_per_host = 4, max_idle_total = 4)
838841
try
839842
req = HT.Request("POST", "/early"; host = address, body = callback_body, content_length = 10)
840-
started = time()
841-
res = HT.roundtrip!(transport, address, req)
842-
elapsed = time() - started
843-
@test res.status == 200
844-
@test String(_read_all_transport_body_bytes(res.body)) == "early"
845-
@test elapsed < 0.75
846-
@test timedwait(() -> close_count[] == 1, 2.0; pollint = 0.001) != :timed_out
843+
res_task = errormonitor(Threads.@spawn HT.roundtrip!(transport, address, req))
844+
ready = timedwait(() -> istaskdone(res_task) || isready(second_chunk_started), 2.0; pollint = 0.001)
845+
@test ready == :ok
846+
if ready == :ok && isready(second_chunk_started) && !istaskdone(res_task)
847+
response_ready = timedwait(() -> istaskdone(res_task), 2.0; pollint = 0.001)
848+
@test response_ready == :ok
849+
end
850+
@test istaskdone(res_task)
851+
isready(release_second_chunk) || put!(release_second_chunk, nothing)
852+
if istaskdone(res_task)
853+
res = fetch(res_task)
854+
@test res.status == 200
855+
@test String(_read_all_transport_body_bytes(res.body)) == "early"
856+
@test timedwait(() -> close_count[] == 1, 2.0; pollint = 0.001) != :timed_out
857+
else
858+
_wait_task!(res_task)
859+
end
847860
_wait_task!(server_task)
848861
finally
862+
isready(release_second_chunk) || put!(release_second_chunk, nothing)
849863
close(transport)
850864
HTTP.@try_ignore NC.close(listener)
851865
end

test/http_server_http1_tests.jl

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ function _raw_http_request(
9999
catch err
100100
_is_first_byte_timeout(err) || rethrow(err)
101101
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
102+
@warn "raw probe got no first byte; re-dialing after known Windows IOCP wake strand (Reseau#107)" attempt port
103103
finally
104104
NC.close(sock)
105105
end
@@ -108,15 +108,28 @@ function _raw_http_request(
108108
end
109109
end
110110

111-
function _raw_http_request_until_close(port::Integer, request::AbstractString; timeout_s::Float64 = 3.0)::Tuple{String, Bool}
112-
return _run_with_timeout(; timeout_s = timeout_s + 4.0, label = "raw http request until close (port $(port))") do
113-
sock = ND.connect("tcp", "127.0.0.1:$(Int(port))")
114-
try
115-
write(sock, Vector{UInt8}(codeunits(String(request))))
116-
return _read_until_close(sock; timeout_s)
117-
finally
118-
NC.close(sock)
111+
function _raw_http_request_until_close(
112+
port::Integer,
113+
request::AbstractString;
114+
timeout_s::Float64 = 3.0,
115+
wait_for_first_byte::Bool = true,
116+
attempts::Int = Sys.iswindows() ? 3 : 1,
117+
)::Tuple{String, Bool}
118+
return _run_with_timeout(; timeout_s = (timeout_s + 4.0) * attempts, label = "raw http request until close (port $(port))") do
119+
for attempt in 1:attempts
120+
sock = ND.connect("tcp", "127.0.0.1:$(Int(port))")
121+
try
122+
write(sock, Vector{UInt8}(codeunits(String(request))))
123+
return _read_until_close(sock; timeout_s, wait_for_first_byte)
124+
catch err
125+
_is_first_byte_timeout(err) || rethrow(err)
126+
attempt < attempts || error("timed out waiting for first response byte before close ($(attempts) attempts)")
127+
@warn "raw probe got no first byte before close; re-dialing after known Windows IOCP wake strand (Reseau#107)" attempt port
128+
finally
129+
NC.close(sock)
130+
end
119131
end
132+
error("unreachable: raw probe retry loop exited")
120133
end
121134
end
122135

@@ -199,23 +212,34 @@ function _read_until_quiet(
199212
end
200213
end
201214

202-
function _read_until_close(conn::NC.Conn; timeout_s::Float64 = 1.0)::Tuple{String, Bool}
215+
function _read_until_close(conn::NC.Conn; timeout_s::Float64 = 1.0, wait_for_first_byte::Bool = false)::Tuple{String, Bool}
203216
buf = Vector{UInt8}(undef, 1024)
204217
out = UInt8[]
205218
deadline_ns = Int64(time_ns()) + round(Int64, timeout_s * 1.0e9)
219+
saw_bytes = false
206220
while true
207221
remaining_ns = deadline_ns - Int64(time_ns())
208-
remaining_ns <= 0 && return String(out), false
222+
if remaining_ns <= 0
223+
wait_for_first_byte && !saw_bytes && throw(_FirstByteTimeout())
224+
return String(out), false
225+
end
209226
NC.set_read_deadline!(conn, Int64(time_ns()) + remaining_ns)
210227
try
211228
chunk = readavailable(conn)
212229
n = length(chunk)
213-
n == 0 && return String(out), true
230+
if n == 0
231+
wait_for_first_byte && !saw_bytes && throw(_FirstByteTimeout())
232+
return String(out), true
233+
end
214234
n > length(buf) && resize!(buf, n)
215235
copyto!(buf, 1, chunk, 1, n)
216236
append!(out, @view(buf[1:n]))
237+
saw_bytes = true
217238
catch err
218-
err isa IOP.DeadlineExceededError && return String(out), false
239+
if err isa IOP.DeadlineExceededError
240+
wait_for_first_byte && !saw_bytes && throw(_FirstByteTimeout())
241+
return String(out), false
242+
end
219243
(err isa EOFError || HT._is_peer_close_error(err::Exception)) && return String(out), true
220244
rethrow(err)
221245
end
@@ -1037,7 +1061,8 @@ end
10371061
sock = ND.connect("tcp", "127.0.0.1:$(HT.port(server))")
10381062
try
10391063
write(sock, Vector{UInt8}(codeunits("GET /one HTTP/1.1\r\nHost: $(address)\r\n\r\n")))
1040-
first = _read_until_quiet(sock; timeout_s = 2.0, quiet_timeout_s = 0.1)
1064+
first_timeout_s = Sys.iswindows() ? 5.0 : 2.0
1065+
first = _read_until_quiet(sock; timeout_s = first_timeout_s, quiet_timeout_s = 0.1, wait_for_first_byte = true)
10411066
@test occursin("HTTP/1.1 200 OK", first)
10421067
sleep(1.0)
10431068
closed_after_idle = false

0 commit comments

Comments
 (0)