mirror of
https://github.com/nim-lang/Nim.git
synced 2026-08-04 14:38:38 +00:00
Should fix https://github.com/nim-lang/Nim/blob/devel/tests/async/tasyncclosestall.nim (the flaky one) in CI. Previously CI was somehow slow enough to race on completion through multiple callback layers. Also increased the message size to hopefully fill the socket's buffer quicker
121 lines
4.3 KiB
Nim
121 lines
4.3 KiB
Nim
discard """
|
|
disabled: "windows"
|
|
outputsub: "send has errored. As expected. All good!"
|
|
exitcode: 0
|
|
"""
|
|
import asyncdispatch, asyncnet
|
|
import std/strutils
|
|
|
|
when defined(windows):
|
|
from winlean import ERROR_NETNAME_DELETED
|
|
else:
|
|
from posix import EBADF, ECONNRESET, EPIPE
|
|
|
|
# This reproduces a case where a socket remains stuck waiting for writes
|
|
# even when the socket is closed.
|
|
const
|
|
timeout = 2000
|
|
messagePaddingSize = 64 * 1024
|
|
var port = Port(0)
|
|
|
|
var sent = 0
|
|
|
|
proc isExpectedDisconnectionError(errCode: int32): bool =
|
|
## Check if an error code indicates an expected disconnection.
|
|
## On POSIX systems, the error code depends on timing:
|
|
## - EBADF: Socket was closed locally before kernel detected remote state
|
|
## - ECONNRESET: Remote peer sent RST packet (detected first)
|
|
## - EPIPE: Socket is no longer connected (broken pipe)
|
|
## All three are valid disconnection errors for this test scenario.
|
|
when defined(windows):
|
|
errCode == ERROR_NETNAME_DELETED
|
|
else:
|
|
errCode == EBADF or errCode == ECONNRESET or errCode == EPIPE
|
|
|
|
proc keepSendingTo(c: AsyncSocket) {.async.} =
|
|
let messagePadding = repeat('x', messagePaddingSize)
|
|
while true:
|
|
# Larger writes reach socket backpressure quickly even on slow CI machines.
|
|
# This write will eventually get stuck because the client is not reading.
|
|
# Keep the padding after the newline so recvLine does not drain it.
|
|
let sendFut = c.send("Foobar" & $sent & "\n" & messagePadding, flags = {})
|
|
var sendTimedOut = false
|
|
try:
|
|
# On some platforms (notably macOS ARM64), the kernel may return
|
|
# ECONNRESET immediately when detecting a non-responsive connection,
|
|
# rather than letting the send stall. We catch this case here.
|
|
sendTimedOut = not await withTimeout(sendFut, timeout)
|
|
except OSError as e:
|
|
if isExpectedDisconnectionError(e.errorCode):
|
|
echo("send has errored. As expected. All good!")
|
|
quit(QuitSuccess)
|
|
else:
|
|
raise
|
|
|
|
if sendTimedOut:
|
|
# The write is stuck. Let's simulate a scenario where the socket
|
|
# does not respond to PING messages, and we close it. The above future
|
|
# should complete after the socket is closed, not continue stalling.
|
|
echo("Socket has stalled, closing it")
|
|
c.close()
|
|
|
|
let timeoutFut = withTimeout(sendFut, timeout)
|
|
yield timeoutFut
|
|
if timeoutFut.failed:
|
|
let errCode = ((ref OSError)(timeoutFut.error)).errorCode
|
|
# The behaviour differs across platforms. On Windows ERROR_NETNAME_DELETED
|
|
# is raised which we classif as a "diconnection error", hence we overwrite
|
|
# the flags above in the `send` call so that this error is raised.
|
|
#
|
|
# This means that by default the behaviours will differ between Windows
|
|
# and POSIX. I think this is fine though, it makes sense mainly because
|
|
# Windows doesn't use a IO readiness model. We can fix this later if
|
|
# necessary to reclassify ERROR_NETNAME_DELETED as not a "disconnection
|
|
# error" (TODO)
|
|
if isExpectedDisconnectionError(errCode):
|
|
echo("send has errored. As expected. All good!")
|
|
quit(QuitSuccess)
|
|
else:
|
|
raise newException(ValueError, "Test failed. Send failed with code " & $errCode)
|
|
|
|
# The write shouldn't succeed and also shouldn't be stalled.
|
|
if timeoutFut.read():
|
|
raise newException(ValueError, "Test failed. Send was expected to fail.")
|
|
else:
|
|
raise newException(ValueError, "Test failed. Send future is still stalled.")
|
|
sent.inc(1)
|
|
|
|
proc startClient() {.async.} =
|
|
let client = newAsyncSocket()
|
|
await client.connect("localhost", port)
|
|
echo("Connected")
|
|
|
|
let firstLine = await client.recvLine()
|
|
echo("Received first line as a client: ", firstLine)
|
|
echo("Now not reading anymore")
|
|
while true: await sleepAsync(1000)
|
|
|
|
proc debug() {.async.} =
|
|
while true:
|
|
echo("Sent ", sent)
|
|
await sleepAsync(1000)
|
|
|
|
proc server() {.async.} =
|
|
var s = newAsyncSocket()
|
|
s.setSockOpt(OptReuseAddr, true)
|
|
s.bindAddr(port)
|
|
s.listen()
|
|
let (addr2, port2) = s.getLocalAddr
|
|
port = port2
|
|
|
|
# We're now ready to accept connections, so start the client
|
|
asyncCheck startClient()
|
|
asyncCheck debug()
|
|
|
|
while true:
|
|
let client = await accept(s)
|
|
asyncCheck keepSendingTo(client)
|
|
|
|
when isMainModule:
|
|
waitFor server()
|