mirror of
https://github.com/logos-messaging/nim-sds.git
synced 2026-06-11 19:59:28 +00:00
* feat: make Persistence interface async
The 14 Persistence proc fields now return Future[...] with
{.async: (raises: []), gcsafe.}, allowing real I/O backends (SQLite,
encrypted file, network) to suspend rather than block the Chronos event
loop the manager runs on.
Propagates through:
- ReliabilityManager.lock: system.Lock -> chronos.AsyncLock. Acquired
across awaits cleanly; matches the single-threaded Chronos worker the
FFI uses. Multi-OS-thread use is now explicitly the caller's
responsibility.
- sds_utils + sds.nim public API procs (wrapOutgoingMessage,
unwrapReceivedMessage, markDependenciesMet, setCallbacks,
resetReliabilityManager, cleanup, ensureChannel, removeChannel, the
getter snapshots, etc.) are now async.
- FFI request handlers in library/sds_thread/... await the new API.
- Tests converted via an asyncTest template that wraps each test body
in an async proc; setup/teardown use waitFor for their single async
call (ensureChannel / cleanup).
Lock scope is preserved exactly: the same call sites that held the
kernel Lock today hold AsyncLock now -- no new locking added.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor: drop asyncSpawn, add asyncSetup/asyncTeardown
Three asyncSpawn usages removed:
- sds.nim startPeriodicTasks: stored the periodic-task futures on
ReliabilityManager (new field `periodicTasks: seq[FutureBase]`) so
cleanup can cancel them on shutdown instead of leaking the loops
against a cleared manager.
- library/sds_thread/sds_thread.nim: fireSync moved BEFORE processing,
then `await SdsThreadRequest.process(...)` instead of asyncSpawn'ing
it. Aligns the worker with the SP-channel + lock assumption that
there are no concurrent requests; caller throughput is unchanged
because the caller only waits for receipt (fireSync), not processing.
- tests TestBus repair callback: replaced asyncSpawn(deliverExcept...)
with an explicit pending-delivery queue drained by `bus.drain()`.
Integration tests no longer rely on `sleepAsync(10ms)` to let
spawned deliveries finish — they await drain instead.
Tests also pick up an asyncSetup/asyncTeardown pair (tests/async_unittest.nim)
so suite fixtures can `await` directly. All `waitFor` in setup/teardown
blocks is gone; only the top-level asyncTest wrapper still uses waitFor
(once, to drive the async proc to completion).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Correctly propagate error hidden by new async move
* Correctly handle future cancellation exceptions, +some housekeeping
* Apply suggestion from @Ivansete-status
Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>
* Stylistics, async default implication addressed, nph style run
* Remove leaking CancelledFuture from public facing + as a consequence it is tuneled into handling CatchableError everywhere
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>
71 lines
2.3 KiB
Nim
71 lines
2.3 KiB
Nim
## Shared async-aware wrappers around `unittest` so tests in this repo can
|
|
## `await` directly in setup/test/teardown blocks instead of sprinkling
|
|
## `waitFor` at each call site.
|
|
##
|
|
## Usage:
|
|
##
|
|
## ```nim
|
|
## import ./async_unittest
|
|
##
|
|
## suite "X":
|
|
## var rm: ReliabilityManager
|
|
##
|
|
## asyncSetup:
|
|
## rm = newReliabilityManager(...).get()
|
|
## check (await rm.ensureChannel("ch")).isOk()
|
|
##
|
|
## asyncTeardown:
|
|
## if not rm.isNil:
|
|
## await rm.cleanup()
|
|
##
|
|
## asyncTest "Y":
|
|
## await rm.wrapOutgoingMessage(...)
|
|
## ```
|
|
##
|
|
## All three blocks run inside the same async proc (per test). unittest's
|
|
## own `setup:`/`teardown:` still work for purely synchronous fixtures.
|
|
|
|
import unittest, chronos
|
|
export unittest, chronos
|
|
|
|
template asyncSetup*(body: untyped) {.dirty.} =
|
|
## Async counterpart to unittest's `setup:`. Runs inside each asyncTest's
|
|
## async proc, so `await` works.
|
|
template asyncTestSetupIMPL(): untyped {.dirty.} =
|
|
body
|
|
|
|
template asyncTeardown*(body: untyped) {.dirty.} =
|
|
## Async counterpart to unittest's `teardown:`. Runs in a `finally` so it
|
|
## executes even when the test body (or setup) raises.
|
|
template asyncTestTeardownIMPL(): untyped {.dirty.} =
|
|
body
|
|
|
|
template asyncTest*(name: string, body: untyped) =
|
|
## Wraps a unittest `test` body in an async proc so `await` works on the
|
|
## now-async ReliabilityManager API. unittest's `check` raises Exception,
|
|
## which is wider than chronos's default CatchableError; the exception is
|
|
## caught inside the async body, stashed, and re-raised after waitFor so
|
|
## unittest's normal failure handling sees it.
|
|
##
|
|
## `cast(gcsafe)` is needed because suite-level vars (e.g. `var rm`) look
|
|
## like globals to the async closure, but the FFI runtime is single-thread
|
|
## so the "not gcsafe" warning isn't a real hazard here.
|
|
test name:
|
|
var asyncTestErr {.inject.}: ref Exception = nil
|
|
proc inner() {.async.} =
|
|
{.cast(gcsafe).}:
|
|
try:
|
|
when declared(asyncTestSetupIMPL):
|
|
asyncTestSetupIMPL()
|
|
try:
|
|
body
|
|
finally:
|
|
when declared(asyncTestTeardownIMPL):
|
|
asyncTestTeardownIMPL()
|
|
except Exception as e:
|
|
asyncTestErr = e
|
|
|
|
waitFor inner()
|
|
if asyncTestErr != nil:
|
|
raise asyncTestErr
|