mirror of
https://github.com/logos-messaging/nim-sds.git
synced 2026-07-30 19:43:16 +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>
164 lines
6.4 KiB
Nim
164 lines
6.4 KiB
Nim
import chronos
|
|
import ./sds_message_id
|
|
import ./sds_message
|
|
import ./unacknowledged_message
|
|
import ./incoming_message
|
|
import ./repair_entry
|
|
export
|
|
sds_message_id, sds_message, unacknowledged_message, incoming_message, repair_entry
|
|
|
|
## SDS state persistence interface (issue #64).
|
|
##
|
|
## Defines WHAT operations a persistence backend must provide. The actual
|
|
## storage technology (SQLite, encrypted file, in-memory) is supplied by the
|
|
## caller — nim-sds knows nothing about it. Every state-mutating proc in the
|
|
## protocol calls into one of these procs immediately after the in-memory
|
|
## change, so on-disk state stays in lockstep with in-memory state.
|
|
##
|
|
## All proc fields are async (return `Future`) so backends can do real I/O
|
|
## without blocking the Chronos event loop the manager runs on.
|
|
##
|
|
## Bloom filter is intentionally not persisted: it is rebuilt from the local
|
|
## history log on bootstrap. Async timers are likewise recomputed from the
|
|
## absolute timestamps stored in the repair buffer entries.
|
|
|
|
type
|
|
ChannelSnapshot* = object
|
|
## Returned by `loadAllForChannel` on bootstrap. Carries the entire
|
|
## per-channel state needed to repopulate a `ChannelContext`. The bloom
|
|
## filter is NOT in the snapshot — callers rebuild it from `messageHistory`.
|
|
lamportTimestamp*: int64
|
|
messageHistory*: seq[SdsMessage]
|
|
## MUST be ordered oldest-first. FIFO eviction relies on insertion order;
|
|
## skipping ORDER BY corrupts the log across restarts.
|
|
outgoingBuffer*: seq[UnacknowledgedMessage]
|
|
incomingBuffer*: seq[IncomingMessage]
|
|
outgoingRepairBuffer*: seq[(SdsMessageID, OutgoingRepairEntry)]
|
|
incomingRepairBuffer*: seq[(SdsMessageID, IncomingRepairEntry)]
|
|
|
|
Persistence* = object
|
|
## Pluggable persistence contract. The caller supplies an instance of this
|
|
## type at `newReliabilityManager` construction time. Each proc field is
|
|
## invoked by nim-sds at the corresponding state-mutation point.
|
|
## All fields are async; nim-sds awaits each call to keep on-disk and
|
|
## in-memory state in lockstep without blocking the event loop.
|
|
|
|
# Per-channel lamport clock
|
|
saveLamport*: proc(channelId: SdsChannelID, lamport: int64): Future[void] {.
|
|
async: (raises: []), gcsafe
|
|
.}
|
|
|
|
# Local log (delivered messages)
|
|
appendLogEntry*: proc(channelId: SdsChannelID, msg: SdsMessage): Future[void] {.
|
|
async: (raises: []), gcsafe
|
|
.}
|
|
removeLogEntry*: proc(channelId: SdsChannelID, msgId: SdsMessageID): Future[void] {.
|
|
async: (raises: []), gcsafe
|
|
.}
|
|
setRetrievalHint*: proc(msgId: SdsMessageID, hint: seq[byte]): Future[void] {.
|
|
async: (raises: []), gcsafe
|
|
.}
|
|
|
|
# Outgoing unacknowledged buffer
|
|
saveOutgoing*: proc(
|
|
channelId: SdsChannelID, msg: UnacknowledgedMessage
|
|
): Future[void] {.async: (raises: []), gcsafe.}
|
|
removeOutgoing*: proc(channelId: SdsChannelID, msgId: SdsMessageID): Future[void] {.
|
|
async: (raises: []), gcsafe
|
|
.}
|
|
|
|
# Incoming dependency-waiting buffer
|
|
saveIncoming*: proc(channelId: SdsChannelID, msg: IncomingMessage): Future[void] {.
|
|
async: (raises: []), gcsafe
|
|
.}
|
|
removeIncoming*: proc(channelId: SdsChannelID, msgId: SdsMessageID): Future[void] {.
|
|
async: (raises: []), gcsafe
|
|
.}
|
|
|
|
# SDS-R outgoing repair buffer
|
|
saveOutgoingRepair*: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID, entry: OutgoingRepairEntry
|
|
) {.async: (raises: []).}
|
|
removeOutgoingRepair*: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID
|
|
): Future[void] {.async: (raises: []), gcsafe.}
|
|
|
|
# SDS-R incoming repair buffer
|
|
saveIncomingRepair*: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID, entry: IncomingRepairEntry
|
|
) {.async: (raises: []).}
|
|
removeIncomingRepair*: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID
|
|
): Future[void] {.async: (raises: []), gcsafe.}
|
|
|
|
# Wipe all persisted state for a channel in one transactional call.
|
|
# Called by removeChannel / resetReliabilityManager. Backends should
|
|
# implement this atomically (e.g. one BEGIN/COMMIT) — a per-row loop on
|
|
# the nim-sds side would mean N fsyncs per drop.
|
|
dropChannel*:
|
|
proc(channelId: SdsChannelID): Future[void] {.async: (raises: []), gcsafe.}
|
|
|
|
# Bootstrap on `addChannel` / `getOrCreateChannel`.
|
|
loadAllForChannel*: proc(channelId: SdsChannelID): Future[ChannelSnapshot] {.
|
|
async: (raises: []), gcsafe
|
|
.}
|
|
|
|
proc noOpPersistence*(): Persistence =
|
|
## Default backend that discards every write and returns an empty snapshot.
|
|
## Used so existing callers (and tests) that don't care about durability
|
|
## keep working without supplying a real backend.
|
|
Persistence(
|
|
saveLamport: proc(channelId: SdsChannelID, lamport: int64) {.async: (raises: []).} =
|
|
discard,
|
|
appendLogEntry: proc(
|
|
channelId: SdsChannelID, msg: SdsMessage
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
removeLogEntry: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
setRetrievalHint: proc(
|
|
msgId: SdsMessageID, hint: seq[byte]
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
saveOutgoing: proc(
|
|
channelId: SdsChannelID, msg: UnacknowledgedMessage
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
removeOutgoing: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
saveIncoming: proc(
|
|
channelId: SdsChannelID, msg: IncomingMessage
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
removeIncoming: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
saveOutgoingRepair: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID, entry: OutgoingRepairEntry
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
removeOutgoingRepair: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
saveIncomingRepair: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID, entry: IncomingRepairEntry
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
removeIncomingRepair: proc(
|
|
channelId: SdsChannelID, msgId: SdsMessageID
|
|
) {.async: (raises: []).} =
|
|
discard,
|
|
dropChannel: proc(channelId: SdsChannelID) {.async: (raises: []).} =
|
|
discard,
|
|
loadAllForChannel: proc(
|
|
channelId: SdsChannelID
|
|
): Future[ChannelSnapshot] {.async: (raises: []).} =
|
|
return ChannelSnapshot(),
|
|
)
|