diff --git a/chronos.nimble b/chronos.nimble index 84c8aad..a9aa33e 100644 --- a/chronos.nimble +++ b/chronos.nimble @@ -1,5 +1,5 @@ packageName = "chronos" -version = "2.5.1" +version = "2.5.2" author = "Status Research & Development GmbH" description = "Chronos" license = "Apache License 2.0 or MIT" diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index d24cc1b..5e812c5 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -512,7 +512,48 @@ proc asyncCheck*[T](future: Future[T]) = raise future.error future.callback = cb -proc asyncDiscard*[T](future: Future[T]) = discard +proc asyncSpawn*(future: Future[void]) = + ## Spawns a new concurrent async task. + ## + ## Tasks may not raise exceptions or be cancelled - a ``Defect`` will be + ## raised when this happens. + ## + ## This should be used instead of ``discard`` and ``asyncCheck`` when calling + ## an ``async`` procedure without ``await``, to ensure exceptions in the + ## returned future are not silently discarded. + ## + ## Note, that if passed ``future`` is already finished, it will be checked + ## and processed immediately. + doAssert(not isNil(future), "Future is nil") + + template getFutureLocation(): string = + let loc = future.location[0] + "[" & ( + if len(loc.procedure) == 0: "[unspecified]" else: $loc.procedure & "()" + ) & " at " & $loc.file & ":" & $(loc.line) & "]" + + template getErrorMessage(): string = + "Asynchronous task " & getFutureLocation() & + " finished with an exception \"" & $future.error.name & "\"!" + template getCancelMessage(): string = + "Asynchronous task " & getFutureLocation() & " was cancelled!" + + proc cb(data: pointer) = + if future.failed(): + raise newException(FutureDefect, getErrorMessage()) + elif future.cancelled(): + raise newException(FutureDefect, getCancelMessage()) + + if not(future.finished()): + # We adding completion callback only if ``future`` is not finished yet. + future.addCallback(cb) + else: + if future.failed(): + raise newException(FutureDefect, getErrorMessage()) + elif future.cancelled(): + raise newException(FutureDefect, getCancelMessage()) + +proc asyncDiscard*[T](future: Future[T]) {.deprecated.} = discard ## This is async workaround for discard ``Future[T]``. proc `and`*[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] {. diff --git a/tests/testfut.nim b/tests/testfut.nim index d0e4bc6..d1ed96c 100644 --- a/tests/testfut.nim +++ b/tests/testfut.nim @@ -969,6 +969,58 @@ suite "Future[T] behavior test suite": proc testCancellationRace(): bool = waitFor(testCancellationRaceAsync()) + + proc testAsyncSpawnAsync(): Future[bool] {.async.} = + + proc completeTask1() {.async.} = + discard + + proc completeTask2() {.async.} = + await sleepAsync(100.milliseconds) + + proc errorTask() {.async.} = + if true: + raise newException(ValueError, "") + + proc cancelTask() {.async.} = + await sleepAsync(10.seconds) + + try: + var fut1 = completeTask1() + var fut2 = completeTask2() + asyncSpawn fut1 + asyncSpawn fut2 + await sleepAsync(200.milliseconds) + if not(fut1.finished()) or not(fut2.finished()): + return false + if fut1.failed() or fut1.cancelled() or fut2.failed() or fut2.cancelled(): + return false + except: + return false + + try: + asyncSpawn errorTask() + return false + except FutureDefect: + discard + except: + return false + + try: + var fut = cancelTask() + await cancelAndWait(fut) + asyncSpawn fut + return false + except FutureDefect: + discard + except: + return false + + return true + + proc testAsyncSpawn(): bool = + waitFor(testAsyncSpawnAsync()) + test "Async undefined behavior (#7758) test": check test1() == true test "Immediately completed asynchronous procedure test": @@ -1022,3 +1074,6 @@ suite "Future[T] behavior test suite": check testWithTimeout() == true test "Cancellation race test": check testCancellationRace() == true + + test "asyncSpawn() test": + check testAsyncSpawn() == true