Skip to content

Commit 44f99d4

Browse files
evanjzoumeta-codesync[bot]
authored andcommitted
Add E2E cancellation test for BiDi streaming
Summary: Add `ClientDropsStreamWhileServerBlocksOnInput` test that exercises the full cancellation path through a real Thrift client/server. The server echo transform blocks on `input.next()`, the client sends one item via sink then holds it open, reads the echoed item from the stream, and destroys the stream generator — triggering cancellation that propagates through the full stack. Reviewed By: sazonovkirill Differential Revision: D94977385 fbshipit-source-id: ef800c74ff7803e9e537dc08da113e8decaffd36
1 parent d8aa163 commit 44f99d4

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

third-party/thrift/src/thrift/lib/cpp2/async/tests/BiDiServiceE2ETest.cpp

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <gtest/gtest.h>
1818
#include <folly/coro/Collect.h>
1919
#include <folly/coro/GtestHelpers.h>
20+
#include <folly/coro/Sleep.h>
2021
#include <thrift/lib/cpp2/async/tests/util/gen-cpp2/TestBiDiService.h>
2122
#include <thrift/lib/cpp2/util/ScopedServerInterfaceThread.h>
2223

@@ -26,6 +27,8 @@ namespace apache::thrift {
2627

2728
namespace {
2829

30+
constexpr std::chrono::hours kEffectivelyWaitForever{24};
31+
2932
class BiDiServiceE2ETest : public Test {
3033
using MakeChannelFunc = ScopedServerInterfaceThread::MakeChannelFunc;
3134

@@ -490,4 +493,160 @@ CO_TEST_F(BiDiServiceE2ETest, ConsumeInputNoOutput) {
490493
EXPECT_EQ(handler->counter, kTestLimit);
491494
}
492495

496+
CO_TEST_F(BiDiServiceE2ETest, ClientDropsStreamWhileServerBlocksOnInput) {
497+
struct Handler : public ServiceHandler<detail::test::TestBiDiService> {
498+
folly::coro::Task<StreamTransformation<std::string, std::string>> co_echo()
499+
override {
500+
co_return StreamTransformation<std::string, std::string>{
501+
[](folly::coro::AsyncGenerator<std::string&&> input)
502+
-> folly::coro::AsyncGenerator<std::string&&> {
503+
while (auto item = co_await input.next()) {
504+
co_yield std::move(*item);
505+
}
506+
}};
507+
}
508+
};
509+
510+
testConfig({std::make_shared<Handler>()});
511+
auto client = makeClient<detail::test::TestBiDiService>();
512+
auto bidi = co_await client->co_echo();
513+
514+
// Sink: send one item, then hold open without sending more
515+
auto sinkGen = folly::coro::co_invoke(
516+
[]() -> folly::coro::AsyncGenerator<std::string&&> {
517+
co_yield std::string("hello");
518+
co_await folly::coro::sleep(kEffectivelyWaitForever);
519+
});
520+
521+
auto sinkTask = folly::coro::co_invoke(
522+
[clientSink = std::move(bidi.sink),
523+
sinkGen = std::move(sinkGen)]() mutable -> folly::coro::Task<void> {
524+
try {
525+
co_await std::move(clientSink).sink(std::move(sinkGen));
526+
} catch (...) {
527+
// Expected when server cancels the sink
528+
}
529+
});
530+
531+
auto streamTask = folly::coro::co_invoke(
532+
[streamGen = std::move(bidi.stream).toAsyncGenerator()]() mutable
533+
-> folly::coro::Task<void> {
534+
// Read the echoed item
535+
auto first = co_await streamGen.next();
536+
EXPECT_TRUE(first.has_value());
537+
EXPECT_EQ(*first, "hello");
538+
// streamGen destroyed on return -> cancels the stream
539+
// Server is blocked on input.next() waiting for more sink data
540+
// With cancellation support, this unblocks the server
541+
});
542+
543+
co_await folly::coro::collectAll(std::move(sinkTask), std::move(streamTask));
544+
}
545+
546+
CO_TEST_F(BiDiServiceE2ETest, ServerStopsOutputWhileClientBlocksOnStream) {
547+
struct Handler : public ServiceHandler<detail::test::TestBiDiService> {
548+
folly::coro::Task<StreamTransformation<std::string, std::string>> co_echo()
549+
override {
550+
co_return StreamTransformation<std::string, std::string>{
551+
[](folly::coro::AsyncGenerator<std::string&&> input)
552+
-> folly::coro::AsyncGenerator<std::string&&> {
553+
// Read one item, echo it, then stop producing output
554+
if (auto item = co_await input.next()) {
555+
co_yield std::move(*item);
556+
}
557+
// Returning here destroys the input generator via RAII,
558+
// which should cancel the client's sink
559+
}};
560+
}
561+
};
562+
563+
testConfig({std::make_shared<Handler>()});
564+
auto client = makeClient<detail::test::TestBiDiService>();
565+
auto bidi = co_await client->co_echo();
566+
567+
// Sink: send one item, then hold open without sending more
568+
auto sinkGen = folly::coro::co_invoke(
569+
[]() -> folly::coro::AsyncGenerator<std::string&&> {
570+
co_yield std::string("hello");
571+
co_await folly::coro::sleep(kEffectivelyWaitForever);
572+
});
573+
574+
auto sinkTask = folly::coro::co_invoke(
575+
[clientSink = std::move(bidi.sink),
576+
sinkGen = std::move(sinkGen)]() mutable -> folly::coro::Task<void> {
577+
try {
578+
co_await std::move(clientSink).sink(std::move(sinkGen));
579+
} catch (...) {
580+
// Expected when server cancels the sink
581+
}
582+
});
583+
584+
auto streamTask = folly::coro::co_invoke(
585+
[streamGen = std::move(bidi.stream).toAsyncGenerator()]() mutable
586+
-> folly::coro::Task<void> {
587+
// Read the echoed item
588+
auto first = co_await streamGen.next();
589+
EXPECT_TRUE(first.has_value());
590+
EXPECT_EQ(*first, "hello");
591+
// Server stopped producing, so the stream should complete
592+
auto end = co_await streamGen.next();
593+
EXPECT_FALSE(end.has_value());
594+
});
595+
596+
co_await folly::coro::collectAll(std::move(sinkTask), std::move(streamTask));
597+
}
598+
599+
CO_TEST_F(
600+
BiDiServiceE2ETest, ClientDropsStreamWhileServerProducesIndefinitely) {
601+
struct Handler : public ServiceHandler<detail::test::TestBiDiService> {
602+
folly::coro::Task<StreamTransformation<std::string, std::string>> co_echo()
603+
override {
604+
co_return StreamTransformation<std::string, std::string>{
605+
[](folly::coro::AsyncGenerator<std::string&&> /*input*/)
606+
-> folly::coro::AsyncGenerator<std::string&&> {
607+
// Ignore input, produce output forever until cancelled
608+
int i = 0;
609+
while (true) {
610+
co_yield std::to_string(i++);
611+
co_await folly::coro::co_safe_point;
612+
}
613+
}};
614+
}
615+
};
616+
617+
testConfig({std::make_shared<Handler>()});
618+
auto client = makeClient<detail::test::TestBiDiService>();
619+
auto bidi = co_await client->co_echo();
620+
621+
// Sink: hold open without sending anything
622+
auto sinkTask = folly::coro::co_invoke(
623+
[clientSink = std::move(bidi.sink)]() mutable -> folly::coro::Task<void> {
624+
try {
625+
auto emptyGen = folly::coro::co_invoke(
626+
[]() -> folly::coro::AsyncGenerator<std::string&&> {
627+
co_await folly::coro::sleep(kEffectivelyWaitForever);
628+
});
629+
co_await std::move(clientSink).sink(std::move(emptyGen));
630+
} catch (...) {
631+
// Expected when server cancels the sink
632+
}
633+
});
634+
635+
auto streamTask = folly::coro::co_invoke(
636+
[streamGen = std::move(bidi.stream).toAsyncGenerator()]() mutable
637+
-> folly::coro::Task<void> {
638+
// Read a few items to confirm the server is producing
639+
auto first = co_await streamGen.next();
640+
EXPECT_TRUE(first.has_value());
641+
EXPECT_EQ(*first, "0");
642+
auto second = co_await streamGen.next();
643+
EXPECT_TRUE(second.has_value());
644+
EXPECT_EQ(*second, "1");
645+
// streamGen destroyed on return -> cancels the stream
646+
// Server's co_safe_point should observe cancellation and stop
647+
});
648+
649+
co_await folly::coro::collectAll(std::move(sinkTask), std::move(streamTask));
650+
}
651+
493652
} // namespace apache::thrift

0 commit comments

Comments
 (0)