mirror of
https://github.com/logos-messaging/nim-sds.git
synced 2026-06-11 19:59:28 +00:00
* feat: propagate persistence backend errors via Result
The Persistence contract previously returned `Future[void]` for writes and
`Future[ChannelSnapshot]` for the loader, with `raises: []`. Backends had no
way to report a failure, so a failed write or a failed/partial read was
silently swallowed — and on the read path a mid-scan failure could bootstrap
a *truncated* channel snapshot, corrupting the rebuilt bloom filter and
lamport clock across a restart.
Make every contract field Result-returning:
* mutating ops -> Future[Result[void, string]]
* loadAllForChannel -> Future[Result[ChannelSnapshot, string]]
The backend-supplied error string is mapped to a new
`ReliabilityError.rePersistenceError` (logged once at the boundary via
`reliabilityErr`) and threaded up through every persistence-touching proc to
the public API, where the caller decides what to do. Request-driven paths
(wrap/unwrap/markDependenciesMet/ensureChannel/removeChannel/reset) propagate
the error; background maintenance loops (periodicBufferSweep,
periodicRepairSweep) log and retry on the next tick, since they have no
synchronous caller.
Tests: in-memory backend gains a `failingOps` injection hook; new
"Persistence: error propagation" suite asserts read/write/drop failures
surface as `rePersistenceError`. Full suite passes (90 OK).
BREAKING CHANGE: the `Persistence` contract signature changed; custom
backends must return `Result` and `ok()` on success. Bumped to 0.3.0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(persistence): add snapshot types and codec (phase 0)
Introduce atomic-snapshot persistence types that will replace the current
fine-grained 13-proc Persistence interface. This commit is purely additive:
no existing call site changes, no behaviour change.
New types (sds/types/):
- channel_meta.nim — ChannelMeta (atomic per-channel snapshot blob),
ChannelData (bootstrap payload), OutgoingRepairKV / IncomingRepairKV
(flattened map entries for protobuf wire shape).
- history_update.nim — HistoryUpdate (combined append/evict payload for
the message log).
New codec (sds/snapshot_codec.nim):
- Protobuf encode/decode for all new types, reusing the existing
SdsMessage and HistoryEntry encoders from sds/protobuf.nim.
- Explicit schemaVersion=1 on ChannelMeta; decoder rejects unknown
versions loudly rather than silently truncating.
- Time encoded as int64 unix milliseconds.
Tests (tests/test_snapshot_codec.nim):
- 13 round-trip cases covering empty, single-entry, full-buffer, and
repair-heavy snapshots; ChannelData ordering; HistoryUpdate variants;
schemaVersion rejection.
Planning artefacts:
- ANALYSIS_SDS_PERSISTENCE.md — problem statement (partial-write
divergence, chatty call rate, non-fatal-error policy gap).
- ANALYSIS_SNAPSHOT_SAVE_POINTS.md — exact save points per protocol op
and projected call rates.
- PLAN_SNAPSHOT_PERSISTENCE.md — phased refactor plan; this commit
implements phase 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 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>
* refactor(persistence): migrate runRepairSweep to PersistenceV2 (phase 2.1)
Per-entry removeIncomingRepair / removeOutgoingRepair calls are replaced
by a single trySaveMeta per *dirty* channel at the end of that channel's
sweep. Failure is logged but does NOT abort the sweep — in-memory state
is the source of truth (PLAN_SNAPSHOT_PERSISTENCE.md §8).
Helpers added in sds/sds_utils.nim:
- snapshotMeta(channel) — capture current ChannelContext as ChannelMeta
blob (flattens Table-keyed buffers to seqs for the wire shape).
- trySaveMeta(rm, channelId, channel) — best-effort meta snapshot save;
logs on failure, never propagates.
- tryUpdateHistory(rm, channelId, append, evict) — best-effort history
update; skips the call entirely when both lists are empty (HistoryUpdate
contract).
Call-rate impact for runRepairSweep:
- Before: N persistence calls per expired entry per channel.
- After: at most 1 saveChannelMeta per dirty channel; 0 on idle channels
(matches the dirty-flag floor in ANALYSIS_SNAPSHOT_SAVE_POINTS).
All existing tests pass — including the 3 SDS-R Repair Sweep tests that
directly exercise this proc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(persistence): migrate checkUnacknowledgedMessages to PersistenceV2 (phase 2.2)
Per-entry saveOutgoing / removeOutgoing calls are replaced by one
trySaveMeta at the end of the pass, conditional on a dirty flag (resend
attempt incremented, or entry expired). Pass succeeds even if the save
fails — next tick reissues the snapshot.
Call-rate impact:
- Before: N persistence calls per affected entry per pass.
- After: at most 1 saveChannelMeta per pass; 0 when nothing aged out.
All existing tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(persistence): add V2 meta snapshot saves to foreground ops (phase 2A)
Wires `trySaveMeta` into the three public protocol ops that mutate
per-channel state — wrapOutgoingMessage, unwrapReceivedMessage, and
markDependenciesMet — at the operation's end, under the channel lock.
Legacy fine-grained persistence calls REMAIN in place; this commit is
additive. Both interfaces persist the same state simultaneously, so all
existing tests pass and a real backend wired to either interface
continues to work. Phase 2B will strip the legacy calls.
Save points match the §"Save Points" table in
ANALYSIS_SNAPSHOT_SAVE_POINTS.md exactly:
- wrapOutgoingMessage: 1 save (always)
- unwrapReceivedMessage: 1 save on every path including duplicate
(the duplicate path still mutates the repair buffers)
- markDependenciesMet: 1 save after the processIncomingBuffer cascade
Non-fatal failure policy (PLAN §8): trySaveMeta logs and continues;
the protocol op never returns rePersistenceError for snapshot failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(persistence): strip legacy interface from protocol path; migrate tests to V2 (phase 2B+2C+2D)
End-state of phase 2: the protocol code no longer issues any legacy
fine-grained Persistence calls. All state survives via the snapshot-based
PersistenceV2 interface — one trySaveMeta per op end, plus tryUpdateHistory
batched inside addToHistory. The legacy Persistence field on
ReliabilityManager remains for backwards compatibility; phase 3 deletes it.
Protocol changes (sds.nim, sds/sds_utils.nim):
- reviewAckStatus, processIncomingBuffer, updateLamportTimestamp →
pure in-memory; no per-mutation persistence.
- addToHistory: replaces appendLogEntry+removeLogEntry with a single
tryUpdateHistory call carrying (append, evict) atomically.
- getRecentHistoryEntries: setRetrievalHint switched to V2; non-fatal.
- wrapOutgoingMessage, unwrapReceivedMessage, markDependenciesMet:
all per-row saveOutgoing / removeOutgoing / saveIncoming /
removeIncoming / saveOutgoingRepair / removeOutgoingRepair /
saveIncomingRepair / removeIncomingRepair calls removed (16 call
sites in total). State is captured by the op-end trySaveMeta added
in phase 2A.
- getOrCreateChannel: bootstraps from persistenceV2.loadChannel.
- dropChannelFromPersistence: uses persistenceV2.dropChannel.
Failure policy (PLAN_SNAPSHOT_PERSISTENCE.md §8):
- Foreground ops (wrap, unwrap, markDeps, sweeps): non-fatal —
trySaveMeta / tryUpdateHistory log and continue; the protocol op
returns ok regardless of disk failure. In-memory state is the source
of truth; the next op re-issues a complete snapshot and disk catches
up automatically.
- Durability-intent ops (removeChannel, resetReliabilityManager via
dropChannelFromPersistence; getOrCreateChannel via loadChannel):
still propagate rePersistenceError, because the caller asked us to
confirm a disk operation and we cannot silently lie.
Test infrastructure:
- tests/in_memory_persistence_v2.nim: new V2 adapter mock that
decomposes the meta blob into the existing InMemoryStore shape so
test assertions on store.outgoing / store.incoming / etc. continue to
work without change.
- tests/test_persistence.nim: 17 tests, all rewritten against V2.
- 13 state-survival tests carry over with identical assertions.
- "loadChannel failure surfaces as err on bootstrap" — bootstrap
keeps durability-intent semantics.
- "saveChannelMeta failure during send does NOT surface" — deliberate
inversion of the legacy "write failure surfaces as err" test. Asserts
the new non-fatal policy: op returns ok, in-memory state correct,
disk re-syncs on the next op.
- "updateHistory failure during send does NOT surface" — same policy
applied to the history path.
- "dropChannel failure during removeChannel surfaces as err" — kept.
- All 17 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 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>
* Persisting persistence redesign plan for reference
* refactor(persistence): R2 pending-write queue + per-op accumulator (PR #72 review fix)
Addresses all three substantive review findings on PR #72 in one
structural change: fold the per-op accumulator and the R2 retry buffer
into a single queue on `ChannelContext`, flushed once at op end.
Changes:
- sds/types/channel_context.nim: add `pendingHistoryAppends`
(`OrderedSet[SdsMessageID]`) and `pendingHistoryEvicts`
(`HashSet[SdsMessageID]`) fields. Only ids are stored — the full
SdsMessage is looked up from `messageHistory` at flush time. Documented
invariant: every id in pendingHistoryAppends is also in messageHistory,
upheld by the merge rule.
- sds/sds_utils.nim:
* `queueHistoryAppend(channel, msgId)` / `queueHistoryEvict(channel,
msgId)` — "latest-wins" merge: append cancels any pending evict
and vice versa. Symmetric, simple, handles the evict-then-re-add
sequence correctly (SDS-R repair re-delivering an evicted message
while the backend is unreachable).
* `tryUpdateHistory(rm, channelId)` — no more list params; flushes the
channel's pending queue. Dual role: per-op accumulator (multiple
`addToHistory` calls within one op queue together and flush as one
round-trip) AND R2 retry buffer (a failed flush leaves the queue
populated for the next op to retry).
* `addToHistory` queues via the helpers; does not call persistence.
* Pending queue cleared on `cleanup` and `removeChannel`.
- sds.nim:
* `processIncomingBuffer` returns to its single-arg signature — the
queue lives on the channel, no parameter threading needed.
* `wrapOutgoingMessage`, `unwrapReceivedMessage` (all three paths),
`markDependenciesMet` issue exactly one `trySaveMeta` +
`tryUpdateHistory` pair at op end, under the lock, with no
intervening `await`-of-other-work. Matches the Persistence atomicity
contract documented in `sds/types/persistence.nim`.
* Pending queue cleared in `resetReliabilityManager`.
- tests/test_persistence.nim:
* Direct `addToHistory` callers (state-survival setup) now follow with
explicit `tryUpdateHistory(channelId)` to flush. Reflects the
production op-end flush pattern.
* New: `updateHistory failure is retried via R2 pending-write queue` —
verifies that two failed sends leave both messages on the queue,
and a third successful send drains the whole queue in one call.
* New: `pending queue survives idle ops` — verifies that an op with
no history changes of its own still flushes a previously-failed
batch at op end.
* New: `evict-then-re-add merge rule preserves the re-added message
on disk` — regression for the "latest-wins" merge rule. The original
"evict-wins" rule would silently drop the re-add and leave the
message permanently absent from disk; this test would fail under
that rule and passes under the corrected one.
Resolves PR #72 review comments:
- #1 (delta loss on failed updateHistory) — R2 retry queue.
- #2 (cascade chattiness — N updateHistory calls per op) — queue collects
cascaded entries, flushed as one batch.
- #3 (atomicity contract mismatch) — implementation now matches the
documented "saveChannelMeta then updateHistory back-to-back" pairing.
Test summary: 50 tests pass (47 prior + 3 new R2/merge-rule tests).
FFI dylib (`nimble libsdsDynamicMac`, refc + threads:on): clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
327 lines
12 KiB
Nim
327 lines
12 KiB
Nim
## Storage encoding for the snapshot persistence types.
|
|
##
|
|
## This is the codec nim-sds runs on its side of the persistence boundary
|
|
## to turn a `ChannelMeta` (or `ChannelData`, or `HistoryUpdate`) into the
|
|
## opaque `seq[byte]` blob the KV persistence backend stores. The KV
|
|
## backend treats the blob as fully opaque. See PLAN_SNAPSHOT_PERSISTENCE.md
|
|
## §1.5 for why this codec exists at all and §6 for the choice of protobuf.
|
|
##
|
|
## This is NOT the SDS network wire format — that lives in `sds/protobuf.nim`
|
|
## and is unchanged. Encoders for `SdsMessage` and `HistoryEntry` are reused
|
|
## from there to avoid maintaining two codecs for the same shape.
|
|
|
|
{.push raises: [].}
|
|
|
|
import std/[sets, times]
|
|
import libp2p/protobuf/minprotobuf
|
|
import ./types/[
|
|
channel_meta, history_update, sds_message, sds_message_id, history_entry,
|
|
unacknowledged_message, incoming_message, repair_entry, reliability_error,
|
|
]
|
|
import ./protobufutil
|
|
import ./protobuf as wire
|
|
|
|
export channel_meta, history_update
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Time <-> int64 unix milliseconds
|
|
# ---------------------------------------------------------------------------
|
|
# The protocol uses `getTime()` (wall clock). For wire stability we encode
|
|
# as unix milliseconds in int64 (zigzag not needed — pre-1970 values do not
|
|
# occur in practice). Sub-millisecond precision is intentionally dropped:
|
|
# the protocol's repair backoff windows are seconds-scale.
|
|
|
|
proc toUnixMs(t: Time): int64 =
|
|
t.toUnix * 1000'i64 + int64(t.nanosecond div 1_000_000)
|
|
|
|
proc fromUnixMs(ms: int64): Time =
|
|
let secs = ms div 1000
|
|
let nanos = (ms mod 1000).int * 1_000_000
|
|
initTime(secs, nanos)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# UnacknowledgedMessage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc encodeUnacked(u: UnacknowledgedMessage): ProtoBuffer =
|
|
var pb = initProtoBuffer()
|
|
let msgPb = wire.encode(u.message)
|
|
pb.write(1, msgPb.buffer)
|
|
pb.write(2, uint64(u.sendTime.toUnixMs))
|
|
pb.write(3, uint32(u.resendAttempts))
|
|
pb.finish()
|
|
pb
|
|
|
|
proc decodeUnacked(buf: seq[byte]): ProtobufResult[UnacknowledgedMessage] =
|
|
let pb = initProtoBuffer(buf)
|
|
var msgBytes: seq[byte]
|
|
if not ?pb.getField(1, msgBytes):
|
|
return err(ProtobufError.missingRequiredField("UnacknowledgedMessage.message"))
|
|
let msg = SdsMessage.decode(msgBytes).valueOr:
|
|
return err(ProtobufError.missingRequiredField("UnacknowledgedMessage.message"))
|
|
var sendMs: uint64
|
|
if not ?pb.getField(2, sendMs):
|
|
return err(ProtobufError.missingRequiredField("UnacknowledgedMessage.sendTime"))
|
|
var attempts: uint32
|
|
discard pb.getField(3, attempts)
|
|
ok(
|
|
UnacknowledgedMessage.init(
|
|
message = msg,
|
|
sendTime = fromUnixMs(int64(sendMs)),
|
|
resendAttempts = int(attempts),
|
|
)
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# IncomingMessage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc encodeIncoming(m: IncomingMessage): ProtoBuffer =
|
|
var pb = initProtoBuffer()
|
|
let msgPb = wire.encode(m.message)
|
|
pb.write(1, msgPb.buffer)
|
|
for dep in m.missingDeps:
|
|
pb.write(2, dep) # SdsMessageID is string
|
|
pb.finish()
|
|
pb
|
|
|
|
proc decodeIncoming(buf: seq[byte]): ProtobufResult[IncomingMessage] =
|
|
let pb = initProtoBuffer(buf)
|
|
var msgBytes: seq[byte]
|
|
if not ?pb.getField(1, msgBytes):
|
|
return err(ProtobufError.missingRequiredField("IncomingMessage.message"))
|
|
let msg = SdsMessage.decode(msgBytes).valueOr:
|
|
return err(ProtobufError.missingRequiredField("IncomingMessage.message"))
|
|
var deps: seq[SdsMessageID]
|
|
discard pb.getRepeatedField(2, deps)
|
|
var depSet = initHashSet[SdsMessageID]()
|
|
for d in deps:
|
|
depSet.incl(d)
|
|
ok(IncomingMessage.init(message = msg, missingDeps = depSet))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OutgoingRepairEntry / OutgoingRepairKV
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc encodeOutRepairEntry(e: OutgoingRepairEntry): ProtoBuffer =
|
|
var pb = initProtoBuffer()
|
|
let histPb = wire.encodeHistoryEntry(e.outHistEntry)
|
|
pb.write(1, histPb.buffer)
|
|
pb.write(2, uint64(e.minTimeRepairReq.toUnixMs))
|
|
pb.finish()
|
|
pb
|
|
|
|
proc decodeOutRepairEntry(buf: seq[byte]): ProtobufResult[OutgoingRepairEntry] =
|
|
let pb = initProtoBuffer(buf)
|
|
var histBytes: seq[byte]
|
|
if not ?pb.getField(1, histBytes):
|
|
return err(ProtobufError.missingRequiredField("OutgoingRepairEntry.outHistEntry"))
|
|
let histPb = initProtoBuffer(histBytes)
|
|
let entry = ?wire.decodeHistoryEntry(histPb)
|
|
var ms: uint64
|
|
if not ?pb.getField(2, ms):
|
|
return err(ProtobufError.missingRequiredField("OutgoingRepairEntry.minTimeRepairReq"))
|
|
ok(
|
|
OutgoingRepairEntry.init(
|
|
outHistEntry = entry, minTimeRepairReq = fromUnixMs(int64(ms))
|
|
)
|
|
)
|
|
|
|
proc encodeOutRepairKV(kv: OutgoingRepairKV): ProtoBuffer =
|
|
var pb = initProtoBuffer()
|
|
pb.write(1, kv.messageId)
|
|
let entryPb = encodeOutRepairEntry(kv.entry)
|
|
pb.write(2, entryPb.buffer)
|
|
pb.finish()
|
|
pb
|
|
|
|
proc decodeOutRepairKV(buf: seq[byte]): ProtobufResult[OutgoingRepairKV] =
|
|
let pb = initProtoBuffer(buf)
|
|
var msgId: SdsMessageID
|
|
if not ?pb.getField(1, msgId):
|
|
return err(ProtobufError.missingRequiredField("OutgoingRepairKV.messageId"))
|
|
var entryBytes: seq[byte]
|
|
if not ?pb.getField(2, entryBytes):
|
|
return err(ProtobufError.missingRequiredField("OutgoingRepairKV.entry"))
|
|
let entry = ?decodeOutRepairEntry(entryBytes)
|
|
ok(OutgoingRepairKV(messageId: msgId, entry: entry))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# IncomingRepairEntry / IncomingRepairKV
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc encodeInRepairEntry(e: IncomingRepairEntry): ProtoBuffer =
|
|
var pb = initProtoBuffer()
|
|
let histPb = wire.encodeHistoryEntry(e.inHistEntry)
|
|
pb.write(1, histPb.buffer)
|
|
pb.write(2, e.cachedMessage)
|
|
pb.write(3, uint64(e.minTimeRepairResp.toUnixMs))
|
|
pb.finish()
|
|
pb
|
|
|
|
proc decodeInRepairEntry(buf: seq[byte]): ProtobufResult[IncomingRepairEntry] =
|
|
let pb = initProtoBuffer(buf)
|
|
var histBytes: seq[byte]
|
|
if not ?pb.getField(1, histBytes):
|
|
return err(ProtobufError.missingRequiredField("IncomingRepairEntry.inHistEntry"))
|
|
let histPb = initProtoBuffer(histBytes)
|
|
let entry = ?wire.decodeHistoryEntry(histPb)
|
|
var cached: seq[byte]
|
|
if not ?pb.getField(2, cached):
|
|
return err(ProtobufError.missingRequiredField("IncomingRepairEntry.cachedMessage"))
|
|
var ms: uint64
|
|
if not ?pb.getField(3, ms):
|
|
return err(ProtobufError.missingRequiredField("IncomingRepairEntry.minTimeRepairResp"))
|
|
ok(
|
|
IncomingRepairEntry.init(
|
|
inHistEntry = entry,
|
|
cachedMessage = cached,
|
|
minTimeRepairResp = fromUnixMs(int64(ms)),
|
|
)
|
|
)
|
|
|
|
proc encodeInRepairKV(kv: IncomingRepairKV): ProtoBuffer =
|
|
var pb = initProtoBuffer()
|
|
pb.write(1, kv.messageId)
|
|
let entryPb = encodeInRepairEntry(kv.entry)
|
|
pb.write(2, entryPb.buffer)
|
|
pb.finish()
|
|
pb
|
|
|
|
proc decodeInRepairKV(buf: seq[byte]): ProtobufResult[IncomingRepairKV] =
|
|
let pb = initProtoBuffer(buf)
|
|
var msgId: SdsMessageID
|
|
if not ?pb.getField(1, msgId):
|
|
return err(ProtobufError.missingRequiredField("IncomingRepairKV.messageId"))
|
|
var entryBytes: seq[byte]
|
|
if not ?pb.getField(2, entryBytes):
|
|
return err(ProtobufError.missingRequiredField("IncomingRepairKV.entry"))
|
|
let entry = ?decodeInRepairEntry(entryBytes)
|
|
ok(IncomingRepairKV(messageId: msgId, entry: entry))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ChannelMeta (top-level snapshot)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc encode*(meta: ChannelMeta): ProtoBuffer =
|
|
var pb = initProtoBuffer()
|
|
pb.write(1, meta.schemaVersion)
|
|
pb.write(2, uint64(meta.lamportTimestamp))
|
|
for u in meta.outgoingBuffer:
|
|
let entryPb = encodeUnacked(u)
|
|
pb.write(3, entryPb.buffer)
|
|
for m in meta.incomingBuffer:
|
|
let entryPb = encodeIncoming(m)
|
|
pb.write(4, entryPb.buffer)
|
|
for kv in meta.outgoingRepairBuffer:
|
|
let entryPb = encodeOutRepairKV(kv)
|
|
pb.write(5, entryPb.buffer)
|
|
for kv in meta.incomingRepairBuffer:
|
|
let entryPb = encodeInRepairKV(kv)
|
|
pb.write(6, entryPb.buffer)
|
|
pb.finish()
|
|
pb
|
|
|
|
proc decode*(T: type ChannelMeta, buf: seq[byte]): ProtobufResult[T] =
|
|
let pb = initProtoBuffer(buf)
|
|
var meta = ChannelMeta.init()
|
|
|
|
var ver: uint32
|
|
if not ?pb.getField(1, ver):
|
|
return err(ProtobufError.missingRequiredField("ChannelMeta.schemaVersion"))
|
|
if ver != ChannelMetaSchemaVersion:
|
|
# Per the contract: refuse loudly rather than silently truncating.
|
|
return err(ProtobufError.missingRequiredField(
|
|
"ChannelMeta.schemaVersion(unsupported)"
|
|
))
|
|
meta.schemaVersion = ver
|
|
|
|
var lts: uint64
|
|
if not ?pb.getField(2, lts):
|
|
return err(ProtobufError.missingRequiredField("ChannelMeta.lamportTimestamp"))
|
|
meta.lamportTimestamp = int64(lts)
|
|
|
|
var outBufs, inBufs, outRepBufs, inRepBufs: seq[seq[byte]]
|
|
discard pb.getRepeatedField(3, outBufs)
|
|
for b in outBufs:
|
|
meta.outgoingBuffer.add(?decodeUnacked(b))
|
|
discard pb.getRepeatedField(4, inBufs)
|
|
for b in inBufs:
|
|
meta.incomingBuffer.add(?decodeIncoming(b))
|
|
discard pb.getRepeatedField(5, outRepBufs)
|
|
for b in outRepBufs:
|
|
meta.outgoingRepairBuffer.add(?decodeOutRepairKV(b))
|
|
discard pb.getRepeatedField(6, inRepBufs)
|
|
for b in inRepBufs:
|
|
meta.incomingRepairBuffer.add(?decodeInRepairKV(b))
|
|
ok(meta)
|
|
|
|
proc serialize*(meta: ChannelMeta): Result[seq[byte], ReliabilityError] =
|
|
ok(encode(meta).buffer)
|
|
|
|
proc deserializeChannelMeta*(
|
|
data: seq[byte]
|
|
): Result[ChannelMeta, ReliabilityError] =
|
|
let m = ChannelMeta.decode(data).valueOr:
|
|
return err(ReliabilityError.reDeserializationError)
|
|
ok(m)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ChannelData (bootstrap payload)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc encode*(d: ChannelData): ProtoBuffer =
|
|
var pb = initProtoBuffer()
|
|
let metaPb = encode(d.meta)
|
|
pb.write(1, metaPb.buffer)
|
|
for m in d.messageHistory:
|
|
let msgPb = wire.encode(m)
|
|
pb.write(2, msgPb.buffer)
|
|
pb.finish()
|
|
pb
|
|
|
|
proc decode*(T: type ChannelData, buf: seq[byte]): ProtobufResult[T] =
|
|
let pb = initProtoBuffer(buf)
|
|
var d = ChannelData.init()
|
|
var metaBytes: seq[byte]
|
|
if not ?pb.getField(1, metaBytes):
|
|
return err(ProtobufError.missingRequiredField("ChannelData.meta"))
|
|
d.meta = ?ChannelMeta.decode(metaBytes)
|
|
var histBufs: seq[seq[byte]]
|
|
discard pb.getRepeatedField(2, histBufs)
|
|
for b in histBufs:
|
|
let m = SdsMessage.decode(b).valueOr:
|
|
return err(ProtobufError.missingRequiredField("ChannelData.messageHistory[i]"))
|
|
d.messageHistory.add(m)
|
|
ok(d)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HistoryUpdate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
proc encode*(u: HistoryUpdate): ProtoBuffer =
|
|
var pb = initProtoBuffer()
|
|
for m in u.append:
|
|
let msgPb = wire.encode(m)
|
|
pb.write(1, msgPb.buffer)
|
|
for id in u.evict:
|
|
pb.write(2, id)
|
|
pb.finish()
|
|
pb
|
|
|
|
proc decode*(T: type HistoryUpdate, buf: seq[byte]): ProtobufResult[T] =
|
|
let pb = initProtoBuffer(buf)
|
|
var u = HistoryUpdate.init()
|
|
var appBufs: seq[seq[byte]]
|
|
discard pb.getRepeatedField(1, appBufs)
|
|
for b in appBufs:
|
|
let m = SdsMessage.decode(b).valueOr:
|
|
return err(ProtobufError.missingRequiredField("HistoryUpdate.append[i]"))
|
|
u.append.add(m)
|
|
var ev: seq[SdsMessageID]
|
|
discard pb.getRepeatedField(2, ev)
|
|
u.evict = ev
|
|
ok(u)
|
|
|
|
{.pop.}
|