Skip to content

relay: merge multiple upstream publishers per track (§9.5)#20

Merged
floatdrop merged 2 commits into
draft-18from
feat/multiple-publishers-per-track
Jun 26, 2026
Merged

relay: merge multiple upstream publishers per track (§9.5)#20
floatdrop merged 2 commits into
draft-18from
feat/multiple-publishers-per-track

Conversation

@floatdrop

Copy link
Copy Markdown
Owner

What

Implements full §9.5 multiple-publishers-per-track in the relay. A track can now be ingested from N upstreams — redundant origins, a graceful switchover overlap, or several discovered relays — and merged into a single clean outbound subgroup stream per subscriber, with each object deduplicated and seamless continuity when one origin drops out.

Previously the relay assumed one usable upstream per track; two publishers of the same (Namespace, Name) produced two outbound streams per subscriber (a §2.2 violation) and double-delivered every object.

How

  • Shared fan-out merge. Outbound subgroup writers move from per-inbound-stream (local to runFanout) to a per-(GroupID, SubgroupID) shared, refcounted set on TrackEntry (SharedSubgroup + Acquire/ReleaseSubgroup). All inbound streams for one Subgroup share one writer per subscriber, so a Subgroup is never split across streams (§2.2).
  • Dedup ledger. TrackEntry.ClaimDelivered keyed by {GroupID, ObjectID} (§2.1 — SubgroupID is not part of object identity). It lives on the entry (not a per-Subgroup structure) so redundant streams that don't temporally overlap still dedup, and it's independent of the size-bounded Object Cache so a peer lagging by more than the cache capacity dedups correctly. Memory is bounded to the most recent deliveredGroupWindow (32) groups.
  • Failover teardown. FIN-vs-reset is aggregated across contributors: the merged stream FINs if any upstream completed cleanly, resets only if all reset — one origin failing doesn't disturb subscribers (§2.2 reset/"upstream conditions" carve-out).
  • Subscribe to all. handler_subscribe now subscribes to every matching publisher (all local + all remotes via upstreamPool.resolveAll) with source dedup, instead of stopping at the first.

Spec conformance

Verified against draft-ietf-moq-transport-18: object identity {GroupID, ObjectID} (§2.1, Subgroup ID excluded); one-stream-per-Subgroup (§2.2) with its reset/out-of-order carve-out covering failover.

Tests

  • Redundant-origin dedup → one stream, each object once
  • Mid-stream failover continuity (survivor continues, clean FIN)
  • Disjoint fan-in union
  • Dedup survives cache eviction (the regression that motivated the entry-level group-windowed ledger)
  • Two-remote cross-relay fan-in (dial-all + dedup)
  • ClaimDelivered unit test (dedup + window pruning)

go build ./..., go test ./..., and go test -race ./pkg/relay/... are green; gofmt/go vet/modernize clean.

Notes / residual scope

  • Late publishers that begin advertising after an on-demand upstream set is established aren't retroactively pulled until the set drains (documented in README); proactive PUBLISHers are always merged.
  • subscribeUpstream now does a Discovery lookup + sequential remote dials on every SUBSCRIBE even when a local publisher already succeeded — a latency cost on cross-relay deployments, left as a follow-up (could be parallelized / short-circuited).

🤖 Generated with Claude Code

floatdrop and others added 2 commits June 26, 2026 17:53
Implements full §9.5 multiple-publishers-per-track. The relay can now
ingest a track from N upstreams — redundant origins, a switchover
overlap, or several discovered relays — and fan them into a single
clean outbound subgroup stream per subscriber, deduplicating objects so
each is delivered exactly once and continuing seamlessly when one origin
drops out.

Core change: outbound subgroup writers move from per-inbound-stream
(local to runFanout) to a per-(GroupID, SubgroupID) shared, refcounted
set on TrackEntry (SharedSubgroup / Acquire/ReleaseSubgroup). N inbound
streams for the same Subgroup share ONE writer per subscriber, so the
relay never splits a Subgroup across streams (§2.2).

Deduplication uses a persistent, group-windowed ledger on the
TrackEntry (ClaimDelivered), keyed by {GroupID, ObjectID} per §2.1. It
lives on the entry rather than on a per-Subgroup structure so redundant
streams that do not temporally overlap (one origin's subgroup FINs
before the peer's arrives) still dedup, and it is independent of the
size-bounded Object Cache so a peer lagging by more than the cache
capacity dedups correctly. Memory is bounded to the most recent
deliveredGroupWindow groups.

Teardown aggregates FIN-vs-reset across contributors: the merged stream
FINs if any upstream completed cleanly and resets only if all reset, so
one origin failing does not disturb subscribers.

handler_subscribe now subscribes to every matching publisher (all local
publishers + all remotes via upstreamPool.resolveAll) with source dedup,
instead of stopping at the first.

Tests: redundant-origin dedup, mid-stream failover continuity, disjoint
fan-in union, dedup surviving cache eviction, two-remote cross-relay
fan-in, and a ClaimDelivered unit test. Full suite and -race are green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@floatdrop floatdrop merged commit d4011fd into draft-18 Jun 26, 2026
9 checks passed
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