1414#include " relay/PublisherCrossExecFilter.h"
1515#include " relay/SubscriberCrossExecFilter.h"
1616#include " relay/WeakRelayForwarderCallback.h"
17+ #include < folly/ScopeGuard.h>
1718#include < folly/container/F14Set.h>
1819#include < moxygen/MoQFilters.h>
1920#include < moxygen/MoQTrackProperties.h>
@@ -451,28 +452,44 @@ MoqxRelay::validatePublishNamespace(const FullTrackName& ftn, RequestID requestI
451452 return std::nullopt ;
452453}
453454
454- // Constructs the publisher's local forwarder and installs its callback chain (Weak ->
455- // CrossExec -> LocalForwarder) on publisherExec, before the reply hops to relayExec_ .
456- // tlForwarders_ must already be initialized.
457- std::shared_ptr<MoQForwarder> MoqxRelay::createPublisherForwarder ( const PublishRequest& pub) {
458- const auto & ftn = pub. fullTrackName ;
459- auto localPubFwd = std::make_shared<MoQForwarder>(ftn, pub. largest );
460- localPubFwd-> setExtensions (pub. extensions );
461-
462- // removeOnEmpty=false: the publisher's forwarder must survive subscriber churn, so
463- // LocalForwarderCallback removes it from tlForwarders_ only when the source ends.
455+ // Installs the publisher- forwarder callback chain (Weak -> CrossExec(relayExec_) ->
456+ // LocalForwarder) and claims the tlForwarders_ slot. Must run on the forwarder's exec .
457+ void MoqxRelay::installPublisherForwarderCallbackChain (
458+ const FullTrackName& ftn,
459+ const std::shared_ptr<MoQForwarder>& publisherFwd,
460+ bool removeOnEmpty
461+ ) {
462+ if (!tlForwarders_. get ()) {
463+ tlForwarders_. reset ( new LocalForwarderRegistry ());
464+ }
464465 auto relayAdapter = std::make_shared<WeakRelayForwarderCallback>(weak_from_this ());
465466 auto crossExec = std::make_shared<CrossExecForwarderCallback>(
466467 relayExec_,
467- localPubFwd ,
468+ publisherFwd ,
468469 std::move (relayAdapter)
469470 );
470- localPubFwd ->setCallback (std::make_shared<LocalForwarderCallback>(
471+ publisherFwd ->setCallback (std::make_shared<LocalForwarderCallback>(
471472 tlForwarders_.get (),
472473 ftn,
473474 std::move (crossExec),
474- /* removeOnEmpty= */ false
475+ removeOnEmpty
475476 ));
477+ // Authoritative slot claim; displaces any stale subscribe-path local forwarder so
478+ // same-thread subscribers reuse THIS forwarder via the fast path. Publish forwarders
479+ // (removeOnEmpty=false) are seeded at creation; subscribe claims await upstream OK.
480+ tlForwarders_->set (ftn, publisherFwd, /* seeded=*/ !removeOnEmpty);
481+ }
482+
483+ // Constructs the publisher's local forwarder and installs its callback chain (Weak ->
484+ // CrossExec -> LocalForwarder) on publisherExec, before the reply hops to relayExec_.
485+ std::shared_ptr<MoQForwarder> MoqxRelay::createPublisherForwarder (const PublishRequest& pub) {
486+ const auto & ftn = pub.fullTrackName ;
487+ auto localPubFwd = std::make_shared<MoQForwarder>(ftn, pub.largest );
488+ localPubFwd->setExtensions (pub.extensions );
489+
490+ // removeOnEmpty=false: the publisher's forwarder must survive subscriber churn, so
491+ // LocalForwarderCallback removes it from tlForwarders_ only when the source ends.
492+ installPublisherForwarderCallbackChain (ftn, localPubFwd, /* removeOnEmpty=*/ false );
476493
477494 return localPubFwd;
478495}
@@ -488,17 +505,9 @@ Subscriber::PublishResult MoqxRelay::publishFromPublisherExec(
488505 return folly::makeUnexpected (std::move (*err));
489506 }
490507
491- if (!tlForwarders_.get ()) {
492- tlForwarders_.reset (new LocalForwarderRegistry ());
493- }
494-
508+ // createPublisherForwarder claims the tlForwarders_ slot on this exec.
495509 auto localPubFwd = createPublisherForwarder (pub);
496510
497- // The publisher's forwarder is authoritative — claim the slot, displacing any
498- // stale subscribe-path local forwarder so same-thread subscribers reuse THIS
499- // forwarder via the fast path.
500- tlForwarders_->set (pub.fullTrackName , localPubFwd);
501-
502511 // crossExecFilter is a channel subscriber for the relay exec
503512 // regulsterPublishOnRelay exec completes wiring the chain (topNFilter → terminationFilter →
504513 // cache).
@@ -886,14 +895,17 @@ void teardownLocalForwarderOnFailure(
886895// Single-threaded mode:
887896// forwarder.callback = MoqxRelay (direct, no hop)
888897//
889- // Multi-threaded — publisher forwarder (lives on publisherExec):
898+ // Multi-threaded — publisher forwarder (lives on publisherExec). Built by
899+ // installPublisherForwarderCallbackChain for BOTH publish-initiated (removeOnEmpty=false,
900+ // survives churn) and subscribe-initiated (removeOnEmpty=true, drops on empty) tracks:
890901// publisherFwd.callback =
891- // CrossExecForwarderCallback(relayExec_, publisherFwd,
892- // WeakRelayForwarderCallback(relay))
902+ // LocalForwarderCallback(tlForwarders_, ftn,
903+ // CrossExecForwarderCallback(relayExec_, publisherFwd,
904+ // WeakRelayForwarderCallback(relay)))
893905//
894- // [publisherExec] CrossExecForwarderCallback: captures ftn by value,
895- // dispatches to relayExec_ fire-and-forget
896- // ↓
906+ // [publisherExec] LocalForwarderCallback: removes from tlForwarders_ (onPublishDone
907+ // always; onEmpty if removeOnEmpty), passes the rest through
908+ // ↓ (CrossExecForwarderCallback dispatches to relayExec_ fire-and-forget)
897909// [relayExec_] WeakRelayForwarderCallback: recovers relay via weak_ptr,
898910// calls onEmptyImpl / forwardChangedImpl / newGroupRequestedImpl
899911//
@@ -1762,6 +1774,7 @@ folly::coro::Task<MoqxRelay::PublisherAttachment> MoqxRelay::attachNewLocalForwa
17621774 bool forward
17631775) {
17641776 // Runs on relayExec_.
1777+ XCHECK (mode () == Mode::LocalForwarder) << " subscribe-init chain install is LF-only" ;
17651778 const auto & ftn = subReq.fullTrackName ;
17661779 PublisherAttachment attach;
17671780
@@ -1796,6 +1809,10 @@ folly::coro::Task<MoqxRelay::PublisherAttachment> MoqxRelay::attachNewLocalForwa
17961809 co_await folly::coro::co_withExecutor (
17971810 folly::getKeepAliveToken (attach.publisherExec ),
17981811 [&]() -> folly::coro::Task<void > {
1812+ // First subscriber installs the publisher chain + tl slot before any sub is added.
1813+ if (sr.firstSetup ) {
1814+ installPublisherForwarderCallbackChain (ftn, attach.publisherFwd , /* removeOnEmpty=*/ true );
1815+ }
17991816 installChannelSubscriber (
18001817 *cbs.channelCb ,
18011818 *attach.publisherFwd ,
@@ -1888,7 +1905,7 @@ MoqxRelay::joinOrPrepareUpstreamSubscription(SubscribeRequest subReq) {
18881905
18891906 auto firstOrSubsequent = registry_.getOrCreateFromSubscribe (
18901907 ftn,
1891- shared_from_this () ,
1908+ /* callback= */ nullptr ,
18921909 [this , &ftn](std::shared_ptr<MoQForwarder> f) { return buildFilterChain (ftn, std::move (f)); }
18931910 );
18941911
@@ -1952,14 +1969,27 @@ folly::coro::Task<Publisher::SubscribeResult> MoqxRelay::subscribeFromSubscriber
19521969 acquireLocalForwarder (ftn, [&] { return std::make_shared<MoQForwarder>(ftn); });
19531970
19541971 if (!isNew) {
1972+ // Wait for an in-flight isNew=true setup to seed largest, else SUBSCRIBE_OK carries
1973+ // the pre-seeding value a client reads as a track restart.
1974+ if (auto ready = localReg->readiness (ftn); ready && !ready->isFulfilled ()) {
1975+ co_await ready->getSemiFuture ();
1976+ }
19551977 if (auto err = checkRangeNotInPast (*localFwd, subReq)) {
19561978 co_return folly::makeUnexpected (std::move (*err));
19571979 }
19581980 co_return attachSubscriber (*localFwd, std::move (session), subReq, std::move (consumer));
19591981 }
19601982
1961- // isNew=true: this thread owns setup. Install PendingForwarderCallback first so
1962- // forwardChanged/newGroupRequested/onEmpty events during setup are captured for replay.
1983+ // Fulfill on every exit (incl. error/cancel below) so isNew=false waiters never hang.
1984+ auto readiness = localReg->beginReadiness (ftn);
1985+ auto fulfillReadiness = folly::makeGuard ([&readiness]() noexcept {
1986+ if (!readiness->isFulfilled ()) {
1987+ readiness->setValue ();
1988+ }
1989+ });
1990+
1991+ // Install PendingForwarderCallback first so forwardChanged/newGroupRequested/onEmpty
1992+ // events during setup are captured for replay.
19631993 auto pendingCb = std::make_shared<PendingForwarderCallback>(localReg, ftn);
19641994 localFwd->setCallback (pendingCb);
19651995
0 commit comments