Skip to content

sync: SVS v4 (mhash, PARTIAL, publish+pull) via feature branch - #202

Open
Taranum01 wants to merge 18 commits into
named-data:psvsfrom
Taranum01:psvs/svs-v4
Open

sync: SVS v4 (mhash, PARTIAL, publish+pull) via feature branch#202
Taranum01 wants to merge 18 commits into
named-data:psvsfrom
Taranum01:psvs/svs-v4

Conversation

@Taranum01

@Taranum01 Taranum01 commented Aug 11, 2026

Copy link
Copy Markdown

Follow-up to #190 — same content, landed via a feature branch off psvs per @tianyuan129's suggestion. Refactor passes will be done on this branch before merging into psvs.

What's in this PR

Identical content to #190:

  • SVS v4 wire format: mhash (0xCB), FullStateVector (0xCD), PartialStateVector (0xCE), SvsDataRef (0x07).
  • Spec at docs/svs-v4.md.
  • Core in std/sync/, package renamed std/ndn/svs/v3std/ndn/svs/v4.
  • e2e bumps: e2e/dv_util.converge deadline 30s → 90s, e2e/test_001.py post-put sleep 30s → 90s.
  • Client retry/lifetime: unchanged (Retries:3, Lifetime:1s).

Taranum Wasu added 18 commits July 21, 2026 12:45
This rewrites the State Vector Sync protocol to v4 in the ndnd implementation.
v4 introduces a membership hash (mhash) carried on every Sync Data, two
embedded State Vector encodings (FULL and PARTIAL), and a publish-only form
that references a retrievable full vector at .../32=sv/<version>.
Highlights:
  * SvsData now carries MemberSetHash and VectorType on every Sync Data;
    there is no legacy StateVector-only wire form.
  * New publication sends embedded PARTIAL when FULL exceeds
    SyncVectorThreshold; entry [0] is always the sender's own entry.
    If the sender-only baseline itself exceeds the threshold, the sender
    falls back to publish+pull rather than emit a PARTIAL vector missing
    the required entry [0].
  * Periodic sync and mhash mismatch recovery use publish+pull: produce
    full-vector Data at .../32=sv/<version>, then announce-only Sync Data
    carrying mhash + SvsDataRef.
  * pullFullVector is debounced per sender (5s) to bound the pull fan-in
    when many peers cross the membership hash boundary simultaneously.
  * SyncDataName wire version is bumped from v=3 to v=4.
File renames and surface API changes:
  * ComputeMhash -> ComputeMembershipHash (in svs_membership_hash.go).
The wire-level Go TLV package path std/ndn/svs/v3/ is unchanged because
it is an internal ndnd package name, not part of the wire profile.
Covers the new v4 surface:
  * Membership hash: stability across rerun, name-order independence,
    sensitivity to membership changes, insensitivity to SeqNo values.
  * Embedded FULL / PARTIAL round-trip and decode.
  * Publish-only form decoding: mhash + SvsDataRef, no StateVector.
  * handleMhashMismatch behavior when local superset, when only the
    sender added members, and when only remote added members.
  * buildAnnounceSvsData + parseFullVectorContent round-trip.
  * Pull-debounce per-sender gate (pullFullVectorMinInterval).
Uses ComputeMembershipHash (renamed from ComputeMhash in the previous
commit).
Standalone v4 specification, renamed from the previous "v3 revision" doc.
Renames the protocol version throughout (sync interest name v=4, section
references updated), replaces "announce + pull" with "publish + pull" and
"announce-only" with "publish-only", and rewrites section 5.6 with explicit
"Sender procedure" and "Receiver procedure" subsections.
Adds rationale for keeping VectorType on the wire (Section 3.4): mhash alone
cannot distinguish FULL from PARTIAL because two parties with identical
membership but different subscription views may legitimately disagree on
what subset was sent.
Section 4.3 notes that auto-MTU sizing of SyncVectorThreshold is planned
future work and is intentionally out of scope for v4.
Section 5.6 includes an implementation note documenting the 5s per-sender
debounce on pullFullVector as a local detail that does not affect protocol
correctness.
The fetcher was using NewFixedCongestionWindow(100), which ignores
all congestion signals. On the 53-node sprint e2e topology, 8
concurrent ndnd cat consumers each maintain 100 outstanding
Interests, producing ~800 concurrent Interests that overwhelm the
network. With no congestion response, loss rate climbs and a single
segment loses 3 retries in a row, aborting the cat fetch with
'retries exhausted, segment number=N'.
Switch to NewAIMDCongestionWindow(100). The AIMD window halves on
SigLoss/SigCongest and grows by 1/cwnd on SigData. The signals are
already emitted in handleResult; this just wires them to a window
that responds.
Verified locally: all 3 scenarios (NDNd, NFD, NDNd replay) pass on
the sprint topology.
* onReceiveStateVector: gate handleMhashMismatch on FULL/publish-only.
  PARTIAL vectors are subsets by design, so the recvSv tuple-count
  superset check in handleMhashMismatch would spuriously trigger
  sendRecoveryAnnounce on every PARTIAL receipt from a node whose
  own view is a superset (i.e. always, in steady state).
* partialTargets: fix the comment to say it returns only repair
  targets; propagation is currently unused and always nil.
* partialCandidateNames: use a single SortFunc comparator for
  (recency desc, canonical name asc). The previous code sorted by
  name then by recency in two passes, but slices.SortFunc is not
  stable so the first sort is not a reliable tie-breaker for
  entries with equal recency scores.
The metadata and prefix fetch paths in client_consume use default
Retries: 3 and Lifetime: 1s. On the 53-node sprint e2e topology,
DV's startup broadcasts a global Reset on every node, which can
take up to 9 seconds (locally) or more (in CI) to fully drain. Even
after the test announces 'routing converged', a late Reset arriving
at a consumer wipes the producer prefix from its route table. The
producer's re-announcement follows over the next few DV sync cycles,
so a 4-second retry budget can be exhausted before routes stabilize.

Bumping to Retries: 5 and Lifetime: 2s gives ~10s of client-side
budget for this transient post-convergence gap. Verifies locally
on the sprint topology under all three scenarios (NDNd, NFD, NDNd
replay). Client-side retry/lifetime are not part of the SVS v4 wire
format; this only affects the consumer's tolerance for slow DV
propagation.
docs: rename §1.2 row names to 'Inline FULL / Inline PARTIAL / Out-of-band
FULL' and replace 'embedded' with 'inline' throughout the spec to match.

docs: simplify mhash description in §3 (point to §4.2).

docs: remove 'Future work' block from §4.3 (out of scope for v4).

docs: clarify §6.2 outdated-vector rule applies to FULL only; PARTIAL
omissions are a subset by design.

docs: remove v3 / 'Python strawman' / NDNLPv2 / 'ndnd object segmentation
APIs' references.

code: bump rrSegFetcher maxRetries 3 -> 5 (matches PR description and the
existing 'TODO: make it configurable' comment; aligns with metadata fetch
retry budget in client_consume.go).

code: drop long 'clarification' comment on sortPartialTail (kept the
one-line summary).

Spec logic is unchanged. Build passes.
docs: §3.1 inline-form 'VectorType is only meaningful in the inline form' ->
'The inline form carries VectorType; the publish-only form does not.'

docs: §3.2 'If an entry is absent' -> 'A missing entry compares as
SeqNo = 0 against a present entry.'

docs: §3.3 drop 'is not a hash of the full State Vector and not a hash of
sequence numbers'; describe what mhash is in positive terms and note that
it is unaffected by data publications.

docs: §3.3 'Membership data and State Vector data are separate concepts' ->
describe how the full State Vector carries membership implicitly.

docs: §4.1 / §4.2 redundant 'Set VectorType = ...' -> cross-references
to §3.4.

docs: §6.2 'applies to FULL only ... never indicate' -> 'applies to FULL.
... do not carry any information about whether A is outdated relative
to B.'

code: drop 'there is no legacy StateVector-only mode' from
SyncVectorThreshold doc comment.

Spec logic is unchanged. Build passes.
Address the four open Copilot code-level comments on SVS v4:

* onSyncData: reject SvsData that omit the required 32-byte MemberSetHash
  (mhash) and, for inline form, that omit or carry an invalid VectorType.
  Prevents PARTIAL-as-FULL misclassification and skipped recovery.
* parseFullVectorContent: require VectorType=FULL and a 32-byte mhash on
  fetched full-vector Data; always verify mhash against the local
  computation (no more 'if present' opt-in).
* pullFullVector: lazy-init lastPullTime under the mutex so the debounce
  is safe for SvSync instances built via keyed struct literals (tests).
* partialTargets: sort the repair name list in NDN canonical order so
  PARTIAL selection is deterministic across runs.

Add unit tests for missing-mhash and missing-VectorType cases on
parseFullVectorContent.

Spec logic is unchanged. All std/... tests pass.
Address two more review comments on SVS v4:

* SyncVectorThreshold: move from SvSyncOpts to a package-level constant
  (syncVectorThreshold = 1200). The threshold is not application-tunable:
  it is a fixed library constant. Removes the public field from SvSyncOpts
  and the <= 0 default-application block in NewSvSync.
* Rename SVS Go identifiers to drop 'announce':
    buildAnnounceSvsData    -> buildPublishSvsData
    shouldUseAnnouncePull   -> shouldUsePublishPull
    sendRecoveryAnnounce    -> sendRecoveryPublish
    TestSvsDataAnnounceTLV         -> TestSvsDataPublishTLV
    TestBuildAnnounceSvsData       -> TestBuildPublishSvsData
    TestShouldUseAnnouncePull      -> TestShouldUsePublishPull
    TestEncodeSyncDataAnnounceMode -> TestEncodeSyncDataPublishMode
  Also clean up the 'announce-only Sync Data' and 'announce or pull
  recovery' comments and rename the test fixture variable.

docs/svs-v4.md: spec §1.2, §4.3, §8 now describe SyncVectorThreshold as
a fixed library constant (1200 bytes), not an application-configured
parameter.

The Client.AnnouncePrefix API (used for routing prefix announcement, not
SVS) is left unchanged.

Spec logic is unchanged. All std/... tests pass.
ExpressR previously only retried on InterestResultTimeout. A persistent
Nack (e.g. NackReasonNoRoute) failed the request immediately, even
within the configured retry budget. On the 53-node sprint topology the
DV/NLSR startup race produces a burst of prefix resets; the first
metadata Interest from a scenario transition can arrive at a forwarder
whose FIB has not yet been re-populated for the producer's /test
sub-prefix, triggering an immediate Nack.

* ExpressR: treat InterestResultNack with NackReasonNoRoute or
  NackReasonCongestion as retryable, alongside the existing Timeout
  retry. The retry budget (Retries) is now respected for both
  transient timeouts and transient Nacks; non-retryable Nacks
  (e.g. Duplicate) still pass through immediately.
* rrSegFetcher.handleResult: when a segment Interest is Nacked with
  NackReasonNoRoute, treat it as a loss so the AIMD window shrinks
  and the segment is retried, instead of aborting the fetch.

Verified locally: all three scenarios (NDNd, NFD, NDNd replay) pass
on the 53-node sprint topology.
- Rename "inline" to "direct" and "out-of-band" to "referenced" for the
  two StateVector delivery forms (more descriptive, per a-thieme).
- PARTIAL vector layout: replace "[0]" / "[1...n]" notation with prose
  ("the first entry is the sender's own entry").
- Clarify single-member mhash in §5.7 and §7.4: the membership set is
  {N} for a joining node.
- Dedupe the mhash definition: the §1.2 paragraph now points to §3.3
  instead of restating SHA-256 digest.
- Drop the "ndnd" qualifier on segmentation; the spec is forwarder-agnostic.
- §6.2: clarify that "A is outdated" applies when A is FULL, and explain
  why PARTIAL receivers cannot use the omitted-name test.

Source: PR named-data#190 review comments addressed:
  3612873010, 3612971226, 3612986026, 3641436856, 3641470105, 3641509945
- encodePartialStateVector: return an empty StateVector (no entries)
  instead of nil when the sender-only baseline exceeds the size budget.
  The caller treats the empty vector as the publish+pull trigger.
  Doc updated; spec §4.2 explicitly allows the empty-PARTIAL signal.

- buildSvsDataForSend: detect the empty result with len(...Entries) == 0.

- partialCandidateNames -> priorityOrderedPeers, more descriptive for
  review and maintenance.

- sortPartialTail: drop the redundant trailing sort in
  encodePartialStateVector — the inner-loop sort already produces a
  tail-sorted slice.

- ComputeMembershipHash: rename the local `tuples` variable to
  `membershipTuples` to disambiguate from the spec tuple.

- shouldUsePublishPull: now returns (usePublish, full, size). The
  build-the-full-SvsData-once pattern means the inline-full case no
  longer re-encodes for size check + send.

- deriveFullVectorPrefix and resolveFullVectorPrefix: doc clarified to
  call out the helper as an implementation convention, not a spec
  constraint (the spec only requires a sender-controlled full-vector
  prefix; callers wire to a different location by setting
  opts.FullVectorPrefix).

- buildPublishSvsData: doc added explaining what `ref` is (the
  sender's published full-vector Data name) and how it is consumed.

Verified locally: all 3 e2e scenarios pass (NDNd + NFD + replay).

Source: PR named-data#190 review comments addressed:
  3613120231, 3613133568, 3613158180, 3613167366,
  3613250227, 3613262079, 3613275363
…ctor TLVs

Per Adam's review note in the v4 spec, the VectorType discriminator
field is removed from SvsData. The wire TLV itself now disambiguates
between direct FULL (0xCD), direct PARTIAL (0xCE), and publish-only
SvsDataRef (0x07). MemberSetHash moves to the top-level SvsData TLV
so all three forms carry it in the same place.
The StateVector type itself is unchanged and remains used directly by
DV's advertisement (which has its own simpler wire shape).
- definitions.go: introduce FullStateVector and PartialStateVector
  structs, drop VectorType field and constants.
- accessors.go (new): helpers Kind / IsFull / IsPartial /
  GetStateVector so callers don't have to switch on which struct
  was non-nil.
- svs_pull.go, svs_encode.go: build FULL/PARTIAL SvsData with the
  new struct types; mhash on SvsData, vector on inner struct.
- svs.go: svSyncRecvSvArgs now carries partial bool; onSyncData
  routes by IsFull/IsPartial and rejects malformed direct forms.
- svs_test.go: all tests rewritten against the new constructors
  and accessors; TestSvsDataLegacyParse removed.
- dv/dv/advert_sync.go: use spec_svs.StateVector directly (DV's
  wire shape was always a single-entry StateVector).
- docs/svs-v4.md: collapse §3.1, add new TLVs to the table in §3.2,
  replace §3.4 VectorType section with FullStateVector vs
  PartialStateVector, update §4 / §5 / §6 / §7 / §8 references.
Tests: std/sync passes. ndnd build unchanged aside from new SVS
v4 wire format.
goimports had two complaints on the prior commit:
  * std/ndn/svs/v3/accessors.go was missing a trailing newline.
  * std/sync/svs_pull.go had a misaligned struct literal in
    shouldUsePublishPull (MemberSetHash not column-aligned with
    FullStateVector after the SvsData tagged-union reshuffle).
Rename the wire-spec Go package std/ndn/svs/v3 -> std/ndn/svs/v4 to
match the spec filename (svs-v4.md) and the new wire behaviour
(mhash, distinct FULL / PARTIAL / SvsDataRef TLVs). The folder
previously advertised itself as the v3 package, but the wire
format it now produces is v4. Per Adam's review note: keeping
"v3" in the import path would be misleading for any downstream
consumer pulling this ndnd build.
  * git mv std/ndn/svs/v3 std/ndn/svs/v4
  * update import path in 9 caller files (std/sync/*, dv/dv/*,
    std/ndn/svs_ps/*) and the one in-tree comment reference.
  * struct names, TLV type numbers, and the spec text are
    unchanged - only the import path moves.
Tests: std/sync, std/object/storage, std/security pass.
goimports -l clean on all touched files.
…bump

Per Adam's review (PR named-data#190, comment 3641275220): the DV
convergence flake on the 52-node sprint topology should be fixed
by giving the test more time to converge, not by relaxing the
client's metadata/prefix fetch retry budget.
Reverts std/object/client_consume.go to Retries:3 / Lifetime:1s
on both fetchMetadata and fetchDataByPrefix (the values used
before commit 9340c00). The library default is now consistent
across all consumers, not just the sprint e2e.
Bumps the e2e wait times instead:
  * dv_util.converge deadline: 30s -> 90s
    Worst observed post-DV-startup propagation in CI is ~20s
    (9340c00 commit message); 90s gives a 4x safety margin.
  * test_001 scenario_ndnd_fw post-put sleep: 30s -> 90s
    The cat-phase failure mode in CI was a metadata Timeout
    ~13s after the put started (Time=cat ≈ Put+30s in failing
    runs). After 8 simultaneous puts, DV has to re-converge for
    the new --expose prefixes; 90s of post-put quiescence is
    enough for the late "Reset" storm to fully drain.
Test runtime impact: scenario_ndnd_fw goes from ~5min to ~7min.
Tests: std/object, std/sync, std/security pass locally.
Two Copilot review nits on PR named-data#190, both approved by Adam.
* svs.go onSyncData (the direct-form gate): the previous check
  `!IsFull() && !IsPartial()` only rejected packets that had
  neither form. A malformed packet carrying both FullStateVector
  and PartialStateVector, or carrying SvsDataRef alongside an
  embedded vector, was being processed. Tighten to
  `IsFull() == IsPartial() || len(SvsDataRef) > 0` so only the
  exactly-one-of-{FULL, PARTIAL}-and-no-ref shape is accepted.
* svs_encode.go encodePartialStateVector: ComputeMembershipHash(state)
  is constant for a given state but was being recomputed for the
  sender-only baseline check and again for every trial entry. Hoist
  to a single `mhash` local at the top of the function.
PR description also updated to drop the now-stale references to
the previous VectorType discriminator, the v3 package path, the
"Retries 3 -> 5" / "Lifetime 1s -> 2s" claim, and the legacy
`Threshold <= 0` mode. The new description matches the wire, the
package path, and the e2e wait windows that are actually shipped.
Tests: std/sync passes.
Carries the SVS v4 sync surface (mhash, FullStateVector /
PartialStateVector / SvsDataRef TLVs, and publish + pull recovery) from
PR named-data#190 onto a feature branch off psvs. Refactor passes will happen on
this branch before merging into psvs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant