1111
1212import std/ [atomics, json, jsonutils, net, os, sequtils, strutils,
1313 unittest]
14+ from std/ nativesockets import selectRead
1415from std/ times import epochTime
1516when defined (posix):
1617 from std/ posix import Timeval , Time , Suseconds , SockLen , SOL_SOCKET ,
@@ -196,13 +197,34 @@ proc setSocketTimeoutMs(sock: Socket; ms: int) =
196197proc acceptWithinDeadline (server: SseServer ; client: var Socket ): bool =
197198 # # Accept with a deadline. Returns false when no client dialed in time,
198199 # # letting serve threads (and their joiners) terminate instead of hanging
199- # # on an accept that will never come.
200+ # # on an accept that will never come. Waits via selectRead, not
201+ # # SO_RCVTIMEO on the listening socket: accept(2) ignores that option on
202+ # # macOS, so a timeout-wrapped accept blocks forever there and the
203+ # # joiner hangs (observed as a deterministic 300s CI kill on macos-14).
200204 while epochTime () < server.acceptDeadline:
205+ var fds = @ [server.socket.getFd]
201206 try :
207+ if selectRead (fds, 200 ) == 0 :
208+ continue # No pending connection yet: re-check the deadline.
202209 server.socket.accept (client)
203210 return true
204211 except OSError :
205- discard # SO_RCVTIMEO wake: loop and re-check the deadline.
212+ discard
213+ false
214+
215+ proc acceptUntilDone (server: SseServer ; client: var Socket ): bool =
216+ # # acceptWithinDeadline, plus a stop flag the caller flips before
217+ # # joinThread: a serve loop that would otherwise park in accept until
218+ # # the deadline joins within one 200ms selectRead window.
219+ while not server.done.load (moAcquire) and epochTime () < server.acceptDeadline:
220+ var fds = @ [server.socket.getFd]
221+ try :
222+ if selectRead (fds, 200 ) == 0 :
223+ continue
224+ server.socket.accept (client)
225+ return true
226+ except OSError :
227+ discard
206228 false
207229
208230proc newSseServer (response: string ): SseServer =
@@ -279,25 +301,16 @@ proc logDiag(msg: string) =
279301 stderr.writeLine (" [diag " , epochTime ().formatFloat (ffDecimal, 3 ), " ] " , msg)
280302
281303proc serveRetryable (server: SseServer ) {.thread .} =
282- logDiag " retryable server up port " & $ server.port.uint16
283304 # # Like serveThread, but serves every connection until the test flips
284305 # # `server.done` or the accept deadline passes. The transport re-dials
285306 # # once when a first attempt dies on a socket error (the RST race
286307 # # documented at drainRequestBody); against a one-shot server that
287308 # # retry then waits out the whole quiet window for a head the dead
288309 # # server can never send — the 300s CI watchdog kills. Serving the
289310 # # re-dial turns that flake into a slightly slower green run.
290- var wakes = 0
291- while not server.done.load (moAcquire) and epochTime () < server.acceptDeadline:
311+ while true :
292312 var client: Socket
293- try :
294- server.socket.accept (client)
295- except OSError :
296- inc wakes
297- if wakes <= 3 or wakes mod 100 == 0 :
298- logDiag " accept wake " & $ wakes
299- continue # 200ms SO_RCVTIMEO wake: re-check done and the deadline.
300- logDiag " accepted conn " & $ wakes
313+ if not server.acceptUntilDone (client): return
301314 let contentLength = client.readRequestHead ()
302315 let body = server.response
303316 let resp = " HTTP/1.1 200 OK\r\n Content-Type: text/event-stream\r\n Transfer-Encoding: chunked\r\n Connection: close\r\n\r\n "
@@ -307,7 +320,6 @@ proc serveRetryable(server: SseServer) {.thread.} =
307320 client.send (" 0\r\n\r\n " )
308321 client.drainRequestBody (contentLength)
309322 client.close ()
310- logDiag " served conn " & $ wakes
311323
312324proc url (server: SseServer ): string =
313325 # Bare endpoint like production provider urls; the transport appends
0 commit comments