nim-sds/sds/types/persistence.nim
NagyZoltanPeter 23a0ea7a6f
refactor(persistence): delete legacy interface; rename PersistenceV2 -> Persistence (phase 3)
End-state of the snapshot-persistence refactor. The legacy 13-proc
Persistence interface and its noOpPersistence are gone; the 5-proc
snapshot-based interface (formerly PersistenceV2) takes their place under
the canonical name.

Source:
- sds/types/persistence.nim: replaced 13-proc contract with the 5-proc
  snapshot interface (saveChannelMeta, updateHistory, loadChannel,
  dropChannel, setRetrievalHint). noOpPersistence returns ok everywhere
  and an empty ChannelData on load.
- sds/types/persistence_v2.nim: removed.
- sds/types/reliability_manager.nim: dropped the second persistenceV2
  field; constructor takes a single `persistence: Persistence`.
- sds/sds_utils.nim: rm.persistenceV2.X -> rm.persistence.X; doc-comments
  updated.
- sds.nim: dropped the persistenceV2 parameter from newReliabilityManager.

Tests:
- tests/in_memory_persistence_v2.nim: removed; its content moved to...
- tests/in_memory_persistence.nim: replaces the old legacy mock with the
  snapshot adapter under the canonical filename. Same InMemoryStore
  shape so test assertions stay unchanged.
- tests/test_persistence.nim: ctor param renamed, suite name de-prefixed.

FFI smoke (`nimble libsdsDynamicMac`, refc/threads:on): builds clean.
All 4 test suites pass:
- test_bloom
- test_reliability
- test_persistence (17 V2 tests)
- test_snapshot_codec (13 codec round-trip tests)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 13:19:16 +02:00

95 lines
4.0 KiB
Nim

## Snapshot-based persistence interface (5 procs).
##
## Each protocol op issues AT MOST one `saveChannelMeta` and one
## `updateHistory` call at the end of the op, under the channel lock. The
## meta blob is the complete current per-channel state (lamport clock,
## outgoing/incoming buffers, SDS-R repair buffers); the history update
## carries (append, evict) for the message log. Bloom filter is rebuilt
## from history on bootstrap, never persisted.
##
## Atomicity expectation: nim-sds issues `saveChannelMeta` and (when
## non-empty) `updateHistory` back-to-back with NO intervening
## `await`-of-other-work. The backend MAY treat the pair as one
## transaction. The pair is keyed on the same `channelId`.
##
## Failure policy: a failed `saveChannelMeta` or `updateHistory` MUST NOT
## abort the protocol op. The next op's save is fully self-contained and
## will re-synchronise on-disk state. See PLAN_SNAPSHOT_PERSISTENCE.md §8.
## `loadChannel` and `dropChannel` DO surface errors — they're the
## durability-intent ops.
import chronos, results
import ./sds_message_id
import ./channel_meta
import ./history_update
export results, sds_message_id, channel_meta, history_update
type Persistence* = object
## Pluggable durability backend. Supplied at `newReliabilityManager`
## construction time; defaults to `noOpPersistence()` when not given.
saveChannelMeta*: proc(
channelId: SdsChannelID, meta: ChannelMeta
): Future[Result[void, string]] {.async: (raises: []), gcsafe.}
## Persist the complete current per-channel snapshot. Idempotent: the
## blob is the full state, so a missed write is recovered by any later
## successful write.
updateHistory*: proc(
channelId: SdsChannelID, update: HistoryUpdate
): Future[Result[void, string]] {.async: (raises: []), gcsafe.}
## Append newly-delivered messages and evict oldest ones past the
## maxMessageHistory cap. Callers SHOULD skip this call entirely when
## `update.isEmpty`.
loadChannel*: proc(
channelId: SdsChannelID
): Future[Result[ChannelData, string]] {.async: (raises: []), gcsafe.}
## Bootstrap on `getOrCreateChannel`. Returns the full prior state, or
## an empty `ChannelData` if the channel is new on disk. Failure
## propagates to the caller — bootstrap is a durability-intent op.
dropChannel*: proc(
channelId: SdsChannelID
): Future[Result[void, string]] {.async: (raises: []), gcsafe.}
## Wipe all persisted state for a channel. Called by `removeChannel` /
## `resetReliabilityManager`. Backends SHOULD execute atomically.
## Failure propagates to the caller — the caller asked us to confirm a
## disk wipe and we cannot silently lie.
setRetrievalHint*: proc(
msgId: SdsMessageID, hint: seq[byte]
): Future[Result[void, string]] {.async: (raises: []), gcsafe.}
## Record a retrieval hint for a message id. Called from
## `getRecentHistoryEntries` when an application-supplied hint
## provider returns a non-empty hint. Out-of-band from the
## snapshot/history write path because hints are populated lazily
## during read. Non-fatal on failure.
proc noOpPersistence*(): Persistence =
## Default backend: discards all writes, returns an empty snapshot on
## load. Used when no real backend is supplied (existing tests and
## non-durability-needing callers).
Persistence(
saveChannelMeta: proc(
channelId: SdsChannelID, meta: ChannelMeta
): Future[Result[void, string]] {.async: (raises: []).} =
ok(),
updateHistory: proc(
channelId: SdsChannelID, update: HistoryUpdate
): Future[Result[void, string]] {.async: (raises: []).} =
ok(),
loadChannel: proc(
channelId: SdsChannelID
): Future[Result[ChannelData, string]] {.async: (raises: []).} =
ok(ChannelData.init()),
dropChannel: proc(
channelId: SdsChannelID
): Future[Result[void, string]] {.async: (raises: []).} =
ok(),
setRetrievalHint: proc(
msgId: SdsMessageID, hint: seq[byte]
): Future[Result[void, string]] {.async: (raises: []).} =
ok(),
)