feat(persistence): add PersistenceV2 interface alongside legacy (phase 1)

Introduce the 5-proc snapshot-based Persistence interface that will
replace the legacy 13-proc one. Both coexist on `ReliabilityManager` so
phase 2 can migrate protocol ops one at a time without breaking existing
callers.

New file:
- sds/types/persistence_v2.nim — `PersistenceV2` type with
  saveChannelMeta / updateHistory / loadChannel / dropChannel /
  setRetrievalHint. `noOpPersistenceV2()` default. Doc-comments capture
  the atomicity pairing (meta save + history update issued back-to-back
  under the channel lock) and the non-fatal failure policy from PLAN §8.

Modified:
- sds/types/reliability_manager.nim — adds `persistenceV2: PersistenceV2`
  field alongside `persistence`; constructor takes both, both default to
  no-op.
- sds.nim — `newReliabilityManager` plumbs the new optional parameter.
- AGENTS.md / CLAUDE.md — GitNexus index re-indexed after phase 0 +
  phase 1 additions; symbol counts updated by `npx gitnexus analyze`.

No call site uses the new interface yet — that's phase 2. All existing
tests still pass against the legacy interface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
NagyZoltanPeter 2026-05-29 12:39:10 +02:00
parent 979a66360b
commit f5946763c4
No known key found for this signature in database
GPG Key ID: 3E1F97CF4A7B6F42
5 changed files with 115 additions and 8 deletions

View File

@ -1,7 +1,7 @@
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **nim-sds** (889 symbols, 1437 relationships, 45 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **nim-sds** (1079 symbols, 1770 relationships, 61 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
@ -40,4 +40,4 @@ This project is indexed by GitNexus as **nim-sds** (889 symbols, 1437 relationsh
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
<!-- gitnexus:end -->
<!-- gitnexus:end -->

View File

@ -166,7 +166,7 @@ If using Nix, also recalculate the fixed-output hash in `nix/deps.nix` after upd
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **nim-sds** (889 symbols, 1437 relationships, 45 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **nim-sds** (1079 symbols, 1770 relationships, 61 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

10
sds.nim
View File

@ -8,13 +8,17 @@ proc newReliabilityManager*(
participantId: SdsParticipantID,
config: ReliabilityConfig = defaultConfig(),
persistence: Persistence = noOpPersistence(),
persistenceV2: PersistenceV2 = noOpPersistenceV2(),
): Result[ReliabilityManager, ReliabilityError] =
## Creates a new multi-channel ReliabilityManager.
## `participantId` is REQUIRED (see `ReliabilityManager.new`).
## `persistence` defaults to a no-op backend; supply a real one to durably
## store SDS state across restarts.
## `persistence` is the legacy fine-grained backend; defaults to a no-op.
## `persistenceV2` is the snapshot-based backend (target interface; see
## PLAN_SNAPSHOT_PERSISTENCE.md). Also defaults to a no-op. During the
## phased refactor both backends coexist; phase 3 deletes the legacy
## interface and renames `persistenceV2` to `persistence`.
try:
let rm = ReliabilityManager.new(participantId, config, persistence)
let rm = ReliabilityManager.new(participantId, config, persistence, persistenceV2)
return ok(rm)
except Exception:
error "Failed to create ReliabilityManager", msg = getCurrentExceptionMsg()

View File

@ -0,0 +1,94 @@
## Snapshot-based persistence interface (5 procs).
##
## This is the target interface for the refactor described in
## PLAN_SNAPSHOT_PERSISTENCE.md. It coexists with the legacy 13-proc
## `Persistence` (in `./persistence.nim`) during phase 2 of the refactor:
## protocol ops are migrated one at a time. Phase 3 deletes the old
## interface and renames `PersistenceV2` to `Persistence`.
##
## Why 5 procs instead of 13: every protocol op now issues at most ONE
## meta save + ONE history update at the end of the op, eliminating
## per-mutation persistence calls and the partial-write divergence they
## made unavoidable. See PLAN_SNAPSHOT_PERSISTENCE.md §2 and §8.
import chronos, results
import ./sds_message_id
import ./channel_meta
import ./history_update
export results, sds_message_id, channel_meta, history_update
type PersistenceV2* = object
## Snapshot-based persistence contract. Supplied at
## `newReliabilityManager` construction time. Each proc field is invoked
## by nim-sds AT MOST ONCE per protocol op, at the end of the op, under
## the channel lock.
##
## 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 §8.
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.
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.
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.
proc noOpPersistenceV2*(): PersistenceV2 =
## 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).
PersistenceV2(
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(),
)

View File

@ -6,16 +6,23 @@ import ./callbacks
import ./reliability_config
import ./channel_context
import ./persistence
import ./persistence_v2
export
sds_message_id, history_entry, callbacks, reliability_config, channel_context,
persistence
persistence, persistence_v2
type ReliabilityManager* = ref object
channels*: Table[SdsChannelID, ChannelContext]
config*: ReliabilityConfig
participantId*: SdsParticipantID
persistence*: Persistence
## Pluggable durability backend; defaults to a no-op when not supplied.
## Legacy fine-grained persistence interface. Phase 1 of the refactor
## (see PLAN_SNAPSHOT_PERSISTENCE.md) keeps this alongside `persistenceV2`
## so protocol ops can be migrated one at a time.
persistenceV2*: PersistenceV2
## Snapshot-based persistence interface. Defaults to a no-op when not
## supplied. During phase 2 of the refactor, individual protocol ops
## are migrated from `persistence.X` to `persistenceV2.X`.
lock*: AsyncLock
## Single-threaded Chronos cooperative lock. Serializes mutators against
## one another at await points; the manager assumes all calls come from
@ -38,6 +45,7 @@ proc new*(
participantId: SdsParticipantID,
config: ReliabilityConfig,
persistence: Persistence = noOpPersistence(),
persistenceV2: PersistenceV2 = noOpPersistenceV2(),
): T =
## `participantId` is REQUIRED — it is the per-manager identity SDS-R uses
## to populate response groups and decide which incoming repair requests
@ -50,6 +58,7 @@ proc new*(
config: config,
participantId: participantId,
persistence: persistence,
persistenceV2: persistenceV2,
lock: newAsyncLock(),
periodicTasks: @[],
)