Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions async_postgres/async_backend.nim
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,20 @@ elif hasAsyncDispatch:
proc `<`*(a, b: Moment): bool =
a.ticks < b.ticks

proc drainOrphan[T](fut: Future[T]) {.gcsafe.} =
## Default ``onOrphan``: drain the orphan's outcome so a late failure is
## read and cleared, and cannot resurface. Callers that need resource
## cleanup pass their own ``onOrphan``, which replaces this default.
if fut.failed:
discard fut.error
fut.error = nil
fut.errorStackTrace = ""
elif fut.finished:
when T is void:
fut.read()
else:
discard fut.read()

proc wait*[T](
fut: Future[T], timeout: Duration, onOrphan: proc(fut: Future[T]) {.gcsafe.} = nil
): Future[T] {.async.} =
Expand All @@ -170,9 +184,10 @@ elif hasAsyncDispatch:
## future keeps running in the background until its I/O completes. This
## ``onOrphan`` callback is registered on the inner future and called once
## that orphan eventually completes. Use it to close a live connection or
## release other resources the orphan holds. Without it the orphan produces
## an unhandled ``FutureCompleted`` warning at best and a leaked socket /
## server slot at worst.
## release other resources the orphan holds. When ``onOrphan`` is omitted
## the default handler drains the orphan's outcome (clears a late failure)
## but does **not** release other resources. Pass an explicit ``onOrphan``
## when the orphan owns a connection or other live resource.
##
## Under chronos the ``onOrphan`` argument is accepted but never called —
## futures are properly cancelled on timeout and no orphan remains.
Expand All @@ -183,6 +198,8 @@ elif hasAsyncDispatch:
proc() =
if not onOrphan.isNil:
onOrphan(fut)
else:
drainOrphan(fut)
)
raise newException(AsyncTimeoutError, "Timeout")
when T is void:
Expand Down
28 changes: 28 additions & 0 deletions tests/test_async_backend.nim
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,34 @@ suite "wait":
)
check not orphanCalled

test "default onOrphan drains a failing orphan":
# The default drain reads and clears the orphan's late error so it
# cannot resurface. This test fails without the drain.
proc boom(): Future[void] {.async.} =
await sleepMsAsync(20)
raise newException(ValueError, "boom")

let fut = boom()
expect AsyncTimeoutError:
waitFor wait(fut, milliseconds(1))
waitFor sleepMsAsync(50)
check fut.finished
check not fut.failed
check fut.error == nil

test "default onOrphan drains a completing orphan with a value":
# Exercises the non-void branch of the default drain.
proc delayed(): Future[int] {.async.} =
await sleepMsAsync(20)
return 42

let fut = delayed()
expect AsyncTimeoutError:
discard waitFor wait(fut, milliseconds(1))
waitFor sleepMsAsync(50)
check fut.finished
check not fut.failed

when hasChronos:
test "the inner future is cancelled on timeout":
let fut = sleepAsync(milliseconds(30))
Expand Down