Skip to content

Commit d184a92

Browse files
authored
Fix rare cancellation race issue on timeout for wait/withTimeout. (#536)
Add tests.
1 parent 7a3eaff commit d184a92

File tree

2 files changed

+163
-11
lines changed

2 files changed

+163
-11
lines changed

chronos/internal/asyncfutures.nim

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,18 +1466,25 @@ proc withTimeout*[T](fut: Future[T], timeout: Duration): Future[bool] {.
14661466
timer: TimerCallback
14671467
timeouted = false
14681468
1469-
template completeFuture(fut: untyped): untyped =
1469+
template completeFuture(fut: untyped, timeout: bool): untyped =
14701470
if fut.failed() or fut.completed():
14711471
retFuture.complete(true)
14721472
else:
1473-
retFuture.cancelAndSchedule()
1473+
if timeout:
1474+
retFuture.complete(false)
1475+
else:
1476+
retFuture.cancelAndSchedule()
14741477
14751478
# TODO: raises annotation shouldn't be needed, but likely similar issue as
14761479
# https://github.com/nim-lang/Nim/issues/17369
14771480
proc continuation(udata: pointer) {.gcsafe, raises: [].} =
14781481
if not(retFuture.finished()):
14791482
if timeouted:
1480-
retFuture.complete(false)
1483+
# We should not unconditionally complete result future with `false`.
1484+
# Initiated by timeout handler cancellation could fail, in this case
1485+
# we could get `fut` in complete or in failed state, so we should
1486+
# complete result future with `true` instead of `false` here.
1487+
fut.completeFuture(timeouted)
14811488
return
14821489
if not(fut.finished()):
14831490
# Timer exceeded first, we going to cancel `fut` and wait until it
@@ -1488,7 +1495,7 @@ proc withTimeout*[T](fut: Future[T], timeout: Duration): Future[bool] {.
14881495
# Future `fut` completed/failed/cancelled first.
14891496
if not(isNil(timer)):
14901497
clearTimer(timer)
1491-
fut.completeFuture()
1498+
fut.completeFuture(false)
14921499
timer = nil
14931500
14941501
# TODO: raises annotation shouldn't be needed, but likely similar issue as
@@ -1499,7 +1506,7 @@ proc withTimeout*[T](fut: Future[T], timeout: Duration): Future[bool] {.
14991506
clearTimer(timer)
15001507
fut.cancelSoon()
15011508
else:
1502-
fut.completeFuture()
1509+
fut.completeFuture(false)
15031510
timer = nil
15041511
15051512
if fut.finished():
@@ -1528,11 +1535,14 @@ proc waitImpl[F: SomeFuture](fut: F, retFuture: auto, timeout: Duration): auto =
15281535
timer: TimerCallback
15291536
timeouted = false
15301537
1531-
template completeFuture(fut: untyped): untyped =
1538+
template completeFuture(fut: untyped, timeout: bool): untyped =
15321539
if fut.failed():
15331540
retFuture.fail(fut.error(), warn = false)
15341541
elif fut.cancelled():
1535-
retFuture.cancelAndSchedule()
1542+
if timeout:
1543+
retFuture.fail(newException(AsyncTimeoutError, "Timeout exceeded!"))
1544+
else:
1545+
retFuture.cancelAndSchedule()
15361546
else:
15371547
when type(fut).T is void:
15381548
retFuture.complete()
@@ -1542,7 +1552,11 @@ proc waitImpl[F: SomeFuture](fut: F, retFuture: auto, timeout: Duration): auto =
15421552
proc continuation(udata: pointer) {.raises: [].} =
15431553
if not(retFuture.finished()):
15441554
if timeouted:
1545-
retFuture.fail(newException(AsyncTimeoutError, "Timeout exceeded!"))
1555+
# We should not unconditionally fail `retFuture` with
1556+
# `AsyncTimeoutError`. Initiated by timeout handler cancellation
1557+
# could fail, in this case we could get `fut` in complete or in failed
1558+
# state, so we should return error/value instead of `AsyncTimeoutError`.
1559+
fut.completeFuture(timeouted)
15461560
return
15471561
if not(fut.finished()):
15481562
# Timer exceeded first.
@@ -1552,7 +1566,7 @@ proc waitImpl[F: SomeFuture](fut: F, retFuture: auto, timeout: Duration): auto =
15521566
# Future `fut` completed/failed/cancelled first.
15531567
if not(isNil(timer)):
15541568
clearTimer(timer)
1555-
fut.completeFuture()
1569+
fut.completeFuture(false)
15561570
timer = nil
15571571
15581572
var cancellation: proc(udata: pointer) {.gcsafe, raises: [].}
@@ -1562,12 +1576,12 @@ proc waitImpl[F: SomeFuture](fut: F, retFuture: auto, timeout: Duration): auto =
15621576
clearTimer(timer)
15631577
fut.cancelSoon()
15641578
else:
1565-
fut.completeFuture()
1579+
fut.completeFuture(false)
15661580
15671581
timer = nil
15681582
15691583
if fut.finished():
1570-
fut.completeFuture()
1584+
fut.completeFuture(false)
15711585
else:
15721586
if timeout.isZero():
15731587
retFuture.fail(newException(AsyncTimeoutError, "Timeout exceeded!"))

tests/testfut.nim

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2177,3 +2177,141 @@ suite "Future[T] behavior test suite":
21772177
check:
21782178
not compiles(Future[void].Raising([42]))
21792179
not compiles(Future[void].Raising(42))
2180+
2181+
asyncTest "Timeout/cancellation race wait() test":
2182+
proc raceTest(T: typedesc, itype: int) {.async.} =
2183+
let monitorFuture = newFuture[T]("monitor",
2184+
{FutureFlag.OwnCancelSchedule})
2185+
2186+
proc raceProc0(future: Future[T]): Future[T] {.async.} =
2187+
await future
2188+
proc raceProc1(future: Future[T]): Future[T] {.async.} =
2189+
await raceProc0(future)
2190+
proc raceProc2(future: Future[T]): Future[T] {.async.} =
2191+
await raceProc1(future)
2192+
2193+
proc activation(udata: pointer) {.gcsafe.} =
2194+
if itype == 0:
2195+
when T is void:
2196+
monitorFuture.complete()
2197+
elif T is int:
2198+
monitorFuture.complete(100)
2199+
elif itype == 1:
2200+
monitorFuture.fail(newException(ValueError, "test"))
2201+
else:
2202+
monitorFuture.cancelAndSchedule()
2203+
2204+
monitorFuture.cancelCallback = activation
2205+
let
2206+
testFut = raceProc2(monitorFuture)
2207+
waitFut = wait(testFut, 10.milliseconds)
2208+
2209+
when T is void:
2210+
let waitRes =
2211+
try:
2212+
await waitFut
2213+
if itype == 0:
2214+
true
2215+
else:
2216+
false
2217+
except CancelledError:
2218+
false
2219+
except CatchableError:
2220+
if itype != 0:
2221+
true
2222+
else:
2223+
false
2224+
check waitRes == true
2225+
elif T is int:
2226+
let waitRes =
2227+
try:
2228+
let res = await waitFut
2229+
if itype == 0:
2230+
(true, res)
2231+
else:
2232+
(false, -1)
2233+
except CancelledError:
2234+
(false, -1)
2235+
except CatchableError:
2236+
if itype != 0:
2237+
(true, 0)
2238+
else:
2239+
(false, -1)
2240+
if itype == 0:
2241+
check:
2242+
waitRes[0] == true
2243+
waitRes[1] == 100
2244+
else:
2245+
check:
2246+
waitRes[0] == true
2247+
2248+
await raceTest(void, 0)
2249+
await raceTest(void, 1)
2250+
await raceTest(void, 2)
2251+
await raceTest(int, 0)
2252+
await raceTest(int, 1)
2253+
await raceTest(int, 2)
2254+
2255+
asyncTest "Timeout/cancellation race withTimeout() test":
2256+
proc raceTest(T: typedesc, itype: int) {.async.} =
2257+
let monitorFuture = newFuture[T]("monitor",
2258+
{FutureFlag.OwnCancelSchedule})
2259+
2260+
proc raceProc0(future: Future[T]): Future[T] {.async.} =
2261+
await future
2262+
proc raceProc1(future: Future[T]): Future[T] {.async.} =
2263+
await raceProc0(future)
2264+
proc raceProc2(future: Future[T]): Future[T] {.async.} =
2265+
await raceProc1(future)
2266+
2267+
proc activation(udata: pointer) {.gcsafe.} =
2268+
if itype == 0:
2269+
when T is void:
2270+
monitorFuture.complete()
2271+
elif T is int:
2272+
monitorFuture.complete(100)
2273+
elif itype == 1:
2274+
monitorFuture.fail(newException(ValueError, "test"))
2275+
else:
2276+
monitorFuture.cancelAndSchedule()
2277+
2278+
monitorFuture.cancelCallback = activation
2279+
let
2280+
testFut = raceProc2(monitorFuture)
2281+
waitFut = withTimeout(testFut, 10.milliseconds)
2282+
2283+
when T is void:
2284+
let waitRes =
2285+
try:
2286+
await waitFut
2287+
except CancelledError:
2288+
false
2289+
except CatchableError:
2290+
false
2291+
if itype == 0:
2292+
check waitRes == true
2293+
elif itype == 1:
2294+
check waitRes == true
2295+
else:
2296+
check waitRes == false
2297+
elif T is int:
2298+
let waitRes =
2299+
try:
2300+
await waitFut
2301+
except CancelledError:
2302+
false
2303+
except CatchableError:
2304+
false
2305+
if itype == 0:
2306+
check waitRes == true
2307+
elif itype == 1:
2308+
check waitRes == true
2309+
else:
2310+
check waitRes == false
2311+
2312+
await raceTest(void, 0)
2313+
await raceTest(void, 1)
2314+
await raceTest(void, 2)
2315+
await raceTest(int, 0)
2316+
await raceTest(int, 1)
2317+
await raceTest(int, 2)

0 commit comments

Comments
 (0)