Skip to content

Commit adddad7

Browse files
Aman Sharmameta-codesync[bot]
authored andcommitted
Clean up established FETCH receivers on session close
Summary: This commit and the next one make it so that we send STOP_SENDING on the unidirectional stream (on which we are receiving the FETCH data) when the session is being cleaned up. Here's the flow: * `MoQSession::cleanup` calls `FetchTrackReceiveState::sessionClosed` * `FetchTrackReceiveState::sessionClosed` (defined in the next commit) calls `cancelSource_.requestCancellation` * The cancellation callback leads to sending a STOP_SENDING In addition to this, we call `reset` on the `FetchConsumer` in order to inform the application. ___ Differential Revision: D116635422 fbshipit-source-id: 68939c0365819aaa8cb0e1cb9d62594dd083092b
1 parent b15d868 commit adddad7

3 files changed

Lines changed: 113 additions & 13 deletions

File tree

moxygen/MoQSession.cpp

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2224,6 +2224,10 @@ class MoQSession::FetchTrackReceiveState
22242224
return fetchGroupOrder_;
22252225
}
22262226

2227+
uint32_t dataStreamCancelCode() const {
2228+
return dataStreamCancelCode_;
2229+
}
2230+
22272231
void resetFetchCallback(MoQSession* session) {
22282232
callback_.reset();
22292233
if (fetchOkAndAllDataReceived()) {
@@ -2246,8 +2250,25 @@ class MoQSession::FetchTrackReceiveState
22462250
resetFetchCallback(session);
22472251
}
22482252

2253+
void sessionClosed() {
2254+
if (!fetchEstablished_) {
2255+
return;
2256+
}
2257+
auto callback = std::exchange(callback_, nullptr);
2258+
if (bidiControl_) {
2259+
bidiControl_->cancel(ResetStreamErrorCode::SESSION_CLOSED);
2260+
}
2261+
dataStreamCancelCode_ =
2262+
folly::to_underlying(ResetStreamErrorCode::SESSION_CLOSED);
2263+
cancelSource_.requestCancellation();
2264+
if (callback) {
2265+
callback->reset(ResetStreamErrorCode::SESSION_CLOSED);
2266+
}
2267+
}
2268+
22492269
void fetchOK(FetchOk ok) {
22502270
XLOG(DBG1) << __func__ << " trackReceiveState=" << this;
2271+
fetchEstablished_ = true;
22512272
promise_.setValue(std::move(ok));
22522273
}
22532274

@@ -2280,6 +2301,8 @@ class MoQSession::FetchTrackReceiveState
22802301
GroupOrder fetchGroupOrder_;
22812302
folly::coro::Promise<FetchResult> promise_;
22822303
uint64_t currentStreamId_{0};
2304+
uint32_t dataStreamCancelCode_{0};
2305+
bool fetchEstablished_{false};
22832306
};
22842307

22852308
const std::shared_ptr<BidiStreamControl>&
@@ -2409,13 +2432,14 @@ MoQSession::~MoQSession() {
24092432

24102433
void MoQSession::cleanup() {
24112434
cancelGoawayTimeout();
2412-
// Each loop disarms its entry's bidi control before tearing it down so a
2413-
// peer close mid-shutdown can't race the canonical error delivery.
2414-
// fetches_ has no per-entry destroy loop, so it needs an upfront pass.
2415-
for (auto& [reqID, fetch] : fetches_) {
2435+
while (!fetches_.empty()) {
2436+
auto it = fetches_.begin();
2437+
auto fetch = std::move(it->second);
2438+
fetches_.erase(it);
24162439
if (const auto& control = fetch->bidiControl()) {
24172440
control->disarmOnPeerTermination();
24182441
}
2442+
fetch->sessionClosed();
24192443
}
24202444
while (!pubTracks_.empty()) {
24212445
auto it = pubTracks_.begin();
@@ -2453,10 +2477,6 @@ void MoQSession::cleanup() {
24532477
subTracks_.clear();
24542478
// We parse a publishDone after cleanup
24552479
reqIdToTrackAlias_.clear();
2456-
// TODO: there needs to be a way to queue an error in TrackReceiveState,
2457-
// both from here, when close races the FETCH stream, and from readLoop
2458-
// where we get a reset.
2459-
fetches_.clear();
24602480
for (auto& [reqID, pendingState] : pendingRequests_) {
24612481
if (const auto& control = pendingState->bidiControl()) {
24622482
control->disarmOnPeerTermination();
@@ -3735,6 +3755,7 @@ folly::coro::Task<void> MoQSession::dataStreamReadLoop(
37353755
auto rhToken = readHandle->getCancelToken();
37363756
XLOG(DBG1) << __func__ << " id=" << id << " sess=" << this;
37373757
bool isSubscriptionStream = false;
3758+
std::shared_ptr<FetchTrackReceiveState> fetchState;
37383759
std::optional<folly::CancellationCallback> fetchCancelCb;
37393760
auto g = folly::makeGuard([func = __func__, this, id, &isSubscriptionStream] {
37403761
XLOG(DBG1) << "exit " << func << " id=" << id << " sess=" << this;
@@ -3824,14 +3845,16 @@ folly::coro::Task<void> MoQSession::dataStreamReadLoop(
38243845

38253846
// Lambda for onFetch
38263847
auto onFetchFunc =
3827-
[this, &token, &codec, &fetchCancelCb, &readHandle](RequestID requestID) {
3848+
[this, &token, &codec, &fetchState, &fetchCancelCb, &readHandle](
3849+
RequestID requestID) {
38283850
auto state = getFetchTrackReceiveState(requestID);
38293851
if (state) {
38303852
// FetchTrackReceiveState lifecycle now controls read loop
3853+
fetchState = state;
38313854
token = state->getCancelToken();
3832-
fetchCancelCb.emplace(token, [&readHandle] {
3833-
if (readHandle) {
3834-
readHandle->stopSending(0);
3855+
fetchCancelCb.emplace(token, [&readHandle, &fetchState] {
3856+
if (readHandle && fetchState) {
3857+
readHandle->stopSending(fetchState->dataStreamCancelCode());
38353858
readHandle = nullptr;
38363859
}
38373860
});

moxygen/test/MoQSessionFetchTests.cpp

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ CO_TEST_P_X(MoQSessionTest, FetchBadLength) {
410410
co_await folly::coro::timeout(
411411
std::move(contract.second), std::chrono::milliseconds(100)),
412412
folly::FutureTimeout);
413+
EXPECT_CALL(*fetchCallback_, reset(ResetStreamErrorCode::SESSION_CLOSED));
413414
clientSession_->close(SessionCloseErrorCode::NO_ERROR);
414415
}
415416
CO_TEST_P_X(PreDraft18Test, FetchOverLimit) {
@@ -436,6 +437,8 @@ CO_TEST_P_X(PreDraft18Test, FetchOverLimit) {
436437
EXPECT_CALL(
437438
*clientSubscriberStatsCallback_,
438439
onFetchError(FetchErrorCode::INTERNAL_ERROR));
440+
EXPECT_CALL(*fetchCallback1, reset(ResetStreamErrorCode::SESSION_CLOSED));
441+
EXPECT_CALL(*fetchCallback2, reset(ResetStreamErrorCode::SESSION_CLOSED));
439442
res = co_await clientSession_->fetch(fetch, fetchCallback3);
440443
EXPECT_TRUE(res.hasError());
441444
}
@@ -470,6 +473,8 @@ CO_TEST_P_X(Draft18Test, FetchOverBidiStreamLimit) {
470473
EXPECT_TRUE(blocked.hasError());
471474
EXPECT_EQ(blocked.error().errorCode, FetchErrorCode::INTERNAL_ERROR);
472475

476+
EXPECT_CALL(*fetchCb, reset(ResetStreamErrorCode::SESSION_CLOSED))
477+
.Times(kLimit);
473478
clientSession_->close(SessionCloseErrorCode::NO_ERROR);
474479
}
475480
CO_TEST_P_X(MoQSessionTest, FetchOutOfOrder) {
@@ -691,6 +696,7 @@ CO_TEST_P_X(Draft18Test, FetchBidiStreamStopSending) {
691696
clientWt_->readHandles.at(0)->stopSending(0);
692697
co_await cancelBaton;
693698

699+
EXPECT_CALL(*fetchCallback_, reset(ResetStreamErrorCode::SESSION_CLOSED));
694700
clientSession_->close(SessionCloseErrorCode::NO_ERROR);
695701
}
696702

@@ -752,6 +758,71 @@ CO_TEST_P_X(Draft18Test, FetchBidiStreamFetchCancel) {
752758
clientSession_->close(SessionCloseErrorCode::NO_ERROR);
753759
}
754760

761+
CO_TEST_P_X(Draft18Test, FetchSessionCleanupBeforeDataStreamOpens) {
762+
co_await setupMoQSession();
763+
764+
expectFetch([](Fetch fetch, auto /*fetchPub*/) -> TaskFetchResult {
765+
co_return makeFetchOkResult(fetch, AbsoluteLocation{100, 100});
766+
});
767+
expectFetchSuccess();
768+
EXPECT_CALL(*clientSubscriberStatsCallback_, recordFetchLatency(_));
769+
auto res =
770+
co_await clientSession_->fetch(getFetch({0, 0}, {0, 1}), fetchCallback_);
771+
EXPECT_TRUE(res.hasValue());
772+
773+
folly::coro::Baton resetBaton;
774+
EXPECT_CALL(*fetchCallback_, reset(ResetStreamErrorCode::SESSION_CLOSED))
775+
.WillOnce([&] { resetBaton.post(); });
776+
777+
clientSession_->close(SessionCloseErrorCode::NO_ERROR);
778+
co_await resetBaton;
779+
}
780+
781+
CO_TEST_P_X(Draft18Test, FetchSessionCleanupWithOpenDataStream) {
782+
co_await setupMoQSession();
783+
784+
expectFetch([this](Fetch fetch, auto fetchPub) -> TaskFetchResult {
785+
eventBase_.add([fetchPub = std::move(fetchPub)] {
786+
EXPECT_TRUE(
787+
fetchPub
788+
->object(
789+
0, 0, 0, moxygen::test::makeBuf(100), noExtensions(), false)
790+
.hasValue());
791+
});
792+
co_return makeFetchOkResult(fetch, AbsoluteLocation{100, 100});
793+
});
794+
795+
folly::coro::Baton objectReceived;
796+
EXPECT_CALL(
797+
*fetchCallback_, object(0, 0, 0, HasChainDataLengthOf(100), _, false, _))
798+
.WillOnce([&] {
799+
objectReceived.post();
800+
return folly::unit;
801+
});
802+
expectFetchSuccess();
803+
EXPECT_CALL(*clientSubscriberStatsCallback_, recordFetchLatency(_));
804+
auto res =
805+
co_await clientSession_->fetch(getFetch({0, 0}, {0, 1}), fetchCallback_);
806+
EXPECT_TRUE(res.hasValue());
807+
co_await objectReceived;
808+
809+
folly::coro::Baton resetBaton;
810+
EXPECT_CALL(*fetchCallback_, reset(ResetStreamErrorCode::SESSION_CLOSED))
811+
.WillOnce([&] { resetBaton.post(); });
812+
auto dataStream = serverWt_->writeHandles.at(serverObjectStreamId());
813+
814+
clientSession_->close(SessionCloseErrorCode::NO_ERROR);
815+
co_await resetBaton;
816+
817+
auto* dataException = dataStream->writeException();
818+
EXPECT_NE(dataException, nullptr);
819+
if (dataException) {
820+
EXPECT_EQ(
821+
dataException->error,
822+
folly::to_underlying(ResetStreamErrorCode::SESSION_CLOSED));
823+
}
824+
}
825+
755826
// Once the FETCH data stream FINs, peer STOP_SENDING on the bidi is
756827
// informational and must not re-fire fetchCancel on the torn-down handle.
757828
CO_TEST_P_X(Draft18Test, NoFetchCancelAfterFetchComplete) {

moxygen/test/MoQSessionRequestUpdateTests.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1329,8 +1329,13 @@ CO_TEST_P_X(MoQSessionTest, FetchRequestUpdateNotSupported) {
13291329
EXPECT_EQ(updateResult.error().errorCode, RequestErrorCode::NOT_SUPPORTED);
13301330

13311331
// Complete the fetch
1332-
EXPECT_CALL(*fetchCallback_, endOfFetch());
1332+
folly::coro::Baton fetchComplete;
1333+
EXPECT_CALL(*fetchCallback_, endOfFetch()).WillOnce([&] {
1334+
fetchComplete.post();
1335+
return folly::unit;
1336+
});
13331337
fetchPubCaptured->endOfFetch();
1338+
co_await fetchComplete;
13341339

13351340
clientSession_->close(SessionCloseErrorCode::NO_ERROR);
13361341
}
@@ -1524,6 +1529,7 @@ CO_TEST_P_X(MoQSessionTest, FetchRequestUpdateNullSessionAfterAwait) {
15241529
// fetchComplete -> session_ = null. The cancellation token is also
15251530
// triggered, but co_awaitTry catches the OperationCancelled — execution
15261531
// continues to the dereference of session_ without a null check.
1532+
EXPECT_CALL(*fetchCallback_, reset(ResetStreamErrorCode::SESSION_CLOSED));
15271533
serverSession_->close(SessionCloseErrorCode::NO_ERROR);
15281534

15291535
// Post the baton — the coroutine resumes. Without the fix, this crashes

0 commit comments

Comments
 (0)