Introduce `chronos/futures` (#405)

* move `Future[T]` into its own module along with some basic accessors
* mark all fields internal, exposing only read-only versions under the
old names
* introduce `init`/`completed`/etc as a way of creating a future (vs
newFuture)
* introduce `LocationKind` for `SrcLoc` access
* don't expose `FutureList` unless future tracking is enabled
* introduce `chronosStrictFutureAccess` which controls a number of
additional `Defect` being raised when accessing Future fields in the
wrong state - this will become true in a future version

In this version, `Future[T]` backwards compatibility code remains in
`asyncfutures2` meaning that if only `chronos/futures` is imported, only
"new" API is available.

This branch is a refinement / less invasive / minimal version of
https://github.com/status-im/nim-chronos/pull/373.
This commit is contained in:
Jacek Sieka 2023-06-07 20:04:07 +02:00 committed by GitHub
parent 1d6c309d77
commit 0035f4fa66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 331 additions and 165 deletions

View File

@ -10,8 +10,6 @@
import std/sequtils import std/sequtils
import stew/base10 import stew/base10
import "."/srcloc
export srcloc
when chronosStackTrace: when chronosStackTrace:
when defined(nimHasStacktracesModule): when defined(nimHasStacktracesModule):
@ -21,49 +19,20 @@ when chronosStackTrace:
reraisedFromBegin = -10 reraisedFromBegin = -10
reraisedFromEnd = -100 reraisedFromEnd = -100
type StackTrace = string template LocCreateIndex*: auto {.deprecated: "LocationKind.Create".} =
LocationKind.Create
template LocFinishIndex*: auto {.deprecated: "LocationKind.Finish".} =
LocationKind.Finish
template LocCompleteIndex*: untyped {.deprecated: "LocationKind.Finish".} =
LocationKind.Finish
const func `[]`*(loc: array[LocationKind, ptr SrcLoc], v: int): ptr SrcLoc {.deprecated: "use LocationKind".} =
LocCreateIndex* = 0 case v
LocFinishIndex* = 1 of 0: loc[LocationKind.Create]
of 1: loc[LocationKind.Finish]
template LocCompleteIndex*: untyped {.deprecated: "LocFinishIndex".} = else: raiseAssert("Unknown source location " & $v)
LocFinishIndex
when chronosStrictException:
{.pragma: closureIter, raises: [CatchableError], gcsafe.}
else:
{.pragma: closureIter, raises: [Exception], gcsafe.}
type type
FutureState* {.pure.} = enum
Pending, Completed, Cancelled, Failed
FutureBase* = ref object of RootObj ## Untyped future.
location*: array[2, ptr SrcLoc]
callbacks: seq[AsyncCallback]
cancelcb*: CallbackFunc
child*: FutureBase
state*: FutureState
error*: ref CatchableError ## Stored exception
mustCancel*: bool
closure*: iterator(f: FutureBase): FutureBase {.closureIter.}
when chronosFutureId:
id*: uint
when chronosStackTrace:
errorStackTrace*: StackTrace
stackTrace: StackTrace ## For debugging purposes only.
when chronosFutureTracking:
next*: FutureBase
prev*: FutureBase
Future*[T] = ref object of FutureBase ## Typed future.
when T isnot void:
value*: T ## Stored value
FutureStr*[T] = ref object of Future[T] FutureStr*[T] = ref object of Future[T]
## Future to hold GC strings ## Future to hold GC strings
gcholder*: string gcholder*: string
@ -72,59 +41,24 @@ type
## Future to hold GC seqs ## Future to hold GC seqs
gcholder*: seq[B] gcholder*: seq[B]
FutureDefect* = object of Defect
cause*: FutureBase
FutureError* = object of CatchableError
CancelledError* = object of FutureError
FutureList* = object
head*: FutureBase
tail*: FutureBase
count*: uint
# Backwards compatibility for old FutureState name # Backwards compatibility for old FutureState name
template Finished* {.deprecated: "Use Completed instead".} = Completed template Finished* {.deprecated: "Use Completed instead".} = Completed
template Finished*(T: type FutureState): FutureState {.deprecated: "Use FutureState.Completed instead".} = FutureState.Completed template Finished*(T: type FutureState): FutureState {.deprecated: "Use FutureState.Completed instead".} = FutureState.Completed
when chronosFutureId:
var currentID* {.threadvar.}: uint
else:
template id*(f: FutureBase): uint =
cast[uint](addr f[])
when chronosFutureTracking:
var futureList* {.threadvar.}: FutureList
template setupFutureBase(loc: ptr SrcLoc) =
new(result)
result.state = FutureState.Pending
when chronosStackTrace:
result.stackTrace = getStackTrace()
when chronosFutureId:
currentID.inc()
result.id = currentID
result.location[LocCreateIndex] = loc
when chronosFutureTracking:
result.next = nil
result.prev = futureList.tail
if not(isNil(futureList.tail)):
futureList.tail.next = result
futureList.tail = result
if isNil(futureList.head):
futureList.head = result
futureList.count.inc()
proc newFutureImpl[T](loc: ptr SrcLoc): Future[T] = proc newFutureImpl[T](loc: ptr SrcLoc): Future[T] =
setupFutureBase(loc) let fut = Future[T]()
internalInitFutureBase(fut, loc, FutureState.Pending)
fut
proc newFutureSeqImpl[A, B](loc: ptr SrcLoc): FutureSeq[A, B] = proc newFutureSeqImpl[A, B](loc: ptr SrcLoc): FutureSeq[A, B] =
setupFutureBase(loc) let fut = FutureSeq[A, B]()
internalInitFutureBase(fut, loc, FutureState.Pending)
fut
proc newFutureStrImpl[T](loc: ptr SrcLoc): FutureStr[T] = proc newFutureStrImpl[T](loc: ptr SrcLoc): FutureStr[T] =
setupFutureBase(loc) let fut = FutureStr[T]()
internalInitFutureBase(fut, loc, FutureState.Pending)
fut
template newFuture*[T](fromProc: static[string] = ""): Future[T] = template newFuture*[T](fromProc: static[string] = ""): Future[T] =
## Creates a new future. ## Creates a new future.
@ -149,24 +83,6 @@ template newFutureStr*[T](fromProc: static[string] = ""): FutureStr[T] =
## that this future belongs to, is a good habit as it helps with debugging. ## that this future belongs to, is a good habit as it helps with debugging.
newFutureStrImpl[T](getSrcLocation(fromProc)) newFutureStrImpl[T](getSrcLocation(fromProc))
proc finished*(future: FutureBase): bool {.inline.} =
## Determines whether ``future`` has finished, i.e. ``future`` state changed
## from state ``Pending`` to one of the states (``Finished``, ``Cancelled``,
## ``Failed``).
(future.state != FutureState.Pending)
proc cancelled*(future: FutureBase): bool {.inline.} =
## Determines whether ``future`` has cancelled.
(future.state == FutureState.Cancelled)
proc failed*(future: FutureBase): bool {.inline.} =
## Determines whether ``future`` finished with an error.
(future.state == FutureState.Failed)
proc completed*(future: FutureBase): bool {.inline.} =
## Determines whether ``future`` finished with a value.
(future.state == FutureState.Completed)
proc done*(future: FutureBase): bool {.deprecated: "Use `completed` instead".} = proc done*(future: FutureBase): bool {.deprecated: "Use `completed` instead".} =
## This is an alias for ``completed(future)`` procedure. ## This is an alias for ``completed(future)`` procedure.
completed(future) completed(future)
@ -178,8 +94,8 @@ when chronosFutureTracking:
let future = cast[FutureBase](udata) let future = cast[FutureBase](udata)
if future == futureList.tail: futureList.tail = future.prev if future == futureList.tail: futureList.tail = future.prev
if future == futureList.head: futureList.head = future.next if future == futureList.head: futureList.head = future.next
if not(isNil(future.next)): future.next.prev = future.prev if not(isNil(future.next)): future.next.internalPrev = future.prev
if not(isNil(future.prev)): future.prev.next = future.next if not(isNil(future.prev)): future.prev.internalNext = future.next
futureList.count.dec() futureList.count.dec()
proc scheduleDestructor(future: FutureBase) {.inline.} = proc scheduleDestructor(future: FutureBase) {.inline.} =
@ -194,9 +110,9 @@ proc checkFinished(future: FutureBase, loc: ptr SrcLoc) =
msg.add("Details:") msg.add("Details:")
msg.add("\n Future ID: " & Base10.toString(future.id)) msg.add("\n Future ID: " & Base10.toString(future.id))
msg.add("\n Creation location:") msg.add("\n Creation location:")
msg.add("\n " & $future.location[LocCreateIndex]) msg.add("\n " & $future.location[LocationKind.Create])
msg.add("\n First completion location:") msg.add("\n First completion location:")
msg.add("\n " & $future.location[LocFinishIndex]) msg.add("\n " & $future.location[LocationKind.Finish])
msg.add("\n Second completion location:") msg.add("\n Second completion location:")
msg.add("\n " & $loc) msg.add("\n " & $loc)
when chronosStackTrace: when chronosStackTrace:
@ -209,20 +125,21 @@ proc checkFinished(future: FutureBase, loc: ptr SrcLoc) =
err.cause = future err.cause = future
raise err raise err
else: else:
future.location[LocFinishIndex] = loc future.internalLocation[LocationKind.Finish] = loc
proc finish(fut: FutureBase, state: FutureState) = proc finish(fut: FutureBase, state: FutureState) =
# We do not perform any checks here, because: # We do not perform any checks here, because:
# 1. `finish()` is a private procedure and `state` is under our control. # 1. `finish()` is a private procedure and `state` is under our control.
# 2. `fut.state` is checked by `checkFinished()`. # 2. `fut.state` is checked by `checkFinished()`.
fut.state = state fut.internalState = state
doAssert fut.cancelcb == nil or state != FutureState.Cancelled when chronosStrictFutureAccess:
fut.cancelcb = nil # release cancellation callback memory doAssert fut.internalCancelcb == nil or state != FutureState.Cancelled
for item in fut.callbacks.mitems(): fut.internalCancelcb = nil # release cancellation callback memory
for item in fut.internalCallbacks.mitems():
if not(isNil(item.function)): if not(isNil(item.function)):
callSoon(item) callSoon(item)
item = default(AsyncCallback) # release memory as early as possible item = default(AsyncCallback) # release memory as early as possible
fut.callbacks = default(seq[AsyncCallback]) # release seq as well fut.internalCallbacks = default(seq[AsyncCallback]) # release seq as well
when chronosFutureTracking: when chronosFutureTracking:
scheduleDestructor(fut) scheduleDestructor(fut)
@ -230,8 +147,8 @@ proc finish(fut: FutureBase, state: FutureState) =
proc complete[T](future: Future[T], val: T, loc: ptr SrcLoc) = proc complete[T](future: Future[T], val: T, loc: ptr SrcLoc) =
if not(future.cancelled()): if not(future.cancelled()):
checkFinished(future, loc) checkFinished(future, loc)
doAssert(isNil(future.error)) doAssert(isNil(future.internalError))
future.value = val future.internalValue = val
future.finish(FutureState.Completed) future.finish(FutureState.Completed)
template complete*[T](future: Future[T], val: T) = template complete*[T](future: Future[T], val: T) =
@ -241,7 +158,7 @@ template complete*[T](future: Future[T], val: T) =
proc complete(future: Future[void], loc: ptr SrcLoc) = proc complete(future: Future[void], loc: ptr SrcLoc) =
if not(future.cancelled()): if not(future.cancelled()):
checkFinished(future, loc) checkFinished(future, loc)
doAssert(isNil(future.error)) doAssert(isNil(future.internalError))
future.finish(FutureState.Completed) future.finish(FutureState.Completed)
template complete*(future: Future[void]) = template complete*(future: Future[void]) =
@ -251,9 +168,9 @@ template complete*(future: Future[void]) =
proc fail(future: FutureBase, error: ref CatchableError, loc: ptr SrcLoc) = proc fail(future: FutureBase, error: ref CatchableError, loc: ptr SrcLoc) =
if not(future.cancelled()): if not(future.cancelled()):
checkFinished(future, loc) checkFinished(future, loc)
future.error = error future.internalError = error
when chronosStackTrace: when chronosStackTrace:
future.errorStackTrace = if getStackTrace(error) == "": future.internalErrorStackTrace = if getStackTrace(error) == "":
getStackTrace() getStackTrace()
else: else:
getStackTrace(error) getStackTrace(error)
@ -269,9 +186,9 @@ template newCancelledError(): ref CancelledError =
proc cancelAndSchedule(future: FutureBase, loc: ptr SrcLoc) = proc cancelAndSchedule(future: FutureBase, loc: ptr SrcLoc) =
if not(future.finished()): if not(future.finished()):
checkFinished(future, loc) checkFinished(future, loc)
future.error = newCancelledError() future.internalError = newCancelledError()
when chronosStackTrace: when chronosStackTrace:
future.errorStackTrace = getStackTrace() future.internalErrorStackTrace = getStackTrace()
future.finish(FutureState.Cancelled) future.finish(FutureState.Cancelled)
template cancelAndSchedule*(future: FutureBase) = template cancelAndSchedule*(future: FutureBase) =
@ -295,22 +212,23 @@ proc cancel(future: FutureBase, loc: ptr SrcLoc): bool =
if future.finished(): if future.finished():
return false return false
if not(isNil(future.child)): if not(isNil(future.internalChild)):
# If you hit this assertion, you should have used the `CancelledError` # If you hit this assertion, you should have used the `CancelledError`
# mechanism and/or use a regular `addCallback` # mechanism and/or use a regular `addCallback`
doAssert future.cancelcb.isNil, when chronosStrictFutureAccess:
"futures returned from `{.async.}` functions must not use `cancelCallback`" doAssert future.internalCancelcb.isNil,
"futures returned from `{.async.}` functions must not use `cancelCallback`"
if cancel(future.child, getSrcLocation()): if cancel(future.internalChild, getSrcLocation()):
return true return true
else: else:
if not(isNil(future.cancelcb)): if not(isNil(future.internalCancelcb)):
future.cancelcb(cast[pointer](future)) future.internalCancelcb(cast[pointer](future))
future.cancelcb = nil future.internalCancelcb = nil
cancelAndSchedule(future, getSrcLocation()) cancelAndSchedule(future, getSrcLocation())
future.mustCancel = true future.internalMustCancel = true
return true return true
template cancel*(future: FutureBase) = template cancel*(future: FutureBase) =
@ -318,7 +236,7 @@ template cancel*(future: FutureBase) =
discard cancel(future, getSrcLocation()) discard cancel(future, getSrcLocation())
proc clearCallbacks(future: FutureBase) = proc clearCallbacks(future: FutureBase) =
future.callbacks = default(seq[AsyncCallback]) future.internalCallbacks = default(seq[AsyncCallback])
proc addCallback*(future: FutureBase, cb: CallbackFunc, udata: pointer) = proc addCallback*(future: FutureBase, cb: CallbackFunc, udata: pointer) =
## Adds the callbacks proc to be called when the future completes. ## Adds the callbacks proc to be called when the future completes.
@ -328,7 +246,7 @@ proc addCallback*(future: FutureBase, cb: CallbackFunc, udata: pointer) =
if future.finished(): if future.finished():
callSoon(cb, udata) callSoon(cb, udata)
else: else:
future.callbacks.add AsyncCallback(function: cb, udata: udata) future.internalCallbacks.add AsyncCallback(function: cb, udata: udata)
proc addCallback*(future: FutureBase, cb: CallbackFunc) = proc addCallback*(future: FutureBase, cb: CallbackFunc) =
## Adds the callbacks proc to be called when the future completes. ## Adds the callbacks proc to be called when the future completes.
@ -343,7 +261,7 @@ proc removeCallback*(future: FutureBase, cb: CallbackFunc,
doAssert(not isNil(cb)) doAssert(not isNil(cb))
# Make sure to release memory associated with callback, or reference chains # Make sure to release memory associated with callback, or reference chains
# may be created! # may be created!
future.callbacks.keepItIf: future.internalCallbacks.keepItIf:
it.function != cb or it.udata != udata it.function != cb or it.udata != udata
proc removeCallback*(future: FutureBase, cb: CallbackFunc) = proc removeCallback*(future: FutureBase, cb: CallbackFunc) =
@ -372,9 +290,10 @@ proc `cancelCallback=`*(future: FutureBase, cb: CallbackFunc) =
## This callback will be called immediately as ``future.cancel()`` invoked and ## This callback will be called immediately as ``future.cancel()`` invoked and
## must be set before future is finished. ## must be set before future is finished.
doAssert not future.finished(), when chronosStrictFutureAccess:
"cancellation callback must be set before finishing the future" doAssert not future.finished(),
future.cancelcb = cb "cancellation callback must be set before finishing the future"
future.internalCancelcb = cb
{.push stackTrace: off.} {.push stackTrace: off.}
proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.} proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.}
@ -396,12 +315,12 @@ proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.} =
while true: while true:
# Call closure to make progress on `fut` until it reaches `yield` (inside # Call closure to make progress on `fut` until it reaches `yield` (inside
# `await` typically) or completes / fails / is cancelled # `await` typically) or completes / fails / is cancelled
next = fut.closure(fut) next = fut.internalClosure(fut)
if fut.closure.finished(): # Reached the end of the transformed proc if fut.internalClosure.finished(): # Reached the end of the transformed proc
break break
if next == nil: if next == nil:
raiseAssert "Async procedure (" & ($fut.location[LocCreateIndex]) & raiseAssert "Async procedure (" & ($fut.location[LocationKind.Create]) &
") yielded `nil`, are you await'ing a `nil` Future?" ") yielded `nil`, are you await'ing a `nil` Future?"
if not next.finished(): if not next.finished():
@ -441,8 +360,8 @@ proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.} =
# `futureContinue` will not be called any more for this future so we can # `futureContinue` will not be called any more for this future so we can
# clean it up # clean it up
fut.closure = nil fut.internalClosure = nil
fut.child = nil fut.internalChild = nil
{.pop.} {.pop.}
@ -527,15 +446,15 @@ when chronosStackTrace:
proc internalCheckComplete*(fut: FutureBase) {.raises: [CatchableError].} = proc internalCheckComplete*(fut: FutureBase) {.raises: [CatchableError].} =
# For internal use only. Used in asyncmacro # For internal use only. Used in asyncmacro
if not(isNil(fut.error)): if not(isNil(fut.internalError)):
when chronosStackTrace: when chronosStackTrace:
injectStacktrace(fut.error) injectStacktrace(fut.internalError)
raise fut.error raise fut.internalError
proc internalRead*[T](fut: Future[T]): T {.inline.} = proc internalRead*[T](fut: Future[T]): T {.inline.} =
# For internal use only. Used in asyncmacro # For internal use only. Used in asyncmacro
when T isnot void: when T isnot void:
return fut.value return fut.internalValue
proc read*[T](future: Future[T] ): T {.raises: [CatchableError].} = proc read*[T](future: Future[T] ): T {.raises: [CatchableError].} =
## Retrieves the value of ``future``. Future must be finished otherwise ## Retrieves the value of ``future``. Future must be finished otherwise
@ -561,7 +480,7 @@ proc readError*(future: FutureBase): ref CatchableError {.raises: [ValueError].}
raise newException(ValueError, "No error in future.") raise newException(ValueError, "No error in future.")
template taskFutureLocation(future: FutureBase): string = template taskFutureLocation(future: FutureBase): string =
let loc = future.location[0] let loc = future.location[LocationKind.Create]
"[" & ( "[" & (
if len(loc.procedure) == 0: "[unspecified]" else: $loc.procedure & "()" if len(loc.procedure) == 0: "[unspecified]" else: $loc.procedure & "()"
) & " at " & $loc.file & ":" & $(loc.line) & "]" ) & " at " & $loc.file & ":" & $(loc.line) & "]"

View File

@ -13,10 +13,10 @@
from nativesockets import Port from nativesockets import Port
import std/[tables, strutils, heapqueue, deques] import std/[tables, strutils, heapqueue, deques]
import stew/results import stew/results
import "."/[config, osdefs, oserrno, osutils, timer] import "."/[config, futures, osdefs, oserrno, osutils, timer]
export Port export Port
export timer, results export futures, timer, results
#{.injectStmt: newGcInvariant().} #{.injectStmt: newGcInvariant().}
@ -155,11 +155,7 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or
export oserrno export oserrno
type type
CallbackFunc* = proc (arg: pointer) {.gcsafe, raises: [].} AsyncCallback = InternalAsyncCallback
AsyncCallback* = object
function*: CallbackFunc
udata*: pointer
AsyncError* = object of CatchableError AsyncError* = object of CatchableError
## Generic async exception ## Generic async exception

View File

@ -242,10 +242,10 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
newLit(prcName)) newLit(prcName))
) )
) )
# -> resultFuture.closure = iterator # -> resultFuture.internalClosure = iterator
outerProcBody.add( outerProcBody.add(
newAssignment( newAssignment(
newDotExpr(retFutureSym, newIdentNode("closure")), newDotExpr(retFutureSym, newIdentNode("internalClosure")),
iteratorNameSym) iteratorNameSym)
) )
@ -280,30 +280,30 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
template await*[T](f: Future[T]): untyped = template await*[T](f: Future[T]): untyped =
when declared(chronosInternalRetFuture): when declared(chronosInternalRetFuture):
chronosInternalRetFuture.child = f chronosInternalRetFuture.internalChild = f
# `futureContinue` calls the iterator generated by the `async` # `futureContinue` calls the iterator generated by the `async`
# transformation - `yield` gives control back to `futureContinue` which is # transformation - `yield` gives control back to `futureContinue` which is
# responsible for resuming execution once the yielded future is finished # responsible for resuming execution once the yielded future is finished
yield chronosInternalRetFuture.child yield chronosInternalRetFuture.internalChild
# `child` is guaranteed to have been `finished` after the yield # `child` is guaranteed to have been `finished` after the yield
if chronosInternalRetFuture.mustCancel: if chronosInternalRetFuture.internalMustCancel:
raise newCancelledError() raise newCancelledError()
# `child` released by `futureContinue` # `child` released by `futureContinue`
chronosInternalRetFuture.child.internalCheckComplete() chronosInternalRetFuture.internalChild.internalCheckComplete()
when T isnot void: when T isnot void:
cast[type(f)](chronosInternalRetFuture.child).internalRead() cast[type(f)](chronosInternalRetFuture.internalChild).internalRead()
else: else:
unsupported "await is only available within {.async.}" unsupported "await is only available within {.async.}"
template awaitne*[T](f: Future[T]): Future[T] = template awaitne*[T](f: Future[T]): Future[T] =
when declared(chronosInternalRetFuture): when declared(chronosInternalRetFuture):
chronosInternalRetFuture.child = f chronosInternalRetFuture.internalChild = f
yield chronosInternalRetFuture.child yield chronosInternalRetFuture.internalChild
if chronosInternalRetFuture.mustCancel: if chronosInternalRetFuture.internalMustCancel:
raise newCancelledError() raise newCancelledError()
cast[type(f)](chronosInternalRetFuture.child) cast[type(f)](chronosInternalRetFuture.internalChild)
else: else:
unsupported "awaitne is only available within {.async.}" unsupported "awaitne is only available within {.async.}"

View File

@ -19,6 +19,8 @@ when (NimMajor, NimMinor) >= (1, 4):
## used from within `async` code may need to be be explicitly annotated ## used from within `async` code may need to be be explicitly annotated
## with `raises: [CatchableError]` when this mode is enabled. ## with `raises: [CatchableError]` when this mode is enabled.
chronosStrictFutureAccess* {.booldefine.}: bool = defined(chronosPreviewV4)
chronosStackTrace* {.booldefine.}: bool = defined(chronosDebug) chronosStackTrace* {.booldefine.}: bool = defined(chronosDebug)
## Include stack traces in futures for creation and completion points ## Include stack traces in futures for creation and completion points
@ -52,6 +54,8 @@ else:
const const
chronosStrictException*: bool = chronosStrictException*: bool =
defined(chronosPreviewV4) or defined(chronosStrictException) defined(chronosPreviewV4) or defined(chronosStrictException)
chronosStrictFutureAccess*: bool =
defined(chronosPreviewV4) or defined(chronosStrictFutureAccess)
chronosStackTrace*: bool = defined(chronosDebug) or defined(chronosStackTrace) chronosStackTrace*: bool = defined(chronosDebug) or defined(chronosStackTrace)
chronosFutureId*: bool = defined(chronosDebug) or defined(chronosFutureId) chronosFutureId*: bool = defined(chronosDebug) or defined(chronosFutureId)
chronosFutureTracking*: bool = chronosFutureTracking*: bool =

View File

@ -37,7 +37,7 @@ proc dumpPendingFutures*(filter = AllFutureStates): string =
for item in pendingFutures(): for item in pendingFutures():
if item.state in filter: if item.state in filter:
inc(count) inc(count)
let loc = item.location[LocCreateIndex][] let loc = item.location[LocationKind.Create][]
let procedure = $loc.procedure let procedure = $loc.procedure
let filename = $loc.file let filename = $loc.file
let procname = if len(procedure) == 0: let procname = if len(procedure) == 0:

221
chronos/futures.nim Normal file
View File

@ -0,0 +1,221 @@
#
# Chronos
#
# (c) Copyright 2015 Dominik Picheta
# (c) Copyright 2018-2023 Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
{.push raises: [].}
import "."/[config, srcloc]
export srcloc
when chronosStackTrace:
type StackTrace = string
when chronosStrictException:
{.pragma: closureIter, raises: [CatchableError], gcsafe.}
else:
{.pragma: closureIter, raises: [Exception], gcsafe.}
type
LocationKind* {.pure.} = enum
Create
Finish
CallbackFunc* = proc (arg: pointer) {.gcsafe, raises: [].}
# Internal type, not part of API
InternalAsyncCallback* = object
function*: CallbackFunc
udata*: pointer
FutureState* {.pure.} = enum
Pending, Completed, Cancelled, Failed
InternalFutureBase* = object of RootObj
# Internal untyped future representation - the fields are not part of the
# public API and neither is `InternalFutureBase`, ie the inheritance
# structure may change in the future (haha)
internalLocation*: array[LocationKind, ptr SrcLoc]
internalCallbacks*: seq[InternalAsyncCallback]
internalCancelcb*: CallbackFunc
internalChild*: FutureBase
internalState*: FutureState
internalError*: ref CatchableError ## Stored exception
internalMustCancel*: bool
internalClosure*: iterator(f: FutureBase): FutureBase {.closureIter.}
when chronosFutureId:
internalId*: uint
when chronosStackTrace:
internalErrorStackTrace*: StackTrace
internalStackTrace*: StackTrace ## For debugging purposes only.
when chronosFutureTracking:
internalNext*: FutureBase
internalPrev*: FutureBase
FutureBase* = ref object of InternalFutureBase
## Untyped Future
Future*[T] = ref object of FutureBase ## Typed future.
when T isnot void:
internalValue*: T ## Stored value
FutureDefect* = object of Defect
cause*: FutureBase
FutureError* = object of CatchableError
CancelledError* = object of FutureError
## Exception raised when accessing the value of a cancelled future
when chronosFutureId:
var currentID* {.threadvar.}: uint
template id*(fut: FutureBase): uint = fut.internalId
else:
template id*(fut: FutureBase): uint =
cast[uint](addr fut[])
when chronosFutureTracking:
type
FutureList* = object
head*: FutureBase
tail*: FutureBase
count*: uint
var futureList* {.threadvar.}: FutureList
# Internal utilities - these are not part of the stable API
proc internalInitFutureBase*(
fut: FutureBase,
loc: ptr SrcLoc,
state: FutureState) =
fut.internalState = state
fut.internalLocation[LocationKind.Create] = loc
if state != FutureState.Pending:
fut.internalLocation[LocationKind.Finish] = loc
when chronosFutureId:
currentID.inc()
fut.internalId = currentID
when chronosStackTrace:
fut.internalStackTrace = getStackTrace()
when chronosFutureTracking:
if state == FutureState.Pending:
fut.internalNext = nil
fut.internalPrev = futureList.tail
if not(isNil(futureList.tail)):
futureList.tail.internalNext = fut
futureList.tail = fut
if isNil(futureList.head):
futureList.head = fut
futureList.count.inc()
# Public API
template init*[T](F: type Future[T], fromProc: static[string] = ""): Future[T] =
## Creates a new pending future.
##
## Specifying ``fromProc``, which is a string specifying the name of the proc
## that this future belongs to, is a good habit as it helps with debugging.
let res = Future[T]()
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Pending)
res
template completed*(
F: type Future, fromProc: static[string] = ""): Future[void] =
## Create a new completed future
let res = Future[T]()
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed)
res
template completed*[T: not void](
F: type Future, valueParam: T, fromProc: static[string] = ""): Future[T] =
## Create a new completed future
let res = Future[T](internalValue: valueParam)
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed)
res
template failed*[T](
F: type Future[T], errorParam: ref CatchableError,
fromProc: static[string] = ""): Future[T] =
## Create a new failed future
let res = Future[T](internalError: errorParam)
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Failed)
when chronosStackTrace:
res.internalErrorStackTrace =
if getStackTrace(res.error) == "":
getStackTrace()
else:
getStackTrace(res.error)
res
func state*(future: FutureBase): FutureState =
future.internalState
func finished*(future: FutureBase): bool {.inline.} =
## Determines whether ``future`` has finished, i.e. ``future`` state changed
## from state ``Pending`` to one of the states (``Finished``, ``Cancelled``,
## ``Failed``).
future.state != FutureState.Pending
func cancelled*(future: FutureBase): bool {.inline.} =
## Determines whether ``future`` has cancelled.
future.state == FutureState.Cancelled
func failed*(future: FutureBase): bool {.inline.} =
## Determines whether ``future`` finished with an error.
future.state == FutureState.Failed
func completed*(future: FutureBase): bool {.inline.} =
## Determines whether ``future`` finished with a value.
future.state == FutureState.Completed
func location*(future: FutureBase): array[LocationKind, ptr SrcLoc] =
future.internalLocation
func value*[T](future: Future[T]): T =
## Return the value in a completed future - raises Defect when
## `fut.completed()` is `false`.
##
## See `read` for a version that raises an catchable error when future
## has not completed.
when chronosStrictFutureAccess:
if not future.completed():
raise (ref FutureDefect)(
msg: "Future not completed while accessing value",
cause: future)
when T isnot void:
future.internalValue
func error*(future: FutureBase): ref CatchableError =
## Return the error of `future`, or `nil` if future did not fail.
##
## See `readError` for a version that raises a catchable error when the
## future has not failed.
when chronosStrictFutureAccess:
if not future.failed() and not future.cancelled():
raise (ref FutureDefect)(
msg: "Future not failed/cancelled while accessing error",
cause: future)
future.internalError
when chronosFutureTracking:
func next*(fut: FutureBase): FutureBase = fut.internalNext
func prev*(fut: FutureBase): FutureBase = fut.internalPrev
when chronosStackTrace:
func errorStackTrace*(fut: FutureBase): StackTrace = fut.internalErrorStackTrace
func stackTrace*(fut: FutureBase): StackTrace = fut.internalStackTrace

View File

@ -8,7 +8,7 @@
import testmacro, testsync, testsoon, testtime, testfut, testsignal, import testmacro, testsync, testsoon, testtime, testfut, testsignal,
testaddress, testdatagram, teststream, testserver, testbugs, testnet, testaddress, testdatagram, teststream, testserver, testbugs, testnet,
testasyncstream, testhttpserver, testshttpserver, testhttpclient, testasyncstream, testhttpserver, testshttpserver, testhttpclient,
testproc, testratelimit testproc, testratelimit, testfutures
# Must be imported last to check for Pending futures # Must be imported last to check for Pending futures
import testutils import testutils

26
tests/testfutures.nim Normal file
View File

@ -0,0 +1,26 @@
# Chronos Test Suite
# (c) Copyright 2018-Present
# Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
{.used.}
import unittest2
import ../chronos/futures
suite "Futures":
test "Future constructors":
let
completed = Future.completed(42)
failed = Future[int].failed((ref ValueError)(msg: "msg"))
check:
completed.value == 42
completed.state == FutureState.Completed
check:
failed.error of ValueError
failed.state == FutureState.Failed