Skip to content

fix: treat peer-forwarded PUBLISH_NAMESPACE as discovery-only - #220

Merged
englishm merged 2 commits into
cloudflare:mainfrom
englishm-cloudflare:fix/peer-namespace-discovery-only
Aug 28, 2026
Merged

fix: treat peer-forwarded PUBLISH_NAMESPACE as discovery-only#220
englishm merged 2 commits into
cloudflare:mainfrom
englishm-cloudflare:fix/peer-namespace-discovery-only

Conversation

@englishm-cloudflare

Copy link
Copy Markdown
Contributor

In multi-relay deployments that use a Coordinator implementation for coordination, when relay A fans a PUBLISH_NAMESPACE out to relay B, B now registers the namespace for discovery only — visible to list_namespaces_matching, absent from route_namespace, with no coordinator ownership claim and no local route. This matches the existing behavior in the SUBSCRIBE_NAMESPACE pull path.

Previously, peer-forwarded announces went through the same registration path as client announces, which incorrectly claimed coordinator ownership of a namespace already owned by the originating relay and created a local routing entry that could not serve subscriptions. A downstream SUBSCRIBE now resolves through the coordinator to the originating relay as expected.

A unit test pins the discovery-only contract on the unservable path so that future routing changes produce an explicit failure rather than a silent DoesNotExist.

Also in this PR:

impl Drop for UpstreamReadyTx — when an upstream session ends before acknowledging a forwarded SUBSCRIBE, the readiness gate now resolves with a diagnostic error instead of closing silently. The check is gated on the pre-write state so it does not fire on gates that have already resolved.

session accepted log — records the session's internal/public classification at the point it is determined. That classification controls whether a peer's PUBLISH_NAMESPACE is treated as proxied or as a local client announce.

A downstream SUBSCRIBE for an uncached track queues a TrackRequest and
waits on a readiness gate until the session that advertised the namespace
answers the upstream SUBSCRIBE. That session frequently dies without
answering: it can return early, be torn down, or have its task dropped
while parked mid-handshake. The sender then disappeared with the gate
still Pending, and the only trace was a closed channel, which the waiter
reported as a bare internal error naming neither the cause nor the
session responsible.

Resolve the gate from Drop instead. The publisher really is gone in all
of those cases, so failing is the correct outcome rather than just a
better log line, and the reason now travels in the error itself: the
waiter already logs it with the namespace and track attached, and the
downstream REQUEST_ERROR carries an honest reason phrase. The wire error
code is unchanged.

Two details are load-bearing:

  - Drop only writes when the gate is still Pending. The sender outlives
    established() by the whole lifetime of the subscription, so an
    unconditional write would turn every completed track into a reported
    failure for later subscribers.

  - Drop constructs the error directly rather than via internal_ctx,
    which mints a UUID; Uuid::new_v4 panics instead of degrading when the
    OS entropy source is unavailable. Drop runs during unwinding, where a
    panic aborts the process, and the failure would be self-amplifying:
    a dead RNG makes every internal_ctx call panic, and unwinding drops
    senders whose Drop would hit the same dead RNG.

established() and failed() now record through send_replace, because send
refuses to update once the last receiver is gone and would leave a
resolved subscription indistinguishable from an abandoned one.
A namespace forwarded by a peer relay was handled as if a local client had
published it: registered as a local route, claimed in the coordinator, and
given a request queue. All three are wrong for a namespace this relay is
only relaying, and they combine into two failures that hide each other.

Claiming ownership races the origin for one key and loses. The losing path
returns before REQUEST_OK and unwinds the local registration it had
already published, which downstream reads as the namespace being withdrawn
milliseconds after it appeared while the publisher is still live.

The local route is worse, because it points somewhere that cannot serve.
A downstream subscribe matches it and is sent back up the forwarding
session, where the peer published an advertise-only track container whose
request channel is closed at creation. That publication takes priority
over the peer's unknown-subscribe fall-through, so the subscribe is
answered DoesNotExist even though the peer holds a working route. And a
dialed session has no Producer draining subscribed() at all, so it could
not be served even if the container were sound.

The ownership rejection was masking that: tearing down the local route
forced a retry through the coordinator, which worked. So removing only the
ownership claim turns a masked defect into a hard failure — verified in
the cross-relay harness, where the transit test passes before that change
and fails with code=16 after it.

Register the namespace for discovery instead, the way the
SUBSCRIBE_NAMESPACE pull path already does: advertised to
list_namespaces_matching, absent from route_namespace. A downstream
subscribe then misses locally and resolves through the coordinator to the
owner, which serves it over an accepted session that does have a Producer.
With no local route there is no queue to strand either.

Also records the resolved interface for every accepted session. Whether a
peer is classified internal depends on the platform populating local_ip,
and nothing observed it: if it is absent, every peer classifies as a
public client and this path is silently never taken, degrading to exactly
the previous behaviour with no error anywhere. Confirmed Internal in the
harness; unverified in production, which this log makes answerable.

The advertise-only containers are left in place but no longer reachable
from a subscribe. Both call sites now say so, and a unit test pins that
such a container accepts a subscribe it can never serve, so a future
change routing one there fails loudly rather than returning DoesNotExist.
@englishm-cloudflare
englishm-cloudflare force-pushed the fix/peer-namespace-discovery-only branch from ab057d1 to e5dc1a3 Compare August 28, 2026 02:39
@englishm
englishm merged commit c290dd8 into cloudflare:main Aug 28, 2026
2 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.

2 participants