for the first problem, i found and read this issue; as i could not understand how addTimer can rescue it, i just ignored this specific exception ... (i tried to add addTimer but it's a shame i didn't know how)
for the second problem, i tried to add a break in the try: poll..catch block, the output did change, but "the future is still running after called .fail" remains.
thanks for help.
import std/[asyncdispatch, times, asyncfutures]
import std/[strutils, strformat]
type Canceled = object of CatchableError
proc ticker(ident, stop, interval: int) {.async.} =
var remain = stop
while remain > 0:
echo &"tick#{ident}: ", now()
dec remain
await sleepAsync(interval)
echo &"tick#{ident}: exiting"
proc waitFor[T](timeout: int, fut: Future[T]): T =
let deadline = epochTime() + timeout.float
var timedOut = true
while epochTime() < deadline:
if fut.finished:
timedOut = false
break
try:
poll()
except:
let e = getCurrentException()
if not e.msg.startsWith("An attempt was made to complete a Future more than once."):
raise e
# break
if timedOut:
fut.fail newException(Canceled, "canceled due to deadline")
fut.read
proc main: int =
try:
1.waitFor ticker(1, 3, 500)
except Canceled:
let err = getCurrentException()
echo "ticker has been canceled ", err.msg
5.waitFor ticker(2, 3, 500)
when isMainModule:
quit main()
output:
tick#1: 2022-02-12T17:05:21+08:00
tick#1: 2022-02-12T17:05:21+08:00
tick#1: 2022-02-12T17:05:22+08:00
ticker has been canceled canceled due to deadline
Async traceback:
Exception message: canceled due to deadline
tick#2: 2022-02-12T17:05:22+08:00
tick#1: exiting
tick#2: 2022-02-12T17:05:22+08:00
tick#2: 2022-02-12T17:05:23+08:00
tick#2: exiting
What you're looking for is cancellation support - you can find it in chronos, but not in asyncdispatch, last I checked.
That said, even if you cancel something, it might run or might already have run and you just don't know about it yet - this is the nature of async programming.
For the special case of cancelling timers, that is futures returned by sleepAsync, I have an implementation. It is a bit hacky though as it manipulates the dispatcher's timer list:
proc cancelTimer*(timer: Future[void], error: ref CatchableError) =
let dispatcher = getGlobalDispatcher()
if dispatcher.timers.len() > 0:
for i in 0 ..< dispatcher.timers.len():
if dispatcher.timers[i].fut == timer:
dispatcher.timers[i].fut.fail(error)
dispatcher.timers.del(i)
return
With this, I would implement your ticker example like this:
import std/[asyncdispatch, times, asyncfutures]
import std/[strutils, strformat]
import std/heapqueue
type Canceled = object of CatchableError
type Ticker = ref object
ident: int
stop: int
interval: int
timer: Future[void]
canceled: bool
proc cancelTimer*(timer: Future[void], error: ref CatchableError) =
let dispatcher = getGlobalDispatcher()
if dispatcher.timers.len() > 0:
for i in 0 ..< dispatcher.timers.len():
if dispatcher.timers[i].fut == timer:
dispatcher.timers[i].fut.fail(error)
dispatcher.timers.del(i)
return
proc start(ticker: Ticker) {.async.} =
var remain = ticker.stop
while remain > 0:
if ticker.canceled:
break
echo &"tick#{ticker.ident}: ", now()
dec remain
ticker.timer = sleepAsync(ticker.interval)
try:
await ticker.timer
except Canceled:
break
echo &"tick#{ticker.ident}: exiting"
proc cancel(ticker: Ticker) =
cancelTimer(ticker.timer, newException(Canceled, "timer was canceled"))
ticker.canceled = true
proc cancelAfter(ticker: Ticker, timeout: int) {.async.} =
await sleepAsync(timeout)
ticker.cancel()
proc run(ticker1, ticker2: Ticker) {.async.} =
asyncCheck ticker1.cancelAfter(1000)
await ticker1.start()
asyncCheck ticker2.cancelAfter(5000)
await ticker2.start()
proc main: int =
asyncCheck run(Ticker(ident: 1, stop: 3, interval: 500, canceled: false),
Ticker(ident: 2, stop: 3, interval: 500, canceled: false))
while hasPendingOperations():
poll(high(int))
when isMainModule:
quit main()
@dom96: I don't understand how this could work. I don't think there are FDs involved in the timer implementation, at least on posix.
To my solution I should add, that it only works on posix systems, not on Windows.
@dom96: Oh, I see. I think that would require writing a custom addTimer that returns the FD though. How about changing addTimer to
proc addTimer*(timeout: int, oneshot: bool, cb: Callback): AsyncFD {.discardable.}?
(and making sure proc unregister(fd: AsyncFD) handles timer FDs correctly)
thanks for the suggestion; as just starting to learn nim, i did not find time to look into status-im/*, but i definitely will. when the interest comes to me, i was thought "just look how simple asyncdispatch.waitFor is, i surely can implement it simply".
That said, even if you cancel something, it might run or might already have run and you just don't know about it yet - this is the nature of async programming.
but this can not explain why the line tick#1: exiting came out when .fail has been called, does it?
The only way you can cancel operations right now in Nim's async is by closing the FD they are pending on.
i will imprint it in my mind. so it means .fail should but do not behave like it claimed Completes future with error..
it seems there are some minefields in nim's async, and i can find warning nowhere: the manual, the doc of api, the tutorials of people.
i yet have one more question when i said minefield: why proc foo {.async.} = must have a return (in my use) ?
thanks for the working implementation. it goes too deep for me. i start to miss python's trio:
with trio.move_on_after(30):
result = await do_http_get("https://...")
print("result is", result)
print("with block finished")
let me expand the last question, below snippet will err because the missing return
proc main: Future[int] {.async.} =
await sleepAsync(0)
1