Fix behavior of wait() and withTimeout() calls to cancel and wait for result of cancelled Future[T].

Add tests.
This commit is contained in:
cheatfate 2020-11-17 11:59:02 +02:00 committed by zah
parent 7fd1a0f635
commit 493cb1dbfd
2 changed files with 83 additions and 24 deletions

View File

@ -847,19 +847,23 @@ proc withTimeout*[T](fut: Future[T], timeout: Duration): Future[bool] =
var retFuture = newFuture[bool]("chronos.`withTimeout`")
var moment: Moment
var timer: TimerCallback
var cancelling = false
proc continuation(udata: pointer) {.gcsafe.} =
if not(retFuture.finished()):
if not(fut.finished()):
# Timer exceeded first.
fut.removeCallback(continuation)
fut.cancel()
retFuture.complete(false)
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:
# Future `fut` completed/failed/cancelled first.
if not isNil(timer):
clearTimer(timer)
retFuture.complete(true)
retFuture.complete(false)
proc cancellation(udata: pointer) {.gcsafe.} =
if not isNil(timer):
@ -900,26 +904,29 @@ proc wait*[T](fut: Future[T], timeout = InfiniteDuration): Future[T] =
var retFuture = newFuture[T]("chronos.wait()")
var moment: Moment
var timer: TimerCallback
var cancelling = false
proc continuation(udata: pointer) {.gcsafe.} =
if not(retFuture.finished()):
if not(fut.finished()):
# Timer exceeded first.
fut.removeCallback(continuation)
fut.cancel()
retFuture.fail(newException(AsyncTimeoutError, "Timeout exceeded!"))
else:
# Future `fut` completed/failed/cancelled first.
if not isNil(timer):
clearTimer(timer)
if fut.failed():
retFuture.fail(fut.error)
if not(cancelling):
if not(fut.finished()):
# Timer exceeded first.
cancelling = true
fut.cancel()
else:
when T is void:
retFuture.complete()
# Future `fut` completed/failed/cancelled first.
if not isNil(timer):
clearTimer(timer)
if fut.failed():
retFuture.fail(fut.error)
else:
retFuture.complete(fut.read())
when T is void:
retFuture.complete()
else:
retFuture.complete(fut.read())
else:
retFuture.fail(newException(AsyncTimeoutError, "Timeout exceeded!"))
proc cancellation(udata: pointer) {.gcsafe.} =
if not isNil(timer):

View File

@ -1067,6 +1067,52 @@ suite "Future[T] behavior test suite":
r10 and r11 and r20 and r21 and r30 and r31
proc testWithTimeoutCancelAndWait(): bool =
proc futureNeverEnds(): Future[void] =
newFuture[void]("neverending.future")
proc futureOneLevelMore() {.async.} =
await futureNeverEnds()
proc testWithTimeout(): Future[bool] {.async.} =
var fut = futureOneLevelMore()
try:
let res = await withTimeout(fut, 100.milliseconds)
# Because `fut` is never-ending Future[T], `withTimeout` should return
# `false` but it also has to wait until `fut` is cancelled.
if not(res) and fut.cancelled():
return true
else:
return false
except CatchableError:
return false
waitFor testWithTimeout()
proc testWaitCancelAndWait(): bool =
proc futureNeverEnds(): Future[void] =
newFuture[void]("neverending.future")
proc futureOneLevelMore() {.async.} =
await futureNeverEnds()
proc testWait(): Future[bool] {.async.} =
var fut = futureOneLevelMore()
try:
await wait(fut, 100.milliseconds)
return false
except AsyncTimeoutError:
# Because `fut` is never-ending Future[T], `wait` should raise
# `AsyncTimeoutError`, but only after `fut` is cancelled.
if fut.cancelled():
return true
else:
return false
except CatchableError:
return false
waitFor testWait()
test "Async undefined behavior (#7758) test":
check test1() == true
test "Immediately completed asynchronous procedure test":
@ -1126,3 +1172,9 @@ suite "Future[T] behavior test suite":
test "location test":
check testSrcLocation() == true
test "withTimeout(fut) should wait cancellation test":
check testWithTimeoutCancelAndWait()
test "wait(fut) should wait cancellation test":
check testWaitCancelAndWait()