[bugfix] Fixes async IO operations stalling even after socket is closed. (#11232)
This commit is contained in:
parent
da035e9c83
commit
73c44c6e9b
4 changed files with 192 additions and 53 deletions
99
tests/async/tasyncclosestall.nim
Normal file
99
tests/async/tasyncclosestall.nim
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
discard """
|
||||
outputsub: "send has errored. As expected. All good!"
|
||||
exitcode: 0
|
||||
"""
|
||||
import asyncdispatch, asyncnet
|
||||
|
||||
when defined(windows):
|
||||
from winlean import ERROR_NETNAME_DELETED
|
||||
else:
|
||||
from posix import EBADF
|
||||
|
||||
# This reproduces a case where a socket remains stuck waiting for writes
|
||||
# even when the socket is closed.
|
||||
const
|
||||
port = Port(50726)
|
||||
timeout = 5000
|
||||
|
||||
var sent = 0
|
||||
|
||||
proc keepSendingTo(c: AsyncSocket) {.async.} =
|
||||
while true:
|
||||
# This write will eventually get stuck because the client is not reading
|
||||
# its messages.
|
||||
let sendFut = c.send("Foobar" & $sent & "\n", flags = {})
|
||||
if not await withTimeout(sendFut, timeout):
|
||||
# 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.
|
||||
#
|
||||
# On Linux the EBADF error code is raised, this is because the socket
|
||||
# is closed.
|
||||
#
|
||||
# This means that by default the behaviours will differ between Windows
|
||||
# and Linux. 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)
|
||||
when defined(windows):
|
||||
if errCode == ERROR_NETNAME_DELETED:
|
||||
echo("send has errored. As expected. All good!")
|
||||
quit(QuitSuccess)
|
||||
else:
|
||||
raise newException(ValueError, "Test failed. Send failed with code " & $errCode)
|
||||
else:
|
||||
if errCode == EBADF:
|
||||
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()
|
||||
|
||||
# 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()
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue