Skip to content

Commit 1306170

Browse files
authored
dedicated exceptions for Future.read failures (#474)
Dedicated exceptions for `read` failures reduce the risk of mixing up "user" exceptions with those of Future itself. The risk still exists, if the user allows a chronos exception to bubble up explicitly. Because `await` structurally guarantees that the Future is not `pending` at the time of `read`, it does not raise this new exception. * introduce `FuturePendingError` and `FutureCompletedError` when `read`:ing a future of uncertain state * fix `waitFor` / `read` to return `lent` values * simplify code generation for `void`-returning async procs * document `Raising` type helper
1 parent f5ff9e3 commit 1306170

File tree

7 files changed

+309
-161
lines changed

7 files changed

+309
-161
lines changed

chronos/futures.nim

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,15 @@ type
7373
cause*: FutureBase
7474

7575
FutureError* = object of CatchableError
76+
future*: FutureBase
7677

7778
CancelledError* = object of FutureError
7879
## Exception raised when accessing the value of a cancelled future
7980

81+
func raiseFutureDefect(msg: static string, fut: FutureBase) {.
82+
noinline, noreturn.} =
83+
raise (ref FutureDefect)(msg: msg, cause: fut)
84+
8085
when chronosFutureId:
8186
var currentID* {.threadvar.}: uint
8287
template id*(fut: FutureBase): uint = fut.internalId
@@ -202,27 +207,23 @@ func value*[T: not void](future: Future[T]): lent T =
202207
## Return the value in a completed future - raises Defect when
203208
## `fut.completed()` is `false`.
204209
##
205-
## See `read` for a version that raises an catchable error when future
210+
## See `read` for a version that raises a catchable error when future
206211
## has not completed.
207212
when chronosStrictFutureAccess:
208213
if not future.completed():
209-
raise (ref FutureDefect)(
210-
msg: "Future not completed while accessing value",
211-
cause: future)
214+
raiseFutureDefect("Future not completed while accessing value", future)
212215

213216
future.internalValue
214217

215218
func value*(future: Future[void]) =
216219
## Return the value in a completed future - raises Defect when
217220
## `fut.completed()` is `false`.
218221
##
219-
## See `read` for a version that raises an catchable error when future
222+
## See `read` for a version that raises a catchable error when future
220223
## has not completed.
221224
when chronosStrictFutureAccess:
222225
if not future.completed():
223-
raise (ref FutureDefect)(
224-
msg: "Future not completed while accessing value",
225-
cause: future)
226+
raiseFutureDefect("Future not completed while accessing value", future)
226227

227228
func error*(future: FutureBase): ref CatchableError =
228229
## Return the error of `future`, or `nil` if future did not fail.
@@ -231,9 +232,8 @@ func error*(future: FutureBase): ref CatchableError =
231232
## future has not failed.
232233
when chronosStrictFutureAccess:
233234
if not future.failed() and not future.cancelled():
234-
raise (ref FutureDefect)(
235-
msg: "Future not failed/cancelled while accessing error",
236-
cause: future)
235+
raiseFutureDefect(
236+
"Future not failed/cancelled while accessing error", future)
237237

238238
future.internalError
239239

chronos/internal/asyncfutures.nim

Lines changed: 144 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
# Apache License, version 2.0, (LICENSE-APACHEv2)
99
# MIT license (LICENSE-MIT)
1010

11+
## Features and utilities for `Future` that integrate it with the dispatcher
12+
## and the rest of the async machinery
13+
1114
{.push raises: [].}
1215

1316
import std/[sequtils, macros]
@@ -45,15 +48,28 @@ func `[]`*(loc: array[LocationKind, ptr SrcLoc], v: int): ptr SrcLoc {.
4548

4649
type
4750
FutureStr*[T] = ref object of Future[T]
48-
## Future to hold GC strings
51+
## Deprecated
4952
gcholder*: string
5053

5154
FutureSeq*[A, B] = ref object of Future[A]
52-
## Future to hold GC seqs
55+
## Deprecated
5356
gcholder*: seq[B]
5457

58+
FuturePendingError* = object of FutureError
59+
## Error raised when trying to `read` a Future that is still pending
60+
FutureCompletedError* = object of FutureError
61+
## Error raised when trying access the error of a completed Future
62+
5563
SomeFuture = Future|InternalRaisesFuture
5664

65+
func raiseFuturePendingError(fut: FutureBase) {.
66+
noinline, noreturn, raises: FuturePendingError.} =
67+
raise (ref FuturePendingError)(msg: "Future is still pending", future: fut)
68+
func raiseFutureCompletedError(fut: FutureBase) {.
69+
noinline, noreturn, raises: FutureCompletedError.} =
70+
raise (ref FutureCompletedError)(
71+
msg: "Future is completed, cannot read error", future: fut)
72+
5773
# Backwards compatibility for old FutureState name
5874
template Finished* {.deprecated: "Use Completed instead".} = Completed
5975
template Finished*(T: type FutureState): FutureState {.
@@ -479,6 +495,10 @@ macro internalCheckComplete*(fut: InternalRaisesFuture, raises: typed) =
479495
# generics are lost - so instead, we pass the raises list explicitly
480496

481497
let types = getRaisesTypes(raises)
498+
types.copyLineInfo(raises)
499+
for t in types:
500+
t.copyLineInfo(raises)
501+
482502
if isNoRaises(types):
483503
return quote do:
484504
if not(isNil(`fut`.internalError)):
@@ -497,8 +517,8 @@ macro internalCheckComplete*(fut: InternalRaisesFuture, raises: typed) =
497517
quote do: discard
498518
),
499519
nnkElseExpr.newTree(
500-
nnkRaiseStmt.newNimNode(lineInfoFrom=fut).add(
501-
quote do: (`fut`.internalError)
520+
nnkRaiseStmt.newTree(
521+
nnkDotExpr.newTree(fut, ident "internalError")
502522
)
503523
)
504524
)
@@ -520,39 +540,51 @@ macro internalCheckComplete*(fut: InternalRaisesFuture, raises: typed) =
520540
ifRaise
521541
)
522542

523-
proc read*[T: not void](future: Future[T] ): lent T {.raises: [CatchableError].} =
524-
## Retrieves the value of ``future``. Future must be finished otherwise
525-
## this function will fail with a ``ValueError`` exception.
543+
proc readFinished[T: not void](fut: Future[T]): lent T {.
544+
raises: [CatchableError].} =
545+
# Read a future that is known to be finished, avoiding the extra exception
546+
# effect.
547+
internalCheckComplete(fut)
548+
fut.internalValue
549+
550+
proc read*[T: not void](fut: Future[T] ): lent T {.raises: [CatchableError].} =
551+
## Retrieves the value of `fut`.
526552
##
527-
## If the result of the future is an error then that error will be raised.
528-
if not future.finished():
529-
# TODO: Make a custom exception type for this?
530-
raise newException(ValueError, "Future still in progress.")
553+
## If the future failed or was cancelled, the corresponding exception will be
554+
## raised.
555+
##
556+
## If the future is still pending, `FuturePendingError` will be raised.
557+
if not fut.finished():
558+
raiseFuturePendingError(fut)
531559

532-
internalCheckComplete(future)
533-
future.internalValue
560+
fut.readFinished()
534561

535-
proc read*(future: Future[void] ) {.raises: [CatchableError].} =
536-
## Retrieves the value of ``future``. Future must be finished otherwise
537-
## this function will fail with a ``ValueError`` exception.
562+
proc read*(fut: Future[void]) {.raises: [CatchableError].} =
563+
## Checks that `fut` completed.
538564
##
539-
## If the result of the future is an error then that error will be raised.
540-
if future.finished():
541-
internalCheckComplete(future)
542-
else:
543-
# TODO: Make a custom exception type for this?
544-
raise newException(ValueError, "Future still in progress.")
565+
## If the future failed or was cancelled, the corresponding exception will be
566+
## raised.
567+
##
568+
## If the future is still pending, `FuturePendingError` will be raised.
569+
if not fut.finished():
570+
raiseFuturePendingError(fut)
571+
572+
internalCheckComplete(fut)
545573

546-
proc readError*(future: FutureBase): ref CatchableError {.raises: [ValueError].} =
547-
## Retrieves the exception stored in ``future``.
574+
proc readError*(fut: FutureBase): ref CatchableError {.raises: [FutureError].} =
575+
## Retrieves the exception of the failed or cancelled `fut`.
548576
##
549-
## An ``ValueError`` exception will be thrown if no exception exists
550-
## in the specified Future.
551-
if not(isNil(future.error)):
552-
return future.error
553-
else:
554-
# TODO: Make a custom exception type for this?
555-
raise newException(ValueError, "No error in future.")
577+
## If the future was completed with a value, `FutureCompletedError` will be
578+
## raised.
579+
##
580+
## If the future is still pending, `FuturePendingError` will be raised.
581+
if not fut.finished():
582+
raiseFuturePendingError(fut)
583+
584+
if isNil(fut.error):
585+
raiseFutureCompletedError(fut)
586+
587+
fut.error
556588

557589
template taskFutureLocation(future: FutureBase): string =
558590
let loc = future.location[LocationKind.Create]
@@ -568,18 +600,46 @@ template taskErrorMessage(future: FutureBase): string =
568600
template taskCancelMessage(future: FutureBase): string =
569601
"Asynchronous task " & taskFutureLocation(future) & " was cancelled!"
570602

571-
proc waitFor*[T](fut: Future[T]): T {.raises: [CatchableError].} =
572-
## **Blocks** the current thread until the specified future finishes and
573-
## reads it, potentially raising an exception if the future failed or was
574-
## cancelled.
575-
var finished = false
576-
# Ensure that callbacks currently scheduled on the future run before returning
577-
proc continuation(udata: pointer) {.gcsafe.} = finished = true
603+
proc pollFor[F: Future | InternalRaisesFuture](fut: F): F {.raises: [].} =
604+
# Blocks the current thread of execution until `fut` has finished, returning
605+
# the given future.
606+
#
607+
# Must not be called recursively (from inside `async` procedures).
608+
#
609+
# See alse `awaitne`.
578610
if not(fut.finished()):
611+
var finished = false
612+
# Ensure that callbacks currently scheduled on the future run before returning
613+
proc continuation(udata: pointer) {.gcsafe.} = finished = true
579614
fut.addCallback(continuation)
615+
580616
while not(finished):
581617
poll()
582-
fut.read()
618+
619+
fut
620+
621+
proc waitFor*[T: not void](fut: Future[T]): lent T {.raises: [CatchableError].} =
622+
## Blocks the current thread of execution until `fut` has finished, returning
623+
## its value.
624+
##
625+
## If the future failed or was cancelled, the corresponding exception will be
626+
## raised.
627+
##
628+
## Must not be called recursively (from inside `async` procedures).
629+
##
630+
## See also `await`, `Future.read`
631+
pollFor(fut).readFinished()
632+
633+
proc waitFor*(fut: Future[void]) {.raises: [CatchableError].} =
634+
## Blocks the current thread of execution until `fut` has finished.
635+
##
636+
## If the future failed or was cancelled, the corresponding exception will be
637+
## raised.
638+
##
639+
## Must not be called recursively (from inside `async` procedures).
640+
##
641+
## See also `await`, `Future.read`
642+
pollFor(fut).internalCheckComplete()
583643

584644
proc asyncSpawn*(future: Future[void]) =
585645
## Spawns a new concurrent async task.
@@ -943,7 +1003,7 @@ proc cancelAndWait*(future: FutureBase, loc: ptr SrcLoc): Future[void] {.
9431003

9441004
retFuture
9451005

946-
template cancelAndWait*(future: FutureBase): Future[void] =
1006+
template cancelAndWait*(future: FutureBase): Future[void].Raising([CancelledError]) =
9471007
## Cancel ``future``.
9481008
cancelAndWait(future, getSrcLocation())
9491009

@@ -1500,37 +1560,56 @@ when defined(windows):
15001560

15011561
{.pop.} # Automatically deduced raises from here onwards
15021562

1503-
proc waitFor*[T, E](fut: InternalRaisesFuture[T, E]): T = # {.raises: [E]}
1504-
## **Blocks** the current thread until the specified future finishes and
1505-
## reads it, potentially raising an exception if the future failed or was
1506-
## cancelled.
1507-
while not(fut.finished()):
1508-
poll()
1563+
proc readFinished[T: not void; E](fut: InternalRaisesFuture[T, E]): lent T =
1564+
internalCheckComplete(fut, E)
1565+
fut.internalValue
1566+
1567+
proc read*[T: not void, E](fut: InternalRaisesFuture[T, E]): lent T = # {.raises: [E, FuturePendingError].}
1568+
## Retrieves the value of `fut`.
1569+
##
1570+
## If the future failed or was cancelled, the corresponding exception will be
1571+
## raised.
1572+
##
1573+
## If the future is still pending, `FuturePendingError` will be raised.
1574+
if not fut.finished():
1575+
raiseFuturePendingError(fut)
15091576

1510-
fut.read()
1577+
fut.readFinished()
15111578

1512-
proc read*[T: not void, E](future: InternalRaisesFuture[T, E]): lent T = # {.raises: [E, ValueError].}
1513-
## Retrieves the value of ``future``. Future must be finished otherwise
1514-
## this function will fail with a ``ValueError`` exception.
1579+
proc read*[E](fut: InternalRaisesFuture[void, E]) = # {.raises: [E].}
1580+
## Checks that `fut` completed.
15151581
##
1516-
## If the result of the future is an error then that error will be raised.
1517-
if not future.finished():
1518-
# TODO: Make a custom exception type for this?
1519-
raise newException(ValueError, "Future still in progress.")
1582+
## If the future failed or was cancelled, the corresponding exception will be
1583+
## raised.
1584+
##
1585+
## If the future is still pending, `FuturePendingError` will be raised.
1586+
if not fut.finished():
1587+
raiseFuturePendingError(fut)
15201588

1521-
internalCheckComplete(future, E)
1522-
future.internalValue
1589+
internalCheckComplete(fut, E)
15231590

1524-
proc read*[E](future: InternalRaisesFuture[void, E]) = # {.raises: [E, CancelledError].}
1525-
## Retrieves the value of ``future``. Future must be finished otherwise
1526-
## this function will fail with a ``ValueError`` exception.
1591+
proc waitFor*[T: not void; E](fut: InternalRaisesFuture[T, E]): lent T = # {.raises: [E]}
1592+
## Blocks the current thread of execution until `fut` has finished, returning
1593+
## its value.
15271594
##
1528-
## If the result of the future is an error then that error will be raised.
1529-
if future.finished():
1530-
internalCheckComplete(future)
1531-
else:
1532-
# TODO: Make a custom exception type for this?
1533-
raise newException(ValueError, "Future still in progress.")
1595+
## If the future failed or was cancelled, the corresponding exception will be
1596+
## raised.
1597+
##
1598+
## Must not be called recursively (from inside `async` procedures).
1599+
##
1600+
## See also `await`, `Future.read`
1601+
pollFor(fut).readFinished()
1602+
1603+
proc waitFor*[E](fut: InternalRaisesFuture[void, E]) = # {.raises: [E]}
1604+
## Blocks the current thread of execution until `fut` has finished.
1605+
##
1606+
## If the future failed or was cancelled, the corresponding exception will be
1607+
## raised.
1608+
##
1609+
## Must not be called recursively (from inside `async` procedures).
1610+
##
1611+
## See also `await`, `Future.read`
1612+
pollFor(fut).internalCheckComplete(E)
15341613

15351614
proc `or`*[T, Y, E1, E2](
15361615
fut1: InternalRaisesFuture[T, E1],

0 commit comments

Comments
 (0)