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
2728namespace {
2829
30+ constexpr std::chrono::hours kEffectivelyWaitForever {24 };
31+
2932class 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