Cancellation fixes and tests. (#445)

* Add callTick and stream cancellation tests.

* Fix stepsAsync() test.

* Cancellation changes.

* Update and add more cancellation tests.

* Fix Posix shutdown call to handle ENOTCONN error.

* With new changes to to cancellation its now possible.

* Refactor testsoon.nim to not produce artifacts after tests are finished.

* Debugging MacOS issue.

* Adjust flaky test times.

* Fix issue.

* Add test for issue #334 which was also addressed in this PR.
Avoid `break` in problematic test.

* Add noCancelWait() call which prohibits cancellation.
Fix closeWait() calls to use noCancelWait() predicate.
Adding sleep to flaky MacOS test.

* Remove all debugging echoes.

* Fix cancelAndWait() which now could perform multiple attempts to cancel target Future (mustCancel behavior).

* Fix issues revealed by switch to different cancelAndWait().

* Address review comments.

* Fix testutils compilation warning.

* Rename callTick() to internalCallTick().

* Add some documentation comments.

* Disable flaky ratelimit test.

* Rename noCancelWait() to noCancel().
Address review comments.
This commit is contained in:
Eugene Kabanov 2023-09-15 19:38:39 +03:00 committed by GitHub
parent 00614476c6
commit 2e8551b0d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1243 additions and 369 deletions

View File

@ -45,8 +45,8 @@ proc closeWait*(bstream: HttpBodyReader) {.async.} =
# data from stream at position [1].
for index in countdown((len(bstream.streams) - 1), 0):
res.add(bstream.streams[index].closeWait())
await allFutures(res)
await procCall(closeWait(AsyncStreamReader(bstream)))
res.add(procCall(closeWait(AsyncStreamReader(bstream))))
await noCancel(allFutures(res))
bstream.bstate = HttpState.Closed
untrackCounter(HttpBodyReaderTrackerName)
@ -68,7 +68,7 @@ proc closeWait*(bstream: HttpBodyWriter) {.async.} =
var res = newSeq[Future[void]]()
for index in countdown(len(bstream.streams) - 1, 0):
res.add(bstream.streams[index].closeWait())
await allFutures(res)
await noCancel(allFutures(res))
await procCall(closeWait(AsyncStreamWriter(bstream)))
bstream.bstate = HttpState.Closed
untrackCounter(HttpBodyWriterTrackerName)

View File

@ -600,14 +600,12 @@ proc closeWait(conn: HttpClientConnectionRef) {.async.} =
res.add(conn.reader.closeWait())
if not(isNil(conn.writer)) and not(conn.writer.closed()):
res.add(conn.writer.closeWait())
if conn.kind == HttpClientScheme.Secure:
res.add(conn.treader.closeWait())
res.add(conn.twriter.closeWait())
res.add(conn.transp.closeWait())
res
if len(pending) > 0: await allFutures(pending)
case conn.kind
of HttpClientScheme.Secure:
await allFutures(conn.treader.closeWait(), conn.twriter.closeWait())
of HttpClientScheme.NonSecure:
discard
await conn.transp.closeWait()
if len(pending) > 0: await noCancel(allFutures(pending))
conn.state = HttpClientConnectionState.Closed
untrackCounter(HttpClientConnectionTrackerName)
@ -631,8 +629,7 @@ proc connect(session: HttpSessionRef,
let conn =
block:
let res = HttpClientConnectionRef.new(session, ha, transp)
case res.kind
of HttpClientScheme.Secure:
if res.kind == HttpClientScheme.Secure:
try:
await res.tls.handshake()
res.state = HttpClientConnectionState.Ready
@ -647,7 +644,7 @@ proc connect(session: HttpSessionRef,
await res.closeWait()
res.state = HttpClientConnectionState.Error
lastError = $exc.msg
of HttpClientScheme.Nonsecure:
else:
res.state = HttpClientConnectionState.Ready
res
if conn.state == HttpClientConnectionState.Ready:
@ -785,7 +782,7 @@ proc closeWait*(session: HttpSessionRef) {.async.} =
for connections in session.connections.values():
for conn in connections:
pending.add(closeWait(conn))
await allFutures(pending)
await noCancel(allFutures(pending))
proc sessionWatcher(session: HttpSessionRef) {.async.} =
while true:
@ -830,26 +827,30 @@ proc sessionWatcher(session: HttpSessionRef) {.async.} =
break
proc closeWait*(request: HttpClientRequestRef) {.async.} =
var pending: seq[FutureBase]
if request.state notin {HttpReqRespState.Closing, HttpReqRespState.Closed}:
request.state = HttpReqRespState.Closing
if not(isNil(request.writer)):
if not(request.writer.closed()):
await request.writer.closeWait()
pending.add(FutureBase(request.writer.closeWait()))
request.writer = nil
await request.releaseConnection()
pending.add(FutureBase(request.releaseConnection()))
await noCancel(allFutures(pending))
request.session = nil
request.error = nil
request.state = HttpReqRespState.Closed
untrackCounter(HttpClientRequestTrackerName)
proc closeWait*(response: HttpClientResponseRef) {.async.} =
var pending: seq[FutureBase]
if response.state notin {HttpReqRespState.Closing, HttpReqRespState.Closed}:
response.state = HttpReqRespState.Closing
if not(isNil(response.reader)):
if not(response.reader.closed()):
await response.reader.closeWait()
pending.add(FutureBase(response.reader.closeWait()))
response.reader = nil
await response.releaseConnection()
pending.add(FutureBase(response.releaseConnection()))
await noCancel(allFutures(pending))
response.session = nil
response.error = nil
response.state = HttpReqRespState.Closed

View File

@ -809,10 +809,7 @@ proc closeUnsecureConnection(conn: HttpConnectionRef) {.async.} =
pending.add(conn.mainReader.closeWait())
pending.add(conn.mainWriter.closeWait())
pending.add(conn.transp.closeWait())
try:
await allFutures(pending)
except CancelledError:
await allFutures(pending)
await noCancel(allFutures(pending))
untrackCounter(HttpServerUnsecureConnectionTrackerName)
reset(conn[])
conn.state = HttpState.Closed
@ -829,7 +826,7 @@ proc new(ht: typedesc[HttpConnectionRef], server: HttpServerRef,
res
proc gracefulCloseWait*(conn: HttpConnectionRef) {.async.} =
await conn.transp.shutdownWait()
await noCancel(conn.transp.shutdownWait())
await conn.closeCb(conn)
proc closeWait*(conn: HttpConnectionRef): Future[void] =
@ -841,11 +838,7 @@ proc closeWait*(req: HttpRequestRef) {.async.} =
req.state = HttpState.Closing
let resp = req.response.get()
if (HttpResponseFlags.Stream in resp.flags) and not(isNil(resp.writer)):
var writer = resp.writer.closeWait()
try:
await writer
except CancelledError:
await writer
await closeWait(resp.writer)
reset(resp[])
untrackCounter(HttpServerRequestTrackerName)
reset(req[])
@ -1038,7 +1031,6 @@ proc processLoop(holder: HttpConnectionHolderRef) {.async.} =
except CatchableError as exc:
raiseAssert "Unexpected error [" & $exc.name & "] happens: " & $exc.msg
server.connections.del(connectionId)
case runLoop
of HttpProcessExitType.KeepAlive:
await connection.closeWait()
@ -1047,6 +1039,8 @@ proc processLoop(holder: HttpConnectionHolderRef) {.async.} =
of HttpProcessExitType.Graceful:
await connection.gracefulCloseWait()
server.connections.del(connectionId)
proc acceptClientLoop(server: HttpServerRef) {.async.} =
var runLoop = true
while runLoop:
@ -1102,7 +1096,7 @@ proc drop*(server: HttpServerRef) {.async.} =
for holder in server.connections.values():
if not(isNil(holder.future)) and not(holder.future.finished()):
pending.add(holder.future.cancelAndWait())
await allFutures(pending)
await noCancel(allFutures(pending))
server.connections.clear()
proc closeWait*(server: HttpServerRef) {.async.} =

View File

@ -30,19 +30,10 @@ proc closeSecConnection(conn: HttpConnectionRef) {.async.} =
var pending: seq[Future[void]]
pending.add(conn.writer.closeWait())
pending.add(conn.reader.closeWait())
try:
await allFutures(pending)
except CancelledError:
await allFutures(pending)
# After we going to close everything else.
pending.setLen(3)
pending[0] = conn.mainReader.closeWait()
pending[1] = conn.mainWriter.closeWait()
pending[2] = conn.transp.closeWait()
try:
await allFutures(pending)
except CancelledError:
await allFutures(pending)
pending.add(conn.mainReader.closeWait())
pending.add(conn.mainWriter.closeWait())
pending.add(conn.transp.closeWait())
await noCancel(allFutures(pending))
reset(cast[SecureHttpConnectionRef](conn)[])
untrackCounter(HttpServerSecureConnectionTrackerName)
conn.state = HttpState.Closed

View File

@ -12,6 +12,7 @@ import std/sequtils
import stew/base10
when chronosStackTrace:
import std/strutils
when defined(nimHasStacktracesModule):
import system/stacktraces
else:
@ -26,7 +27,8 @@ template LocFinishIndex*: auto {.deprecated: "LocationKind.Finish".} =
template LocCompleteIndex*: untyped {.deprecated: "LocationKind.Finish".} =
LocationKind.Finish
func `[]`*(loc: array[LocationKind, ptr SrcLoc], v: int): ptr SrcLoc {.deprecated: "use LocationKind".} =
func `[]`*(loc: array[LocationKind, ptr SrcLoc], v: int): ptr SrcLoc {.
deprecated: "use LocationKind".} =
case v
of 0: loc[LocationKind.Create]
of 1: loc[LocationKind.Finish]
@ -43,29 +45,37 @@ type
# Backwards compatibility for old FutureState name
template Finished* {.deprecated: "Use Completed instead".} = Completed
template Finished*(T: type FutureState): FutureState {.deprecated: "Use FutureState.Completed instead".} = FutureState.Completed
template Finished*(T: type FutureState): FutureState {.
deprecated: "Use FutureState.Completed instead".} =
FutureState.Completed
proc newFutureImpl[T](loc: ptr SrcLoc): Future[T] =
let fut = Future[T]()
internalInitFutureBase(fut, loc, FutureState.Pending)
internalInitFutureBase(fut, loc, FutureState.Pending, {})
fut
proc newFutureImpl[T](loc: ptr SrcLoc, flags: FutureFlags): Future[T] =
let fut = Future[T]()
internalInitFutureBase(fut, loc, FutureState.Pending, flags)
fut
proc newFutureSeqImpl[A, B](loc: ptr SrcLoc): FutureSeq[A, B] =
let fut = FutureSeq[A, B]()
internalInitFutureBase(fut, loc, FutureState.Pending)
internalInitFutureBase(fut, loc, FutureState.Pending, {})
fut
proc newFutureStrImpl[T](loc: ptr SrcLoc): FutureStr[T] =
let fut = FutureStr[T]()
internalInitFutureBase(fut, loc, FutureState.Pending)
internalInitFutureBase(fut, loc, FutureState.Pending, {})
fut
template newFuture*[T](fromProc: static[string] = ""): Future[T] =
template newFuture*[T](fromProc: static[string] = "",
flags: static[FutureFlags] = {}): Future[T] =
## Creates a new future.
##
## Specifying ``fromProc``, which is a string specifying the name of the proc
## that this future belongs to, is a good habit as it helps with debugging.
newFutureImpl[T](getSrcLocation(fromProc))
newFutureImpl[T](getSrcLocation(fromProc), flags)
template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] =
## Create a new future which can hold/preserve GC sequence until future will
@ -132,8 +142,6 @@ proc finish(fut: FutureBase, state: FutureState) =
# 1. `finish()` is a private procedure and `state` is under our control.
# 2. `fut.state` is checked by `checkFinished()`.
fut.internalState = state
when chronosStrictFutureAccess:
doAssert fut.internalCancelcb == nil or state != FutureState.Cancelled
fut.internalCancelcb = nil # release cancellation callback memory
for item in fut.internalCallbacks.mitems():
if not(isNil(item.function)):
@ -194,21 +202,23 @@ proc cancelAndSchedule(future: FutureBase, loc: ptr SrcLoc) =
template cancelAndSchedule*(future: FutureBase) =
cancelAndSchedule(future, getSrcLocation())
proc cancel(future: FutureBase, loc: ptr SrcLoc): bool =
## Request that Future ``future`` cancel itself.
proc tryCancel(future: FutureBase, loc: ptr SrcLoc): bool =
## Perform an attempt to cancel ``future``.
##
## This arranges for a `CancelledError` to be thrown into procedure which
## waits for ``future`` on the next cycle through the event loop.
## The procedure then has a chance to clean up or even deny the request
## using `try/except/finally`.
## NOTE: This procedure does not guarantee that cancellation will actually
## happened.
##
## This call do not guarantee that the ``future`` will be cancelled: the
## exception might be caught and acted upon, delaying cancellation of the
## ``future`` or preventing cancellation completely. The ``future`` may also
## return value or raise different exception.
## Cancellation is the process which starts from the last ``future``
## descendent and moves step by step to the parent ``future``. To initiate
## this process procedure iterates through all non-finished ``future``
## descendents and tries to find the last one. If last descendent is still
## pending it will become cancelled and process will be initiated. In such
## case this procedure returns ``true``.
##
## Immediately after this procedure is called, ``future.cancelled()`` will
## not return ``true`` (unless the Future was already cancelled).
## If last descendent future is not pending, this procedure will be unable to
## initiate cancellation process and so it returns ``false``.
if future.cancelled():
return true
if future.finished():
return false
@ -217,23 +227,18 @@ proc cancel(future: FutureBase, loc: ptr SrcLoc): bool =
# mechanism and/or use a regular `addCallback`
when chronosStrictFutureAccess:
doAssert future.internalCancelcb.isNil,
"futures returned from `{.async.}` functions must not use `cancelCallback`"
if cancel(future.internalChild, getSrcLocation()):
return true
"futures returned from `{.async.}` functions must not use " &
"`cancelCallback`"
tryCancel(future.internalChild, loc)
else:
if not(isNil(future.internalCancelcb)):
future.internalCancelcb(cast[pointer](future))
future.internalCancelcb = nil
cancelAndSchedule(future, getSrcLocation())
if FutureFlag.OwnCancelSchedule notin future.internalFlags:
cancelAndSchedule(future, loc)
future.cancelled()
future.internalMustCancel = true
return true
template cancel*(future: FutureBase) =
## Cancel ``future``.
discard cancel(future, getSrcLocation())
template tryCancel*(future: FutureBase): bool =
tryCancel(future, getSrcLocation())
proc clearCallbacks(future: FutureBase) =
future.internalCallbacks = default(seq[AsyncCallback])
@ -778,27 +783,117 @@ proc oneValue*[T](futs: varargs[Future[T]]): Future[T] {.
return retFuture
proc cancelAndWait*(fut: FutureBase): Future[void] =
## Initiate cancellation process for Future ``fut`` and wait until ``fut`` is
## done e.g. changes its state (become completed, failed or cancelled).
proc cancelSoon(future: FutureBase, aftercb: CallbackFunc, udata: pointer,
loc: ptr SrcLoc) =
## Perform cancellation ``future`` and call ``aftercb`` callback when
## ``future`` become finished (completed with value, failed or cancelled).
##
## If ``fut`` is already finished (completed, failed or cancelled) result
## Future[void] object will be returned complete.
var retFuture = newFuture[void]("chronos.cancelAndWait(T)")
proc continuation(udata: pointer) =
if not(retFuture.finished()):
retFuture.complete()
proc cancellation(udata: pointer) =
if not(fut.finished()):
fut.removeCallback(continuation)
if fut.finished():
## NOTE: Compared to the `tryCancel()` call, this procedure call guarantees
## that ``future``will be finished (completed with value, failed or cancelled)
## as quickly as possible.
proc checktick(udata: pointer) {.gcsafe.} =
# We trying to cancel Future on more time, and if `cancel()` succeeds we
# return early.
if tryCancel(future, loc):
return
# Cancellation signal was not delivered, so we trying to deliver it one
# more time after one tick. But we need to check situation when child
# future was finished but our completion callback is not yet invoked.
if not(future.finished()):
internalCallTick(checktick)
proc continuation(udata: pointer) {.gcsafe.} =
# We do not use `callSoon` here because we was just scheduled from `poll()`.
if not(isNil(aftercb)):
aftercb(udata)
if future.finished():
# We could not schedule callback directly otherwise we could fall into
# recursion problem.
if not(isNil(aftercb)):
let loop = getThreadDispatcher()
loop.callbacks.addLast(AsyncCallback(function: aftercb, udata: udata))
return
future.addCallback(continuation)
# Initiate cancellation process.
if not(tryCancel(future, loc)):
# Cancellation signal was not delivered, so we trying to deliver it one
# more time after async tick. But we need to check case, when future was
# finished but our completion callback is not yet invoked.
if not(future.finished()):
internalCallTick(checktick)
template cancelSoon*(fut: FutureBase, cb: CallbackFunc, udata: pointer) =
cancelSoon(fut, cb, udata, getSrcLocation())
template cancelSoon*(fut: FutureBase, cb: CallbackFunc) =
cancelSoon(fut, cb, nil, getSrcLocation())
template cancelSoon*(fut: FutureBase, acb: AsyncCallback) =
cancelSoon(fut, acb.function, acb.udata, getSrcLocation())
template cancelSoon*(fut: FutureBase) =
cancelSoon(fut, nil, nil, getSrcLocation())
template cancel*(future: FutureBase) {.
deprecated: "Please use cancelSoon() or cancelAndWait() instead".} =
## Cancel ``future``.
cancelSoon(future, nil, nil, getSrcLocation())
proc cancelAndWait*(future: FutureBase, loc: ptr SrcLoc): Future[void] =
## Perform cancellation ``future`` return Future which will be completed when
## ``future`` become finished (completed with value, failed or cancelled).
##
## NOTE: Compared to the `tryCancel()` call, this procedure call guarantees
## that ``future``will be finished (completed with value, failed or cancelled)
## as quickly as possible.
let retFuture = newFuture[void]("chronos.cancelAndWait(FutureBase)",
{FutureFlag.OwnCancelSchedule})
proc continuation(udata: pointer) {.gcsafe.} =
retFuture.complete()
if future.finished():
retFuture.complete()
else:
fut.addCallback(continuation)
retFuture.cancelCallback = cancellation
# Initiate cancellation process.
fut.cancel()
return retFuture
cancelSoon(future, continuation, cast[pointer](retFuture), loc)
retFuture
template cancelAndWait*(future: FutureBase): Future[void] =
## Cancel ``future``.
cancelAndWait(future, getSrcLocation())
proc noCancel*[T](future: Future[T]): Future[T] =
## Prevent cancellation requests from propagating to ``future`` while
## forwarding its value or error when it finishes.
##
## This procedure should be used when you need to perform operations which
## should not be cancelled at all cost, for example closing sockets, pipes,
## connections or servers. Usually it become useful in exception or finally
## blocks.
let retFuture = newFuture[T]("chronos.noCancel(T)",
{FutureFlag.OwnCancelSchedule})
template completeFuture() =
if future.completed():
when T is void:
retFuture.complete()
else:
retFuture.complete(future.value)
elif future.failed():
retFuture.fail(future.error)
else:
raiseAssert("Unexpected future state [" & $future.state & "]")
proc continuation(udata: pointer) {.gcsafe.} =
completeFuture()
if future.finished():
completeFuture()
else:
future.addCallback(continuation)
retFuture
proc allFutures*(futs: varargs[FutureBase]): Future[void] =
## Returns a future which will complete only when all futures in ``futs``
@ -836,7 +931,7 @@ proc allFutures*(futs: varargs[FutureBase]): Future[void] =
if len(nfuts) == 0 or len(nfuts) == finishedFutures:
retFuture.complete()
return retFuture
retFuture
proc allFutures*[T](futs: varargs[Future[T]]): Future[void] =
## Returns a future which will complete only when all futures in ``futs``

View File

@ -11,7 +11,7 @@
{.push raises: [].}
from nativesockets import Port
import std/[tables, strutils, heapqueue, deques]
import std/[tables, heapqueue, deques]
import stew/results
import "."/[config, futures, osdefs, oserrno, osutils, timer]
@ -179,10 +179,11 @@ type
timers*: HeapQueue[TimerCallback]
callbacks*: Deque[AsyncCallback]
idlers*: Deque[AsyncCallback]
ticks*: Deque[AsyncCallback]
trackers*: Table[string, TrackerBase]
counters*: Table[string, TrackerCounter]
proc sentinelCallbackImpl(arg: pointer) {.gcsafe.} =
proc sentinelCallbackImpl(arg: pointer) {.gcsafe, noreturn.} =
raiseAssert "Sentinel callback MUST not be scheduled"
const
@ -254,6 +255,10 @@ template processIdlers(loop: untyped) =
if len(loop.idlers) > 0:
loop.callbacks.addLast(loop.idlers.popFirst())
template processTicks(loop: untyped) =
while len(loop.ticks) > 0:
loop.callbacks.addLast(loop.ticks.popFirst())
template processCallbacks(loop: untyped) =
while true:
let callable = loop.callbacks.popFirst() # len must be > 0 due to sentinel
@ -417,6 +422,7 @@ when defined(windows):
timers: initHeapQueue[TimerCallback](),
callbacks: initDeque[AsyncCallback](64),
idlers: initDeque[AsyncCallback](),
ticks: initDeque[AsyncCallback](),
trackers: initTable[string, TrackerBase](),
counters: initTable[string, TrackerCounter]()
)
@ -746,6 +752,9 @@ when defined(windows):
if networkEventsCount == 0:
loop.processIdlers()
# We move tick callbacks to `loop.callbacks` always.
processTicks(loop)
# All callbacks which will be added during `processCallbacks` will be
# scheduled after the sentinel and are processed on next `poll()` call.
loop.callbacks.addLast(SentinelCallback)
@ -1138,6 +1147,9 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or
if count == 0:
loop.processIdlers()
# We move tick callbacks to `loop.callbacks` always.
processTicks(loop)
# All callbacks which will be added during `processCallbacks` will be
# scheduled after the sentinel and are processed on next `poll()` call.
loop.callbacks.addLast(SentinelCallback)
@ -1255,6 +1267,20 @@ proc callIdle*(cbproc: CallbackFunc, data: pointer) =
proc callIdle*(cbproc: CallbackFunc) =
callIdle(cbproc, nil)
proc internalCallTick*(acb: AsyncCallback) =
## Schedule ``cbproc`` to be called after all scheduled callbacks, but only
## when OS system queue finished processing events.
getThreadDispatcher().ticks.addLast(acb)
proc internalCallTick*(cbproc: CallbackFunc, data: pointer) =
## Schedule ``cbproc`` to be called after all scheduled callbacks when
## OS system queue processing is done.
doAssert(not isNil(cbproc))
internalCallTick(AsyncCallback(function: cbproc, udata: data))
proc internalCallTick*(cbproc: CallbackFunc) =
internalCallTick(AsyncCallback(function: cbproc, udata: nil))
include asyncfutures2
when (chronosEventEngine in ["epoll", "kqueue"]) or defined(windows):
@ -1322,30 +1348,24 @@ proc stepsAsync*(number: int): Future[void] =
##
## This primitive can be useful when you need to create more deterministic
## tests and cases.
##
## WARNING! Do not use this primitive to perform switch between tasks, because
## this can lead to 100% CPU load in the moments when there are no I/O
## events. Usually when there no I/O events CPU consumption should be near 0%.
var retFuture = newFuture[void]("chronos.stepsAsync(int)")
var counter = 0
doAssert(number > 0, "Number should be positive integer")
var
retFuture = newFuture[void]("chronos.stepsAsync(int)")
counter = 0
continuation: proc(data: pointer) {.gcsafe, raises: [].}
var continuation: proc(data: pointer) {.gcsafe, raises: [].}
continuation = proc(data: pointer) {.gcsafe, raises: [].} =
if not(retFuture.finished()):
inc(counter)
if counter < number:
callSoon(continuation, nil)
internalCallTick(continuation)
else:
retFuture.complete()
proc cancellation(udata: pointer) =
discard
if number <= 0:
retFuture.complete()
else:
retFuture.cancelCallback = cancellation
callSoon(continuation, nil)
internalCallTick(continuation)
retFuture
@ -1374,37 +1394,46 @@ proc withTimeout*[T](fut: Future[T], timeout: Duration): Future[bool] =
## If ``fut`` completes first the returned future will hold true,
## otherwise, if ``timeout`` milliseconds has elapsed first, the returned
## future will hold false.
var retFuture = newFuture[bool]("chronos.`withTimeout`")
var moment: Moment
var timer: TimerCallback
var cancelling = false
var
retFuture = newFuture[bool]("chronos.withTimeout",
{FutureFlag.OwnCancelSchedule})
moment: Moment
timer: TimerCallback
timeouted = false
template completeFuture(fut: untyped): untyped =
if fut.failed() or fut.completed():
retFuture.complete(true)
else:
retFuture.cancelAndSchedule()
# TODO: raises annotation shouldn't be needed, but likely similar issue as
# https://github.com/nim-lang/Nim/issues/17369
proc continuation(udata: pointer) {.gcsafe, raises: [].} =
if not(retFuture.finished()):
if not(cancelling):
if not(fut.finished()):
# Timer exceeded first, we going to cancel `fut` and wait until it
# not completes.
cancelling = true
fut.cancel()
else:
# Future `fut` completed/failed/cancelled first.
if not(isNil(timer)):
clearTimer(timer)
retFuture.complete(true)
else:
if timeouted:
retFuture.complete(false)
return
if not(fut.finished()):
# Timer exceeded first, we going to cancel `fut` and wait until it
# not completes.
timeouted = true
fut.cancelSoon()
else:
# Future `fut` completed/failed/cancelled first.
if not(isNil(timer)):
clearTimer(timer)
fut.completeFuture()
# TODO: raises annotation shouldn't be needed, but likely similar issue as
# https://github.com/nim-lang/Nim/issues/17369
proc cancellation(udata: pointer) {.gcsafe, raises: [].} =
if not isNil(timer):
clearTimer(timer)
if not(fut.finished()):
fut.removeCallback(continuation)
fut.cancel()
if not isNil(timer):
clearTimer(timer)
fut.cancelSoon()
else:
fut.completeFuture()
if fut.finished():
retFuture.complete(true)
@ -1420,11 +1449,11 @@ proc withTimeout*[T](fut: Future[T], timeout: Duration): Future[bool] =
timer = setTimer(moment, continuation, nil)
fut.addCallback(continuation)
return retFuture
retFuture
proc withTimeout*[T](fut: Future[T], timeout: int): Future[bool] {.
inline, deprecated: "Use withTimeout(Future[T], Duration)".} =
result = withTimeout(fut, timeout.milliseconds())
withTimeout(fut, timeout.milliseconds())
proc wait*[T](fut: Future[T], timeout = InfiniteDuration): Future[T] =
## Returns a future which will complete once future ``fut`` completes
@ -1435,49 +1464,49 @@ proc wait*[T](fut: Future[T], timeout = InfiniteDuration): Future[T] =
##
## TODO: In case when ``fut`` got cancelled, what result Future[T]
## should return, because it can't be cancelled too.
var retFuture = newFuture[T]("chronos.wait()")
var moment: Moment
var timer: TimerCallback
var cancelling = false
var
retFuture = newFuture[T]("chronos.wait()", {FutureFlag.OwnCancelSchedule})
moment: Moment
timer: TimerCallback
timeouted = false
proc continuation(udata: pointer) {.raises: [].} =
if not(retFuture.finished()):
if not(cancelling):
if not(fut.finished()):
# Timer exceeded first.
cancelling = true
fut.cancel()
else:
# Future `fut` completed/failed/cancelled first.
if not isNil(timer):
clearTimer(timer)
if fut.failed():
retFuture.fail(fut.error)
else:
when T is void:
retFuture.complete()
else:
retFuture.complete(fut.value)
else:
retFuture.fail(newException(AsyncTimeoutError, "Timeout exceeded!"))
var cancellation: proc(udata: pointer) {.gcsafe, raises: [].}
cancellation = proc(udata: pointer) {.gcsafe, raises: [].} =
if not isNil(timer):
clearTimer(timer)
if not(fut.finished()):
fut.removeCallback(continuation)
fut.cancel()
if fut.finished():
template completeFuture(fut: untyped): untyped =
if fut.failed():
retFuture.fail(fut.error)
elif fut.cancelled():
retFuture.cancelAndSchedule()
else:
when T is void:
retFuture.complete()
else:
retFuture.complete(fut.value)
proc continuation(udata: pointer) {.raises: [].} =
if not(retFuture.finished()):
if timeouted:
retFuture.fail(newException(AsyncTimeoutError, "Timeout exceeded!"))
return
if not(fut.finished()):
# Timer exceeded first.
timeouted = true
fut.cancelSoon()
else:
# Future `fut` completed/failed/cancelled first.
if not(isNil(timer)):
clearTimer(timer)
fut.completeFuture()
var cancellation: proc(udata: pointer) {.gcsafe, raises: [].}
cancellation = proc(udata: pointer) {.gcsafe, raises: [].} =
if not(fut.finished()):
if not(isNil(timer)):
clearTimer(timer)
fut.cancelSoon()
else:
fut.completeFuture()
if fut.finished():
fut.completeFuture()
else:
if timeout.isZero():
retFuture.fail(newException(AsyncTimeoutError, "Timeout exceeded!"))
@ -1490,7 +1519,7 @@ proc wait*[T](fut: Future[T], timeout = InfiniteDuration): Future[T] =
timer = setTimer(moment, continuation, nil)
fut.addCallback(continuation)
return retFuture
retFuture
proc wait*[T](fut: Future[T], timeout = -1): Future[T] {.
inline, deprecated: "Use wait(Future[T], Duration)".} =

View File

@ -301,11 +301,6 @@ template await*[T](f: Future[T]): untyped =
# transformation - `yield` gives control back to `futureContinue` which is
# responsible for resuming execution once the yielded future is finished
yield chronosInternalRetFuture.internalChild
# `child` is guaranteed to have been `finished` after the yield
if chronosInternalRetFuture.internalMustCancel:
raise newCancelledError()
# `child` released by `futureContinue`
chronosInternalRetFuture.internalChild.internalCheckComplete()
when T isnot void:
@ -317,8 +312,6 @@ template awaitne*[T](f: Future[T]): Future[T] =
when declared(chronosInternalRetFuture):
chronosInternalRetFuture.internalChild = f
yield chronosInternalRetFuture.internalChild
if chronosInternalRetFuture.internalMustCancel:
raise newCancelledError()
cast[type(f)](chronosInternalRetFuture.internalChild)
else:
unsupported "awaitne is only available within {.async.}"

View File

@ -1241,7 +1241,7 @@ proc closeWait*(p: AsyncProcessRef) {.async.} =
# Here we ignore all possible errrors, because we do not want to raise
# exceptions.
discard closeProcessHandles(p.pipes, p.options, OSErrorCode(0))
await p.pipes.closeProcessStreams(p.options)
await noCancel(p.pipes.closeProcessStreams(p.options))
discard p.closeThreadAndProcessHandle()
untrackCounter(AsyncProcessTrackerName)

View File

@ -736,13 +736,19 @@ proc close*(ab: AsyncEventQueue) {.raises: [].} =
ab.queue.clear()
proc closeWait*(ab: AsyncEventQueue): Future[void] {.raises: [].} =
var retFuture = newFuture[void]("AsyncEventQueue.closeWait()")
let retFuture = newFuture[void]("AsyncEventQueue.closeWait()",
{FutureFlag.OwnCancelSchedule})
proc continuation(udata: pointer) {.gcsafe.} =
if not(retFuture.finished()):
retFuture.complete()
retFuture.complete()
proc cancellation(udata: pointer) {.gcsafe.} =
# We are not going to change the state of `retFuture` to cancelled, so we
# will prevent the entire sequence of Futures from being cancelled.
discard
ab.close()
# Schedule `continuation` to be called only after all the `reader`
# notifications will be scheduled and processed.
retFuture.cancelCallback = cancellation
callSoon(continuation)
retFuture

View File

@ -37,6 +37,11 @@ type
FutureState* {.pure.} = enum
Pending, Completed, Cancelled, Failed
FutureFlag* {.pure.} = enum
OwnCancelSchedule
FutureFlags* = set[FutureFlag]
InternalFutureBase* = object of RootObj
# Internal untyped future representation - the fields are not part of the
# public API and neither is `InternalFutureBase`, ie the inheritance
@ -47,8 +52,8 @@ type
internalCancelcb*: CallbackFunc
internalChild*: FutureBase
internalState*: FutureState
internalFlags*: FutureFlags
internalError*: ref CatchableError ## Stored exception
internalMustCancel*: bool
internalClosure*: iterator(f: FutureBase): FutureBase {.closureIter.}
when chronosFutureId:
@ -94,12 +99,11 @@ when chronosFutureTracking:
var futureList* {.threadvar.}: FutureList
# Internal utilities - these are not part of the stable API
proc internalInitFutureBase*(
fut: FutureBase,
loc: ptr SrcLoc,
state: FutureState) =
proc internalInitFutureBase*(fut: FutureBase, loc: ptr SrcLoc,
state: FutureState, flags: FutureFlags) =
fut.internalState = state
fut.internalLocation[LocationKind.Create] = loc
fut.internalFlags = flags
if state != FutureState.Pending:
fut.internalLocation[LocationKind.Finish] = loc
@ -128,21 +132,34 @@ template init*[T](F: type Future[T], fromProc: static[string] = ""): Future[T] =
## Specifying ``fromProc``, which is a string specifying the name of the proc
## that this future belongs to, is a good habit as it helps with debugging.
let res = Future[T]()
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Pending)
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Pending, {})
res
template init*[T](F: type Future[T], fromProc: static[string] = "",
flags: static[FutureFlags]): Future[T] =
## Creates a new pending future.
##
## Specifying ``fromProc``, which is a string specifying the name of the proc
## that this future belongs to, is a good habit as it helps with debugging.
let res = Future[T]()
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Pending,
flags)
res
template completed*(
F: type Future, fromProc: static[string] = ""): Future[void] =
## Create a new completed future
let res = Future[T]()
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed)
let res = Future[void]()
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed,
{})
res
template completed*[T: not void](
F: type Future, valueParam: T, fromProc: static[string] = ""): Future[T] =
## Create a new completed future
let res = Future[T](internalValue: valueParam)
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed)
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed,
{})
res
template failed*[T](
@ -150,19 +167,21 @@ template failed*[T](
fromProc: static[string] = ""): Future[T] =
## Create a new failed future
let res = Future[T](internalError: errorParam)
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Failed)
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Failed, {})
when chronosStackTrace:
res.internalErrorStackTrace =
if getStackTrace(res.error) == "":
getStackTrace()
else:
getStackTrace(res.error)
res
func state*(future: FutureBase): FutureState =
future.internalState
func flags*(future: FutureBase): FutureFlags =
future.internalFlags
func finished*(future: FutureBase): bool {.inline.} =
## Determines whether ``future`` has finished, i.e. ``future`` state changed
## from state ``Pending`` to one of the states (``Finished``, ``Cancelled``,

View File

@ -88,8 +88,8 @@ proc worker(bucket: TokenBucket) {.async.} =
#buckets
sleeper = sleepAsync(milliseconds(timeToTarget))
await sleeper or eventWaiter
sleeper.cancel()
eventWaiter.cancel()
sleeper.cancelSoon()
eventWaiter.cancelSoon()
else:
await eventWaiter

View File

@ -913,7 +913,7 @@ proc close*(rw: AsyncStreamRW) =
callSoon(continuation)
else:
rw.future.addCallback(continuation)
rw.future.cancel()
rw.future.cancelSoon()
elif rw is AsyncStreamWriter:
if isNil(rw.wsource) or isNil(rw.writerLoop) or isNil(rw.future):
callSoon(continuation)
@ -922,12 +922,36 @@ proc close*(rw: AsyncStreamRW) =
callSoon(continuation)
else:
rw.future.addCallback(continuation)
rw.future.cancel()
rw.future.cancelSoon()
proc closeWait*(rw: AsyncStreamRW): Future[void] =
## Close and frees resources of stream ``rw``.
const FutureName =
when rw is AsyncStreamReader:
"async.stream.reader.closeWait"
else:
"async.stream.writer.closeWait"
if rw.closed():
return Future.completed(FutureName)
let retFuture = newFuture[void](FutureName, {FutureFlag.OwnCancelSchedule})
proc continuation(udata: pointer) {.gcsafe, raises:[].} =
retFuture.complete()
proc cancellation(udata: pointer) {.gcsafe, raises:[].} =
# We are not going to change the state of `retFuture` to cancelled, so we
# will prevent the entire sequence of Futures from being cancelled.
discard
rw.close()
rw.join()
if rw.future.finished():
retFuture.complete()
else:
rw.future.addCallback(continuation, cast[pointer](retFuture))
retFuture.cancelCallback = cancellation
retFuture
proc startReader(rstream: AsyncStreamReader) =
rstream.state = Running

View File

@ -267,19 +267,15 @@ template readAndReset(fut: untyped) =
break
proc cancelAndWait*(a, b, c, d: Future[TLSResult]): Future[void] =
var waiting: seq[Future[TLSResult]]
var waiting: seq[FutureBase]
if not(isNil(a)) and not(a.finished()):
a.cancel()
waiting.add(a)
waiting.add(a.cancelAndWait())
if not(isNil(b)) and not(b.finished()):
b.cancel()
waiting.add(b)
waiting.add(b.cancelAndWait())
if not(isNil(c)) and not(c.finished()):
c.cancel()
waiting.add(c)
waiting.add(c.cancelAndWait())
if not(isNil(d)) and not(d.finished()):
d.cancel()
waiting.add(d)
waiting.add(d.cancelAndWait())
allFutures(waiting)
proc dumpState*(state: cuint): string =
@ -432,7 +428,7 @@ proc tlsLoop*(stream: TLSAsyncStream) {.async.} =
proc tlsWriteLoop(stream: AsyncStreamWriter) {.async.} =
var wstream = TLSStreamWriter(stream)
wstream.state = AsyncStreamState.Running
await stepsAsync(1)
await sleepAsync(0.milliseconds)
if isNil(wstream.stream.mainLoop):
wstream.stream.mainLoop = tlsLoop(wstream.stream)
await wstream.stream.mainLoop
@ -440,7 +436,7 @@ proc tlsWriteLoop(stream: AsyncStreamWriter) {.async.} =
proc tlsReadLoop(stream: AsyncStreamReader) {.async.} =
var rstream = TLSStreamReader(stream)
rstream.state = AsyncStreamState.Running
await stepsAsync(1)
await sleepAsync(0.milliseconds)
if isNil(rstream.stream.mainLoop):
rstream.stream.mainLoop = tlsLoop(rstream.stream)
await rstream.stream.mainLoop

View File

@ -690,8 +690,28 @@ proc join*(transp: DatagramTransport): Future[void] =
proc closeWait*(transp: DatagramTransport): Future[void] =
## Close transport ``transp`` and release all resources.
const FutureName = "datagram.transport.closeWait"
if {ReadClosed, WriteClosed} * transp.state != {}:
return Future.completed(FutureName)
let retFuture = newFuture[void](FutureName, {FutureFlag.OwnCancelSchedule})
proc continuation(udata: pointer) {.gcsafe.} =
retFuture.complete()
proc cancellation(udata: pointer) {.gcsafe.} =
# We are not going to change the state of `retFuture` to cancelled, so we
# will prevent the entire sequence of Futures from being cancelled.
discard
transp.close()
transp.join()
if transp.future.finished():
retFuture.complete()
else:
transp.future.addCallback(continuation, cast[pointer](retFuture))
retFuture.cancelCallback = cancellation
retFuture
proc send*(transp: DatagramTransport, pbytes: pointer,
nbytes: int): Future[void] =

View File

@ -2588,15 +2588,34 @@ proc close*(transp: StreamTransport) =
proc closeWait*(transp: StreamTransport): Future[void] =
## Close and frees resources of transport ``transp``.
const FutureName = "stream.transport.closeWait"
if {ReadClosed, WriteClosed} * transp.state != {}:
return Future.completed(FutureName)
let retFuture = newFuture[void](FutureName, {FutureFlag.OwnCancelSchedule})
proc continuation(udata: pointer) {.gcsafe.} =
retFuture.complete()
proc cancellation(udata: pointer) {.gcsafe.} =
# We are not going to change the state of `retFuture` to cancelled, so we
# will prevent the entire sequence of Futures from being cancelled.
discard
transp.close()
transp.join()
if transp.future.finished():
retFuture.complete()
else:
transp.future.addCallback(continuation, cast[pointer](retFuture))
retFuture.cancelCallback = cancellation
retFuture
proc shutdownWait*(transp: StreamTransport): Future[void] =
## Perform graceful shutdown of TCP connection backed by transport ``transp``.
doAssert(transp.kind == TransportKind.Socket)
let retFuture = newFuture[void]("stream.transport.shutdown")
transp.checkClosed(retFuture)
transp.checkWriteEof(retFuture)
when defined(windows):
let loop = getThreadDispatcher()
@ -2636,7 +2655,14 @@ proc shutdownWait*(transp: StreamTransport): Future[void] =
let res = osdefs.shutdown(SocketHandle(transp.fd), SHUT_WR)
if res < 0:
let err = osLastError()
retFuture.fail(getTransportOsError(err))
case err
of ENOTCONN:
# The specified socket is not connected, it means that our initial
# goal is already happened.
transp.state.incl({WriteEof})
callSoon(continuation, nil)
else:
retFuture.fail(getTransportOsError(err))
else:
transp.state.incl({WriteEof})
callSoon(continuation, nil)

View File

@ -14,7 +14,7 @@ suite "Asynchronous issues test suite":
const HELLO_PORT = 45679
const TEST_MSG = "testmsg"
const MSG_LEN = TEST_MSG.len()
const TestsCount = 500
const TestsCount = 100
type
CustomData = ref object

View File

@ -6,10 +6,15 @@
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import unittest2
import stew/results
import ../chronos, ../chronos/unittest2/asynctests
{.used.}
type
TestFooConnection* = ref object
id*: int
suite "Future[T] behavior test suite":
proc testFuture1(): Future[int] {.async.} =
await sleepAsync(0.milliseconds)
@ -960,7 +965,7 @@ suite "Future[T] behavior test suite":
let discarded {.used.} = await fut1
check res
asyncTest "cancel() async procedure test":
asyncTest "tryCancel() async procedure test":
var completed = 0
proc client1() {.async.} =
@ -980,7 +985,7 @@ suite "Future[T] behavior test suite":
inc(completed)
var fut = client4()
fut.cancel()
discard fut.tryCancel()
# Future must not be cancelled immediately, because it has many nested
# futures.
@ -1031,7 +1036,7 @@ suite "Future[T] behavior test suite":
var fut1 = client2()
var fut2 = client2()
fut1.cancel()
discard fut1.tryCancel()
await fut1
await cancelAndWait(fut2)
check:
@ -1054,17 +1059,17 @@ suite "Future[T] behavior test suite":
if not(retFuture.finished()):
retFuture.complete()
proc cancel(udata: pointer) {.gcsafe.} =
proc cancellation(udata: pointer) {.gcsafe.} =
inc(cancelled)
if not(retFuture.finished()):
removeTimer(moment, completion, cast[pointer](retFuture))
retFuture.cancelCallback = cancel
retFuture.cancelCallback = cancellation
discard setTimer(moment, completion, cast[pointer](retFuture))
return retFuture
var fut = client1(100.milliseconds)
fut.cancel()
discard fut.tryCancel()
await sleepAsync(500.milliseconds)
check:
fut.cancelled()
@ -1112,8 +1117,8 @@ suite "Future[T] behavior test suite":
neverFlag3 = true
res.addCallback(continuation)
res.cancelCallback = cancellation
result = res
neverFlag1 = true
res
proc withTimeoutProc() {.async.} =
try:
@ -1149,12 +1154,12 @@ suite "Future[T] behavior test suite":
someFut = newFuture[void]()
var raceFut3 = raceProc()
someFut.cancel()
discard someFut.tryCancel()
await cancelAndWait(raceFut3)
check:
raceFut1.state == FutureState.Cancelled
raceFut2.state == FutureState.Cancelled
raceFut1.state == FutureState.Completed
raceFut2.state == FutureState.Failed
raceFut3.state == FutureState.Cancelled
asyncTest "asyncSpawn() test":
@ -1255,12 +1260,12 @@ suite "Future[T] behavior test suite":
(loc.procedure == procedure)
check:
chk(loc10, "testfut.nim", 1221, "macroFuture")
chk(loc11, "testfut.nim", 1222, "")
chk(loc20, "testfut.nim", 1234, "template")
chk(loc21, "testfut.nim", 1237, "")
chk(loc30, "testfut.nim", 1231, "procedure")
chk(loc31, "testfut.nim", 1238, "")
chk(loc10, "testfut.nim", 1226, "macroFuture")
chk(loc11, "testfut.nim", 1227, "")
chk(loc20, "testfut.nim", 1239, "template")
chk(loc21, "testfut.nim", 1242, "")
chk(loc30, "testfut.nim", 1236, "procedure")
chk(loc31, "testfut.nim", 1243, "")
asyncTest "withTimeout(fut) should wait cancellation test":
proc futureNeverEnds(): Future[void] =
@ -1535,3 +1540,462 @@ suite "Future[T] behavior test suite":
check:
v1_u == 0'u
v2_u + 1'u == 0'u
asyncTest "wait() cancellation undefined behavior test #1":
proc testInnerFoo(fooFut: Future[void]): Future[TestFooConnection] {.
async.} =
await fooFut
return TestFooConnection()
proc testFoo(fooFut: Future[void]) {.async.} =
let connection =
try:
let res = await testInnerFoo(fooFut).wait(10.seconds)
Result[TestFooConnection, int].ok(res)
except CancelledError:
Result[TestFooConnection, int].err(0)
except CatchableError:
Result[TestFooConnection, int].err(1)
check connection.isOk()
var future = newFuture[void]("last.child.future")
var someFut = testFoo(future)
future.complete()
discard someFut.tryCancel()
await someFut
asyncTest "wait() cancellation undefined behavior test #2":
proc testInnerFoo(fooFut: Future[void]): Future[TestFooConnection] {.
async.} =
await fooFut
return TestFooConnection()
proc testMiddleFoo(fooFut: Future[void]): Future[TestFooConnection] {.
async.} =
await testInnerFoo(fooFut)
proc testFoo(fooFut: Future[void]) {.async.} =
let connection =
try:
let res = await testMiddleFoo(fooFut).wait(10.seconds)
Result[TestFooConnection, int].ok(res)
except CancelledError:
Result[TestFooConnection, int].err(0)
except CatchableError:
Result[TestFooConnection, int].err(1)
check connection.isOk()
var future = newFuture[void]("last.child.future")
var someFut = testFoo(future)
future.complete()
discard someFut.tryCancel()
await someFut
asyncTest "withTimeout() cancellation undefined behavior test #1":
proc testInnerFoo(fooFut: Future[void]): Future[TestFooConnection] {.
async.} =
await fooFut
return TestFooConnection()
proc testFoo(fooFut: Future[void]) {.async.} =
let connection =
try:
let
checkFut = testInnerFoo(fooFut)
res = await withTimeout(checkFut, 10.seconds)
if res:
Result[TestFooConnection, int].ok(checkFut.value)
else:
Result[TestFooConnection, int].err(0)
except CancelledError:
Result[TestFooConnection, int].err(1)
except CatchableError:
Result[TestFooConnection, int].err(2)
check connection.isOk()
var future = newFuture[void]("last.child.future")
var someFut = testFoo(future)
future.complete()
discard someFut.tryCancel()
await someFut
asyncTest "withTimeout() cancellation undefined behavior test #2":
proc testInnerFoo(fooFut: Future[void]): Future[TestFooConnection] {.
async.} =
await fooFut
return TestFooConnection()
proc testMiddleFoo(fooFut: Future[void]): Future[TestFooConnection] {.
async.} =
await testInnerFoo(fooFut)
proc testFoo(fooFut: Future[void]) {.async.} =
let connection =
try:
let
checkFut = testMiddleFoo(fooFut)
res = await withTimeout(checkFut, 10.seconds)
if res:
Result[TestFooConnection, int].ok(checkFut.value)
else:
Result[TestFooConnection, int].err(0)
except CancelledError:
Result[TestFooConnection, int].err(1)
except CatchableError:
Result[TestFooConnection, int].err(2)
check connection.isOk()
var future = newFuture[void]("last.child.future")
var someFut = testFoo(future)
future.complete()
discard someFut.tryCancel()
await someFut
asyncTest "Cancellation behavior test":
proc testInnerFoo(fooFut: Future[void]) {.async.} =
await fooFut
proc testMiddleFoo(fooFut: Future[void]) {.async.} =
await testInnerFoo(fooFut)
proc testOuterFoo(fooFut: Future[void]) {.async.} =
await testMiddleFoo(fooFut)
block:
# Cancellation of pending Future
let future = newFuture[void]("last.child.pending.future")
await cancelAndWait(future)
check:
future.cancelled() == true
block:
# Cancellation of completed Future
let future = newFuture[void]("last.child.completed.future")
future.complete()
await cancelAndWait(future)
check:
future.cancelled() == false
future.completed() == true
block:
# Cancellation of failed Future
let future = newFuture[void]("last.child.failed.future")
future.fail(newException(ValueError, "ABCD"))
await cancelAndWait(future)
check:
future.cancelled() == false
future.failed() == true
block:
# Cancellation of already cancelled Future
let future = newFuture[void]("last.child.cancelled.future")
future.cancelAndSchedule()
await cancelAndWait(future)
check:
future.cancelled() == true
block:
# Cancellation of Pending->Pending->Pending->Pending sequence
let future = newFuture[void]("last.child.pending.future")
let testFut = testOuterFoo(future)
await cancelAndWait(testFut)
check:
testFut.cancelled() == true
block:
# Cancellation of Pending->Pending->Pending->Completed sequence
let future = newFuture[void]("last.child.completed.future")
let testFut = testOuterFoo(future)
future.complete()
await cancelAndWait(testFut)
check:
testFut.cancelled() == false
testFut.completed() == true
block:
# Cancellation of Pending->Pending->Pending->Failed sequence
let future = newFuture[void]("last.child.failed.future")
let testFut = testOuterFoo(future)
future.fail(newException(ValueError, "ABCD"))
await cancelAndWait(testFut)
check:
testFut.cancelled() == false
testFut.failed() == true
block:
# Cancellation of Pending->Pending->Pending->Cancelled sequence
let future = newFuture[void]("last.child.cancelled.future")
let testFut = testOuterFoo(future)
future.cancelAndSchedule()
await cancelAndWait(testFut)
check:
testFut.cancelled() == true
block:
# Cancellation of pending Future, when automatic scheduling disabled
let future = newFuture[void]("last.child.pending.future",
{FutureFlag.OwnCancelSchedule})
proc cancellation(udata: pointer) {.gcsafe.} =
discard
future.cancelCallback = cancellation
# Note, future will never be finished in such case, until we manually not
# finish it
let cancelFut = cancelAndWait(future)
await sleepAsync(100.milliseconds)
check:
cancelFut.finished() == false
future.cancelled() == false
# Now we manually changing Future's state, so `cancelAndWait` could
# finish
future.complete()
await cancelFut
check:
cancelFut.finished() == true
future.cancelled() == false
future.finished() == true
block:
# Cancellation of pending Future, which will fail Future on cancellation,
# when automatic scheduling disabled
let future = newFuture[void]("last.child.completed.future",
{FutureFlag.OwnCancelSchedule})
proc cancellation(udata: pointer) {.gcsafe.} =
future.complete()
future.cancelCallback = cancellation
# Note, future will never be finished in such case, until we manually not
# finish it
await cancelAndWait(future)
check:
future.cancelled() == false
future.completed() == true
block:
# Cancellation of pending Future, which will fail Future on cancellation,
# when automatic scheduling disabled
let future = newFuture[void]("last.child.failed.future",
{FutureFlag.OwnCancelSchedule})
proc cancellation(udata: pointer) {.gcsafe.} =
future.fail(newException(ValueError, "ABCD"))
future.cancelCallback = cancellation
# Note, future will never be finished in such case, until we manually not
# finish it
await cancelAndWait(future)
check:
future.cancelled() == false
future.failed() == true
block:
# Cancellation of pending Future, which will fail Future on cancellation,
# when automatic scheduling disabled
let future = newFuture[void]("last.child.cancelled.future",
{FutureFlag.OwnCancelSchedule})
proc cancellation(udata: pointer) {.gcsafe.} =
future.cancelAndSchedule()
future.cancelCallback = cancellation
# Note, future will never be finished in such case, until we manually not
# finish it
await cancelAndWait(future)
check:
future.cancelled() == true
block:
# Cancellation of pending Pending->Pending->Pending->Pending, when
# automatic scheduling disabled and Future do nothing in cancellation
# callback
let future = newFuture[void]("last.child.pending.future",
{FutureFlag.OwnCancelSchedule})
proc cancellation(udata: pointer) {.gcsafe.} =
discard
future.cancelCallback = cancellation
# Note, future will never be finished in such case, until we manually not
# finish it
let testFut = testOuterFoo(future)
let cancelFut = cancelAndWait(testFut)
await sleepAsync(100.milliseconds)
check:
cancelFut.finished() == false
testFut.cancelled() == false
future.cancelled() == false
# Now we manually changing Future's state, so `cancelAndWait` could
# finish
future.complete()
await cancelFut
check:
cancelFut.finished() == true
future.cancelled() == false
future.finished() == true
testFut.cancelled() == false
testFut.finished() == true
block:
# Cancellation of pending Pending->Pending->Pending->Pending, when
# automatic scheduling disabled and Future completes in cancellation
# callback
let future = newFuture[void]("last.child.pending.future",
{FutureFlag.OwnCancelSchedule})
proc cancellation(udata: pointer) {.gcsafe.} =
future.complete()
future.cancelCallback = cancellation
# Note, future will never be finished in such case, until we manually not
# finish it
let testFut = testOuterFoo(future)
await cancelAndWait(testFut)
await sleepAsync(100.milliseconds)
check:
testFut.cancelled() == false
testFut.finished() == true
future.cancelled() == false
future.finished() == true
block:
# Cancellation of pending Pending->Pending->Pending->Pending, when
# automatic scheduling disabled and Future fails in cancellation callback
let future = newFuture[void]("last.child.pending.future",
{FutureFlag.OwnCancelSchedule})
proc cancellation(udata: pointer) {.gcsafe.} =
future.fail(newException(ValueError, "ABCD"))
future.cancelCallback = cancellation
# Note, future will never be finished in such case, until we manually not
# finish it
let testFut = testOuterFoo(future)
await cancelAndWait(testFut)
await sleepAsync(100.milliseconds)
check:
testFut.cancelled() == false
testFut.failed() == true
future.cancelled() == false
future.failed() == true
block:
# Cancellation of pending Pending->Pending->Pending->Pending, when
# automatic scheduling disabled and Future fails in cancellation callback
let future = newFuture[void]("last.child.pending.future",
{FutureFlag.OwnCancelSchedule})
proc cancellation(udata: pointer) {.gcsafe.} =
future.cancelAndSchedule()
future.cancelCallback = cancellation
# Note, future will never be finished in such case, until we manually not
# finish it
let testFut = testOuterFoo(future)
await cancelAndWait(testFut)
await sleepAsync(100.milliseconds)
check:
testFut.cancelled() == true
future.cancelled() == true
test "Issue #334 test":
proc test(): bool =
var testres = ""
proc a() {.async.} =
try:
await sleepAsync(seconds(1))
except CatchableError as exc:
testres.add("A")
raise exc
proc b() {.async.} =
try:
await a()
except CatchableError as exc:
testres.add("B")
raise exc
proc c() {.async.} =
try:
echo $(await b().withTimeout(seconds(2)))
except CatchableError as exc:
testres.add("C")
raise exc
let x = c()
x.cancelSoon()
try:
waitFor x
except CatchableError:
testres.add("D")
testres.add("E")
waitFor sleepAsync(milliseconds(100))
testres == "ABCDE"
check test() == true
asyncTest "cancelAndWait() should be able to cancel test":
proc test1() {.async.} =
await noCancel sleepAsync(100.milliseconds)
await noCancel sleepAsync(100.milliseconds)
await sleepAsync(100.milliseconds)
proc test2() {.async.} =
await noCancel sleepAsync(100.milliseconds)
await sleepAsync(100.milliseconds)
await noCancel sleepAsync(100.milliseconds)
proc test3() {.async.} =
await sleepAsync(100.milliseconds)
await noCancel sleepAsync(100.milliseconds)
await noCancel sleepAsync(100.milliseconds)
proc test4() {.async.} =
while true:
await noCancel sleepAsync(50.milliseconds)
await sleepAsync(0.milliseconds)
proc test5() {.async.} =
while true:
await sleepAsync(0.milliseconds)
await noCancel sleepAsync(50.milliseconds)
block:
let future1 = test1()
await cancelAndWait(future1)
let future2 = test1()
await sleepAsync(10.milliseconds)
await cancelAndWait(future2)
check:
future1.cancelled() == true
future2.cancelled() == true
block:
let future1 = test2()
await cancelAndWait(future1)
let future2 = test2()
await sleepAsync(10.milliseconds)
await cancelAndWait(future2)
check:
future1.cancelled() == true
future2.cancelled() == true
block:
let future1 = test3()
await cancelAndWait(future1)
let future2 = test3()
await sleepAsync(10.milliseconds)
await cancelAndWait(future2)
check:
future1.cancelled() == true
future2.cancelled() == true
block:
let future1 = test4()
await cancelAndWait(future1)
let future2 = test4()
await sleepAsync(333.milliseconds)
await cancelAndWait(future2)
check:
future1.cancelled() == true
future2.cancelled() == true
block:
let future1 = test5()
await cancelAndWait(future1)
let future2 = test5()
await sleepAsync(333.milliseconds)
await cancelAndWait(future2)
check:
future1.cancelled() == true
future2.cancelled() == true

View File

@ -704,6 +704,107 @@ suite "HTTP client testing suite":
await server.closeWait()
return "redirect-" & $res
proc testSendCancelLeaksTest(secure: bool): Future[bool] {.async.} =
proc process(r: RequestFence): Future[HttpResponseRef] {.
async.} =
return defaultResponse()
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
server.start()
let address = server.instance.localAddress()
let ha =
if secure:
getAddress(address, HttpClientScheme.Secure, "/")
else:
getAddress(address, HttpClientScheme.NonSecure, "/")
var counter = 0
while true:
let
session = createSession(secure)
request = HttpClientRequestRef.new(session, ha, MethodGet)
requestFut = request.send()
if counter > 0:
await stepsAsync(counter)
let exitLoop =
if not(requestFut.finished()):
await cancelAndWait(requestFut)
doAssert(cancelled(requestFut) or completed(requestFut),
"Future should be Cancelled or Completed at this point")
if requestFut.completed():
let response = await requestFut
await response.closeWait()
inc(counter)
false
else:
let response = await requestFut
await response.closeWait()
true
await request.closeWait()
await session.closeWait()
if exitLoop:
break
await server.stop()
await server.closeWait()
return true
proc testOpenCancelLeaksTest(secure: bool): Future[bool] {.async.} =
proc process(r: RequestFence): Future[HttpResponseRef] {.
async.} =
return defaultResponse()
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
server.start()
let address = server.instance.localAddress()
let ha =
if secure:
getAddress(address, HttpClientScheme.Secure, "/")
else:
getAddress(address, HttpClientScheme.NonSecure, "/")
var counter = 0
while true:
let
session = createSession(secure)
request = HttpClientRequestRef.new(session, ha, MethodPost)
bodyFut = request.open()
if counter > 0:
await stepsAsync(counter)
let exitLoop =
if not(bodyFut.finished()):
await cancelAndWait(bodyFut)
doAssert(cancelled(bodyFut) or completed(bodyFut),
"Future should be Cancelled or Completed at this point")
if bodyFut.completed():
let bodyWriter = await bodyFut
await bodyWriter.closeWait()
inc(counter)
false
else:
let bodyWriter = await bodyFut
await bodyWriter.closeWait()
true
await request.closeWait()
await session.closeWait()
if exitLoop:
break
await server.stop()
await server.closeWait()
return true
# proc testBasicAuthorization(): Future[bool] {.async.} =
# let session = HttpSessionRef.new({HttpClientFlag.NoVerifyHost},
# maxRedirections = 10)
@ -1243,6 +1344,18 @@ suite "HTTP client testing suite":
test "HTTP(S) client maximum redirections test":
check waitFor(testRequestRedirectTest(true, 4)) == "redirect-true"
test "HTTP send() cancellation leaks test":
check waitFor(testSendCancelLeaksTest(false)) == true
test "HTTP(S) send() cancellation leaks test":
check waitFor(testSendCancelLeaksTest(true)) == true
test "HTTP open() cancellation leaks test":
check waitFor(testOpenCancelLeaksTest(false)) == true
test "HTTP(S) open() cancellation leaks test":
check waitFor(testOpenCancelLeaksTest(true)) == true
test "HTTPS basic authorization test":
skip()
# This test disabled because remote service is pretty flaky and fails pretty

View File

@ -1326,32 +1326,31 @@ suite "HTTP server testing suite":
server.start()
var transp: StreamTransport
try:
transp = await connect(address)
block:
let response = await transp.httpClient2(test[0], 7)
check:
response.data == "TEST_OK"
response.headers.getString("connection") == test[3]
# We do this sleeping here just because we running both server and
# client in single process, so when we received response from server
# it does not mean that connection has been immediately closed - it
# takes some more calls, so we trying to get this calls happens.
await sleepAsync(50.milliseconds)
let connectionStillAvailable =
try:
let response {.used.} = await transp.httpClient2(test[0], 7)
true
except CatchableError:
false
check connectionStillAvailable == test[2]
transp = await connect(address)
block:
let response = await transp.httpClient2(test[0], 7)
check:
response.data == "TEST_OK"
response.headers.getString("connection") == test[3]
# We do this sleeping here just because we running both server and
# client in single process, so when we received response from server
# it does not mean that connection has been immediately closed - it
# takes some more calls, so we trying to get this calls happens.
await sleepAsync(50.milliseconds)
let connectionStillAvailable =
try:
let response {.used.} = await transp.httpClient2(test[0], 7)
true
except CatchableError:
false
finally:
if not(isNil(transp)):
await transp.closeWait()
await server.stop()
await server.closeWait()
check connectionStillAvailable == test[2]
if not(isNil(transp)):
await transp.closeWait()
await server.stop()
await server.closeWait()
asyncTest "HTTP debug tests":
const
@ -1400,32 +1399,30 @@ suite "HTTP server testing suite":
info.flags == {HttpServerFlags.Http11Pipeline}
info.socketFlags == socketFlags
try:
var clientFutures: seq[Future[StreamTransport]]
for i in 0 ..< TestsCount:
clientFutures.add(client(address, TestRequest))
await allFutures(clientFutures)
var clientFutures: seq[Future[StreamTransport]]
for i in 0 ..< TestsCount:
clientFutures.add(client(address, TestRequest))
await allFutures(clientFutures)
let connections = server.getConnections()
check len(connections) == TestsCount
let currentTime = Moment.now()
for index, connection in connections.pairs():
let transp = clientFutures[index].read()
check:
connection.remoteAddress.get() == transp.localAddress()
connection.localAddress.get() == transp.remoteAddress()
connection.connectionType == ConnectionType.NonSecure
connection.connectionState == ConnectionState.Alive
connection.query.get("") == "/httpdebug"
(currentTime - connection.createMoment.get()) != ZeroDuration
(currentTime - connection.acceptMoment) != ZeroDuration
var pending: seq[Future[void]]
for transpFut in clientFutures:
pending.add(closeWait(transpFut.read()))
await allFutures(pending)
finally:
await server.stop()
await server.closeWait()
let connections = server.getConnections()
check len(connections) == TestsCount
let currentTime = Moment.now()
for index, connection in connections.pairs():
let transp = clientFutures[index].read()
check:
connection.remoteAddress.get() == transp.localAddress()
connection.localAddress.get() == transp.remoteAddress()
connection.connectionType == ConnectionType.NonSecure
connection.connectionState == ConnectionState.Alive
connection.query.get("") == "/httpdebug"
(currentTime - connection.createMoment.get()) != ZeroDuration
(currentTime - connection.acceptMoment) != ZeroDuration
var pending: seq[Future[void]]
for transpFut in clientFutures:
pending.add(closeWait(transpFut.read()))
await allFutures(pending)
await server.stop()
await server.closeWait()
test "Leaks test":
checkLeaks()

View File

@ -49,7 +49,7 @@ suite "Token Bucket":
# Consume 10* the budget cap
let beforeStart = Moment.now()
waitFor(bucket.consume(1000).wait(5.seconds))
check Moment.now() - beforeStart in 900.milliseconds .. 1500.milliseconds
check Moment.now() - beforeStart in 900.milliseconds .. 2200.milliseconds
test "Sync manual replenish":
var bucket = TokenBucket.new(1000, 0.seconds)
@ -96,7 +96,7 @@ suite "Token Bucket":
futBlocker.finished == false
fut2.finished == false
futBlocker.cancel()
futBlocker.cancelSoon()
waitFor(fut2.wait(10.milliseconds))
test "Very long replenish":
@ -117,9 +117,14 @@ suite "Token Bucket":
check bucket.tryConsume(1, fakeNow) == true
test "Short replenish":
var bucket = TokenBucket.new(15000, 1.milliseconds)
let start = Moment.now()
check bucket.tryConsume(15000, start)
check bucket.tryConsume(1, start) == false
skip()
# TODO (cheatfate): This test was disabled, because it continuosly fails in
# Github Actions Windows x64 CI when using Nim 1.6.14 version.
# Unable to reproduce failure locally.
check bucket.tryConsume(15000, start + 1.milliseconds) == true
# var bucket = TokenBucket.new(15000, 1.milliseconds)
# let start = Moment.now()
# check bucket.tryConsume(15000, start)
# check bucket.tryConsume(1, start) == false
# check bucket.tryConsume(15000, start + 1.milliseconds) == true

View File

@ -11,75 +11,83 @@ import ../chronos
{.used.}
suite "callSoon() tests suite":
const CallSoonTests = 10
var soonTest1 = 0'u
var timeoutsTest1 = 0
var timeoutsTest2 = 0
var soonTest2 = 0
proc callback1(udata: pointer) {.gcsafe.} =
soonTest1 = soonTest1 xor cast[uint](udata)
proc test1(): uint =
callSoon(callback1, cast[pointer](0x12345678'u))
callSoon(callback1, cast[pointer](0x23456789'u))
callSoon(callback1, cast[pointer](0x3456789A'u))
callSoon(callback1, cast[pointer](0x456789AB'u))
callSoon(callback1, cast[pointer](0x56789ABC'u))
callSoon(callback1, cast[pointer](0x6789ABCD'u))
callSoon(callback1, cast[pointer](0x789ABCDE'u))
callSoon(callback1, cast[pointer](0x89ABCDEF'u))
callSoon(callback1, cast[pointer](0x9ABCDEF1'u))
callSoon(callback1, cast[pointer](0xABCDEF12'u))
callSoon(callback1, cast[pointer](0xBCDEF123'u))
callSoon(callback1, cast[pointer](0xCDEF1234'u))
callSoon(callback1, cast[pointer](0xDEF12345'u))
callSoon(callback1, cast[pointer](0xEF123456'u))
callSoon(callback1, cast[pointer](0xF1234567'u))
callSoon(callback1, cast[pointer](0x12345678'u))
## All callbacks must be processed exactly with 1 poll() call.
poll()
result = soonTest1
proc testProc() {.async.} =
for i in 1..CallSoonTests:
await sleepAsync(100.milliseconds)
timeoutsTest1 += 1
var callbackproc: proc(udata: pointer) {.gcsafe, raises: [].}
callbackproc = proc (udata: pointer) {.gcsafe, raises: [].} =
timeoutsTest2 += 1
{.gcsafe.}:
callSoon(callbackproc)
proc test2(timers, callbacks: var int) =
callSoon(callbackproc)
waitFor(testProc())
timers = timeoutsTest1
callbacks = timeoutsTest2
proc testCallback(udata: pointer) =
soonTest2 = 987654321
proc test3(): bool =
callSoon(testCallback)
poll()
result = soonTest2 == 987654321
test "User-defined callback argument test":
var values = [0x12345678'u, 0x23456789'u, 0x3456789A'u, 0x456789AB'u,
0x56789ABC'u, 0x6789ABCD'u, 0x789ABCDE'u, 0x89ABCDEF'u,
0x9ABCDEF1'u, 0xABCDEF12'u, 0xBCDEF123'u, 0xCDEF1234'u,
0xDEF12345'u, 0xEF123456'u, 0xF1234567'u, 0x12345678'u]
var expect = 0'u
for item in values:
expect = expect xor item
check test1() == expect
proc test(): bool =
var soonTest = 0'u
proc callback(udata: pointer) {.gcsafe.} =
soonTest = soonTest xor cast[uint](udata)
callSoon(callback, cast[pointer](0x12345678'u))
callSoon(callback, cast[pointer](0x23456789'u))
callSoon(callback, cast[pointer](0x3456789A'u))
callSoon(callback, cast[pointer](0x456789AB'u))
callSoon(callback, cast[pointer](0x56789ABC'u))
callSoon(callback, cast[pointer](0x6789ABCD'u))
callSoon(callback, cast[pointer](0x789ABCDE'u))
callSoon(callback, cast[pointer](0x89ABCDEF'u))
callSoon(callback, cast[pointer](0x9ABCDEF1'u))
callSoon(callback, cast[pointer](0xABCDEF12'u))
callSoon(callback, cast[pointer](0xBCDEF123'u))
callSoon(callback, cast[pointer](0xCDEF1234'u))
callSoon(callback, cast[pointer](0xDEF12345'u))
callSoon(callback, cast[pointer](0xEF123456'u))
callSoon(callback, cast[pointer](0xF1234567'u))
callSoon(callback, cast[pointer](0x12345678'u))
## All callbacks must be processed exactly with 1 poll() call.
poll()
var values = [0x12345678'u, 0x23456789'u, 0x3456789A'u, 0x456789AB'u,
0x56789ABC'u, 0x6789ABCD'u, 0x789ABCDE'u, 0x89ABCDEF'u,
0x9ABCDEF1'u, 0xABCDEF12'u, 0xBCDEF123'u, 0xCDEF1234'u,
0xDEF12345'u, 0xEF123456'u, 0xF1234567'u, 0x12345678'u]
var expect = 0'u
for item in values:
expect = expect xor item
soonTest == expect
check test() == true
test "`Asynchronous dead end` #7193 test":
var timers, callbacks: int
test2(timers, callbacks)
check:
timers == CallSoonTests
callbacks > CallSoonTests * 2
const CallSoonTests = 5
proc test() =
var
timeoutsTest1 = 0
timeoutsTest2 = 0
stopFlag = false
var callbackproc: proc(udata: pointer) {.gcsafe, raises: [].}
callbackproc = proc (udata: pointer) {.gcsafe, raises: [].} =
timeoutsTest2 += 1
if not(stopFlag):
callSoon(callbackproc)
proc testProc() {.async.} =
for i in 1 .. CallSoonTests:
await sleepAsync(10.milliseconds)
timeoutsTest1 += 1
callSoon(callbackproc)
waitFor(testProc())
stopFlag = true
poll()
check:
timeoutsTest1 == CallSoonTests
timeoutsTest2 > CallSoonTests * 2
test()
test "`callSoon() is not working prior getGlobalDispatcher()` #7192 test":
check test3() == true
proc test(): bool =
var soonTest = 0
proc testCallback(udata: pointer) =
soonTest = 987654321
callSoon(testCallback)
poll()
soonTest == 987654321
check test() == true

View File

@ -1271,15 +1271,23 @@ suite "Stream Transport test suite":
server2.start()
server3.start()
# It works cause even though there's an active listening socket bound to dst3, we are using ReusePort
var transp1 = await connect(server1.local, localAddress = server3.local, flags={SocketFlags.ReusePort})
var transp2 = await connect(server2.local, localAddress = server3.local, flags={SocketFlags.ReusePort})
# It works cause even though there's an active listening socket bound to
# dst3, we are using ReusePort
var transp1 = await connect(
server1.localAddress(), localAddress = server3.localAddress(),
flags = {SocketFlags.ReusePort})
var transp2 = await connect(
server2.localAddress(), localAddress = server3.localAddress(),
flags = {SocketFlags.ReusePort})
expect(TransportOsError):
var transp2 {.used.} = await connect(server2.local, localAddress = server3.local)
var transp2 {.used.} = await connect(
server2.localAddress(), localAddress = server3.localAddress())
expect(TransportOsError):
var transp3 {.used.} = await connect(server2.local, localAddress = initTAddress("::", server3.local.port))
var transp3 {.used.} = await connect(
server2.localAddress(),
localAddress = initTAddress("::", server3.localAddress().port))
await transp1.closeWait()
await transp2.closeWait()
@ -1293,6 +1301,77 @@ suite "Stream Transport test suite":
server3.stop()
await server3.closeWait()
proc testConnectCancelLeaksTest() {.async.} =
proc client(server: StreamServer, transp: StreamTransport) {.async.} =
await transp.closeWait()
let
server = createStreamServer(initTAddress("127.0.0.1:0"), client)
address = server.localAddress()
var counter = 0
while true:
let transpFut = connect(address)
if counter > 0:
await stepsAsync(counter)
if not(transpFut.finished()):
await cancelAndWait(transpFut)
doAssert(cancelled(transpFut),
"Future should be Cancelled at this point")
inc(counter)
else:
let transp = await transpFut
await transp.closeWait()
break
server.stop()
await server.closeWait()
proc testAcceptCancelLeaksTest() {.async.} =
var
counter = 0
exitLoop = false
# This timer will help to awake events poll in case its going to stuck
# usually happens on MacOS.
let sleepFut = sleepAsync(1.seconds)
while not(exitLoop):
let
server = createStreamServer(initTAddress("127.0.0.1:0"))
address = server.localAddress()
let
transpFut = connect(address)
acceptFut = server.accept()
if counter > 0:
await stepsAsync(counter)
exitLoop =
if not(acceptFut.finished()):
await cancelAndWait(acceptFut)
doAssert(cancelled(acceptFut),
"Future should be Cancelled at this point")
inc(counter)
false
else:
let transp = await acceptFut
await transp.closeWait()
true
if not(transpFut.finished()):
await transpFut.cancelAndWait()
if transpFut.completed():
let transp = transpFut.value
await transp.closeWait()
server.stop()
await server.closeWait()
if not(sleepFut.finished()):
await cancelAndWait(sleepFut)
markFD = getCurrentFD()
for i in 0..<len(addresses):
@ -1384,8 +1463,12 @@ suite "Stream Transport test suite":
check waitFor(testReadOnClose(addresses[i])) == true
test "[PIPE] readExactly()/write() test":
check waitFor(testPipe()) == true
test "[IP] bind connect to local address":
test "[IP] bind connect to local address test":
waitFor(testConnectBindLocalAddress())
test "[IP] connect() cancellation leaks test":
waitFor(testConnectCancelLeaksTest())
test "[IP] accept() cancellation leaks test":
waitFor(testAcceptCancelLeaksTest())
test "Leaks test":
checkLeaks()
test "File descriptors leak test":

View File

@ -150,9 +150,9 @@ suite "Asynchronous sync primitives test suite":
var fut2 = task(lock, 2, n2)
var fut3 = task(lock, 3, n3)
if cancelIndex == 2:
fut2.cancel()
fut2.cancelSoon()
else:
fut3.cancel()
fut3.cancelSoon()
await allFutures(fut1, fut2, fut3)
result = stripe

View File

@ -91,26 +91,36 @@ suite "Asynchronous timers & steps test suite":
$nanoseconds(1_800_000_600) == "1s800ms600ns"
test "Asynchronous steps test":
var futn1 = stepsAsync(-1)
var fut0 = stepsAsync(0)
var fut1 = stepsAsync(1)
var fut2 = stepsAsync(2)
var fut3 = stepsAsync(3)
check:
futn1.completed() == true
fut0.completed() == true
fut1.completed() == false
fut2.completed() == false
fut3.completed() == false
poll()
# We need `fut` because `stepsAsync` do not power `poll()` anymore.
block:
var fut {.used.} = sleepAsync(50.milliseconds)
poll()
check:
fut1.completed() == true
fut2.completed() == false
fut3.completed() == false
poll()
block:
var fut {.used.} = sleepAsync(50.milliseconds)
poll()
check:
fut2.completed() == true
fut3.completed() == false
poll()
block:
var fut {.used.} = sleepAsync(50.milliseconds)
poll()
check:
fut3.completed() == true

View File

@ -56,7 +56,7 @@ suite "Asynchronous utilities test suite":
check:
getCount() == 1'u
pendingFuturesCount() == 1'u
fut3.cancel()
discard fut3.tryCancel()
poll()
check:
getCount() == 0'u