From f5946763c484d477ce9e4757b8cb61a3a00e1fef Mon Sep 17 00:00:00 2001 From: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Date: Fri, 29 May 2026 12:39:10 +0200 Subject: [PATCH] feat(persistence): add PersistenceV2 interface alongside legacy (phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- AGENTS.md | 4 +- CLAUDE.md | 2 +- sds.nim | 10 +++- sds/types/persistence_v2.nim | 94 +++++++++++++++++++++++++++++++ sds/types/reliability_manager.nim | 13 ++++- 5 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 sds/types/persistence_v2.nim diff --git a/AGENTS.md b/AGENTS.md index 84d8372..80c1843 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # 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` | - \ No newline at end of file + diff --git a/CLAUDE.md b/CLAUDE.md index 1df91d7..3e35e35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -166,7 +166,7 @@ If using Nix, also recalculate the fixed-output hash in `nix/deps.nix` after upd # 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. diff --git a/sds.nim b/sds.nim index d768808..2db548a 100644 --- a/sds.nim +++ b/sds.nim @@ -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() diff --git a/sds/types/persistence_v2.nim b/sds/types/persistence_v2.nim new file mode 100644 index 0000000..aed4635 --- /dev/null +++ b/sds/types/persistence_v2.nim @@ -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(), + ) diff --git a/sds/types/reliability_manager.nim b/sds/types/reliability_manager.nim index 6b4cc7e..8bbfbfe 100644 --- a/sds/types/reliability_manager.nim +++ b/sds/types/reliability_manager.nim @@ -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: @[], )