Skip to content

Commit 4807fa1

Browse files
afrindclaude
andcommitted
relay: install the local forwarder chain on subscribe-created tracks
A track created by its first subscriber left no entry in the publisher thread's LocalForwarderRegistry, so a subscriber landing on that thread joined a separate local forwarder instead of the publisher's. First subscriber setup now calls installPublisherForwarder on the publisher executor with removeOnEmpty=true, so the entry is there to find and is dropped with the last subscriber. The initial state for the requesting subscriber is captured from the publisher forwarder, so subscribers joining an existing attachment also get the most recent largest and extensions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b8e18c0 commit 4807fa1

4 files changed

Lines changed: 401 additions & 31 deletions

File tree

src/MoqxRelay.cpp

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -458,20 +458,18 @@ MoqxRelay::validatePublishNamespace(const FullTrackName& ftn, RequestID requestI
458458
// source ends, so a chain over a forwarder that does not hold it has nothing to remove.
459459
LocalForwarderRegistry::Claim MoqxRelay::installPublisherForwarder(
460460
const FullTrackName& ftn,
461-
const std::shared_ptr<MoQForwarder>& fwd
461+
const std::shared_ptr<MoQForwarder>& fwd,
462+
bool removeOnEmpty
462463
) {
463-
// removeOnEmpty=false: the publisher's forwarder must survive subscriber churn.
464+
auto& localReg = localRegistry();
464465
auto relayAdapter = std::make_shared<WeakRelayForwarderCallback>(weak_from_this());
465466
auto crossExec =
466467
std::make_shared<CrossExecForwarderCallback>(relayExec_, fwd, std::move(relayAdapter));
467-
fwd->setCallback(std::make_shared<LocalForwarderCallback>(
468-
tlForwarders_.get(),
469-
ftn,
470-
std::move(crossExec),
471-
/*removeOnEmpty=*/false
472-
));
468+
fwd->setCallback(
469+
std::make_shared<LocalForwarderCallback>(&localReg, ftn, std::move(crossExec), removeOnEmpty)
470+
);
473471

474-
return tlForwarders_->replace(ftn, fwd);
472+
return localReg.replace(ftn, fwd);
475473
}
476474

477475
// Called from LocalPublishFilter::publish() on publisherExec. Creates the publisher's
@@ -485,12 +483,8 @@ Subscriber::PublishResult MoqxRelay::publishFromPublisherExec(
485483
return folly::makeUnexpected(std::move(*err));
486484
}
487485

488-
if (!tlForwarders_.get()) {
489-
tlForwarders_.reset(new LocalForwarderRegistry());
490-
}
491-
492486
auto localPubFwd = std::make_shared<MoQForwarder>(pub.fullTrackName);
493-
installPublisherForwarder(pub.fullTrackName, localPubFwd)
487+
installPublisherForwarder(pub.fullTrackName, localPubFwd, /*removeOnEmpty=*/false)
494488
.markReady(InitialTrackState{pub.largest, pub.extensions});
495489

496490
// crossExecFilter is a channel subscriber for the relay exec
@@ -1137,6 +1131,10 @@ folly::coro::Task<void> MoqxRelay::addSubscriberAndPublishViaLocalForwarder(
11371131
);
11381132

11391133
auto [localFwd, isNew, localReg] = acquireLocalForwarder(ftn, initial);
1134+
if (!localFwd) {
1135+
// Setup for this track is in flight on this thread; drop the fanout.
1136+
co_return;
1137+
}
11401138

11411139
auto p = startPublish(subscriberSession, localFwd, forward, pinned, nullptr);
11421140
if (!p) {
@@ -1208,8 +1206,12 @@ MoqxRelay::acquireLocalForwarder(const FullTrackName& ftn, const InitialTrackSta
12081206
return {std::move(localFwd), /*isNew=*/true, localReg};
12091207
}
12101208
auto* ready = std::get_if<LocalForwarderRegistry::Ready>(&joined);
1211-
XCHECK(ready) << "local forwarder entry still pending; a caller failed to resolve its claim: "
1212-
<< ftn;
1209+
if (!ready) {
1210+
// A setup on this thread owns the entry and cannot be joined mid-flight. Drop the
1211+
// fanout rather than publishing over a subscribe that is still claiming the track.
1212+
XLOG(WARNING) << "local forwarder setup in flight, dropping publish fanout for " << ftn;
1213+
return {};
1214+
}
12131215
return {ready->forwarder, /*isNew=*/false, localReg};
12141216
}
12151217

@@ -1765,7 +1767,7 @@ std::optional<SubscribeError> MoqxRelay::completeUpstreamSubscription(
17651767
// Runs on relayExec_. Registers the subscribe and wires the local forwarder to the
17661768
// publisher; if it's a new subscription,also installs the passive relay chain, issues the
17671769
// upstream subscribe, and completes the setup. The subscriberExec tail reads the
1768-
// returned PublisherAttachment (handles upstreamOk).
1770+
// returned PublisherAttachment.
17691771
folly::coro::Task<MoqxRelay::PublisherAttachment> MoqxRelay::attachNewLocalForwarderOnRelayExec(
17701772
const SubscribeRequest& subReq,
17711773
LocalForwarderRegistry* localReg,
@@ -1809,6 +1811,12 @@ folly::coro::Task<MoqxRelay::PublisherAttachment> MoqxRelay::attachNewLocalForwa
18091811
co_await folly::coro::co_withExecutor(
18101812
folly::getKeepAliveToken(attach.publisherExec),
18111813
[&]() -> folly::coro::Task<void> {
1814+
LocalForwarderRegistry::Claim publisherClaim;
1815+
if (sr.firstSetup) {
1816+
publisherClaim =
1817+
installPublisherForwarder(ftn, attach.publisherFwd, /*removeOnEmpty=*/true);
1818+
}
1819+
18121820
installChannelSubscriber(
18131821
*cbs.channelCb,
18141822
*attach.publisherFwd,
@@ -1837,7 +1845,14 @@ folly::coro::Task<MoqxRelay::PublisherAttachment> MoqxRelay::attachNewLocalForwa
18371845
attach.publisherFwd,
18381846
setup.clientRequestID
18391847
);
1848+
if (upstreamResult->hasValue()) {
1849+
const auto& ok = upstreamResult->value();
1850+
publisherClaim.markReady(InitialTrackState{ok.largest, ok.extensions});
1851+
}
18401852
}
1853+
// Capture largest/extensions on publisherExec to set the initial state for the
1854+
// subscriber. Captured but ignored when the firstSetup returned an error.
1855+
attach.initial = InitialTrackState::capture(*attach.publisherFwd);
18411856
}()
18421857
);
18431858
// Back on relayExec_.
@@ -1882,7 +1897,6 @@ folly::coro::Task<MoqxRelay::PublisherAttachment> MoqxRelay::attachNewLocalForwa
18821897
attach.error = folly::makeUnexpected(std::move(*err));
18831898
co_return attach;
18841899
}
1885-
attach.upstreamOk = std::move(upstreamOk);
18861900
co_return attach;
18871901
}
18881902

@@ -1900,7 +1914,9 @@ MoqxRelay::joinOrPrepareUpstreamSubscription(SubscribeRequest subReq) {
19001914

19011915
auto firstOrSubsequent = registry_.getOrCreateFromSubscribe(
19021916
ftn,
1903-
shared_from_this(),
1917+
// installPublisherForwarder puts the real chain on the forwarder's own exec instead;
1918+
// a relay-direct callback would run there and touch registry_ off relayExec_.
1919+
std::shared_ptr<MoQForwarder::Callback>(nullptr),
19041920
[this, &ftn](std::shared_ptr<MoQForwarder> f) { return buildFilterChain(ftn, std::move(f)); }
19051921
);
19061922

@@ -2055,15 +2071,9 @@ folly::coro::Task<Publisher::SubscribeResult> MoqxRelay::subscribeFromSubscriber
20552071
co_return std::move(*attach.error);
20562072
}
20572073

2058-
// Only the first subscriber has an upstream OK. A subsequent one leaves this empty and
2059-
// marks the entry ready with no largest (a bug fixed in a subsequent commit)
2060-
InitialTrackState initial;
2061-
if (attach.upstreamOk) {
2062-
initial = InitialTrackState{attach.upstreamOk->largest, attach.upstreamOk->extensions};
2063-
// Also on the subscriber, so a post-SUBSCRIBE_OK joining fetch resolves.
2064-
initial.applyTo(*sub);
2065-
}
2066-
claim.setInitialState(initial);
2074+
claim.setInitialState(attach.initial);
2075+
// Also on the subscriber, so a post-SUBSCRIBE_OK joining fetch resolves.
2076+
attach.initial.applyTo(*sub);
20672077
replayPendingFowarderEvents(localFwd.get(), attach.finalCallback, *pendingCb, forward);
20682078
localFwd->tryProcessNewGroupRequest(subReq.params);
20692079
claim.markReady();

src/MoqxRelay.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,10 +366,12 @@ class MoqxRelay : public moxygen::Publisher,
366366
std::shared_ptr<moxygen::MoQSession> session
367367
);
368368

369-
// Runs on fwd's exec; the returned Claim owes a markReady/fail.
369+
// Runs on fwd's exec; the returned Claim owes a markReady/fail. removeOnEmpty=false for a
370+
// publish-initiated forwarder, which must survive subscriber churn.
370371
LocalForwarderRegistry::Claim installPublisherForwarder(
371372
const moxygen::FullTrackName& ftn,
372-
const std::shared_ptr<moxygen::MoQForwarder>& fwd
373+
const std::shared_ptr<moxygen::MoQForwarder>& fwd,
374+
bool removeOnEmpty
373375
);
374376

375377
std::optional<moxygen::PublishError>
@@ -491,7 +493,8 @@ class MoqxRelay : public moxygen::Publisher,
491493
folly::Executor* publisherExec{nullptr};
492494
bool ownsRelayChain{false}; // firstSetup path installed the passive relay chain
493495
std::shared_ptr<moxygen::MoQForwarder::Callback> finalCallback;
494-
std::optional<UpstreamOk> upstreamOk;
496+
// Captured off the publisher forwarder on its own exec, the only race-free place.
497+
InitialTrackState initial;
495498
std::optional<SubscribeResult> error; // set => bail
496499
};
497500

test/MoqxRelayPublishTests.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,9 @@ TEST_P(MoQRelayTest, PublishReconnectDuringSubscribeScopeGuardCrash) {
451451
// Relay state mutations must run on the relay executor; doPublishNamespaceDone
452452
// touches the namespace tree, which publishDone also cleans up via relayEvb_.
453453
verifyOnRelayExec([&] { relay_->doPublishNamespaceDone(kTestNamespace, publisherSession2); });
454+
// The publisher forwarder reaches the relay through relayExec_, so it drops its session
455+
// refs a hop after the calls above.
456+
driveIfMultiThread();
454457
}
455458

456459
// Same reconnect scenario but the upstream subscribe returns OK instead of an

0 commit comments

Comments
 (0)