Skip to content

Phase C3: ContentDeliveryLayerNode — the per-stream-part overlay node#86

Merged
ptesavol merged 1 commit into
mainfrom
claude/phase-c3
Jul 12, 2026
Merged

Phase C3: ContentDeliveryLayerNode — the per-stream-part overlay node#86
ptesavol merged 1 commit into
mainfrom
claude/phase-c3

Conversation

@ptesavol

Copy link
Copy Markdown
Collaborator

Summary

Ports the content-delivery overlay node and its factory from the TS pin — the class that composes the C1/C2 classes (temporary connections, handshaking, neighbor finding/updating) with propagation, inspection (C7) and the discovery layer (C4) into the actual per-stream-part node. The test porting flushed out five real defects, each diagnosed with runtime evidence (crash-report stacks, rate comparisons) and verified fixed.

What's ported

  • ContentDeliveryLayerNode — neighbor list + the four contact views (nearby/random/ring-left/ring-right) fed from discovery-layer events, broadcast with per-publisher duplicate detection, leave-notice handling with view eviction, temporary-connection and content-delivery RPC server wiring, suppressOwnMessageLoopback, and TS-parity stop(). Plumtree branches are milestone-E scope and omitted.
  • createContentDeliveryLayerNode — defaults taken from the TS source (neighbor target 4, view size 20, min propagation targets 2, 150-message/10 s buffer, 10 s update interval); explicit shared-pointer ownership graph in place of TS garbage collection, with documented destruction order.
  • DhtNodeDiscoveryLayer + DhtNode contact-event re-emission — TS passes a DhtNode wherever a DiscoveryLayerNode is expected (structural typing). DhtNode now re-emits the six PeerManager contact events as its own, exactly like TS (second event-emitter base with merged lookup), and the adapter provides the interface nominally. C6's NetworkStack will reuse it.

Five defects found by the ported tests

  1. NodeList data race — worker threads iterated lists being mutated concurrently (64-node SetUp segfault; crash-report stack through getValuesOfIncludedKeys). All access mutex-guarded; NodeAdded/NodeRemoved emit outside the lock (their handlers make blocking RPC sends).
  2. NodeList ordering parity — TS's Map is insertion-ordered and the interleave protocol depends on it (getLast = newest neighbor to hand over); the std::map port was id-ordered. Now an insertion-ordered vector; TS's ordering test ported.
  3. Teardown lazy-task lifetimestop()'s leave-notice coroutines captured remotes whose last references concurrent handlers could drop before the batch ran (the documented rpc-remote lazy-task trap). The batch pins the remotes; verified 0/3 → 8/8 on the crashing fixture.
  4. previousMessageRef optionality — TS passes undefined when absent; the port forwarded the proto3 default instance, tripping the DuplicateMessageDetector invariant for (0, 0) refs. Optional through the whole chain now.
  5. Synchronous propagation deadlocksendToNeighbor ran blockingWait inside the RPC delivery path, so under broadcast fan-out nodes blocked sending to each other in cycles and propagation stalled (0/3 timeouts at 64 nodes). Propagation sends are now Task-returning bounded scope tasks (3/3 after); ProxyClient keeps its synchronous result-reporting API through a new awaitable collect variant.

Tests (all ported from the pin)

TS test C++
unit/ContentDeliveryLayerNode.test.ts (incl. loopback-suppression cases) ContentDeliveryLayerNodeTest (9 tests)
integration/ContentDeliveryLayerNode-Layer1Node.test.ts ContentDeliveryLayerNodeLayer1Test (single node / 4 nodes + bidirectionality / 64 nodes) over real DhtNodes + simulator
integration/...-Latencies.test.ts same fixture parameterized with 50 ms fixed latency (the TS files differ only in that)
integration/Propagation.test.ts PropagationScaleTest at 64 nodes (TS runs 256; the C++ simulator's per-node cost makes that a ~16 min run, beyond CI budget — scale-up documented as follow-up)
unit/NodeList.test.ts (14 cases) NodeListTest (stub → full port, incl. insertion order)

Results (local, arm64 mac)

trackerless-network 83/83 with verified clean process exit, streamr-dht 181/181 (regression for the DhtNode event additions), full-tree build green, both package lints and the repo-wide lint rc=0.

Remaining milestone-C phases after this: C5 (ContentDeliveryManager + proxy server side), C6 (NetworkStack/NetworkNode), C8 (full-node end-to-end + TS interop).

🤖 Generated with Claude Code

Ports the content-delivery overlay node and its factory from the TS pin,
composing the C1/C2 classes with propagation, inspection (C7) and the
discovery layer (C4):

- ContentDeliveryLayerNode: neighbor list + four contact views fed from
  discovery-layer events, broadcast with per-publisher duplicate
  detection, leave-notice handling, RPC server wiring, and TS-parity
  stop() (leave notices + rpcCommunicator destroy). Plumtree branches
  are milestone-E scope and omitted.
- createContentDeliveryLayerNode: defaults taken from the TS source
  (neighbor target 4, view size 20, min propagation targets 2,
  150-message/10 s buffer, 10 s update interval); components are
  shared_ptr-wired with documented destruction order.
- DhtNodeDiscoveryLayer adapter + DhtNode contact-event re-emission:
  TS passes a DhtNode wherever a DiscoveryLayerNode is expected
  (structural typing); DhtNode now re-emits the six PeerManager contact
  events like TS (second event-emitter base, merged lookup) and the
  adapter provides the interface nominally.

Five real defects found and fixed by the ported tests (all verified
with before/after runs per the runtime-evidence methodology):

1. NodeList data race: worker threads iterated lists freed by
   concurrent replaceAll/add (64-node SetUp SIGSEGV, crash-report
   stack). All access is now mutex-guarded; NodeAdded/NodeRemoved emit
   OUTSIDE the lock (their handlers make blocking RPC sends).
2. NodeList ordering parity: the TS NodeList is insertion-ordered and
   the interleave protocol depends on it (getLast = newest neighbor);
   the std::map-backed port was id-ordered. Now an insertion-ordered
   vector; the TS ordering test is ported.
3. Teardown lazy-task lifetime: stop()'s leave-notice coroutines
   captured remotes whose last references concurrent handlers could
   drop before the batch ran (the rpc-remote lazy-task trap; teardown
   SIGSEGV). The batch now pins the remotes.
4. previousMessageRef optionality: TS passes undefined when absent; the
   port forwarded the proto3 default instance, violating the
   DuplicateMessageDetector invariant for (0,0) refs. Now optional
   through the chain.
5. Synchronous propagation deadlock: sendToNeighbor ran blockingWait
   inside the RPC delivery path — under broadcast fan-out nodes blocked
   sending to each other in cycles and propagation stalled (0/3 timeouts
   at 64 nodes). Propagation sends are now Task-returning scope tasks
   (3/3 after); ProxyClient keeps its synchronous result API via an
   awaitable collect variant.

Tests ported: unit/ContentDeliveryLayerNode.test.ts (+ loopback
suppression cases), integration/ContentDeliveryLayerNode-Layer1Node
.test.ts and its -Latencies variant (shared fixture, 50 ms fixed
latency), integration/Propagation.test.ts (64 nodes instead of TS's
256 — the C++ simulator's per-node cost makes 256 a ~16 min run, beyond
CI budget; scale-up is a documented follow-up), and the full
unit/NodeList.test.ts (14 cases).

Local: trackerless-network 83/83 with clean process exit, streamr-dht
181/181, full-tree build green, both package lints rc=0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ptesavol
ptesavol merged commit e1e3a8f into main Jul 12, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant