@@ -771,6 +771,84 @@ TEST(WriteConnectionBuffered, ErrorFailsPendingFlushes) {
771771 EXPECT_THAT (f2.get (), StatusIs (resume_error.code ()));
772772}
773773
774+ TEST (WriteConnectionBuffered, FlushResumesAndDoesNotCompletePrematurely) {
775+ AsyncSequencer<bool > sequencer;
776+
777+ auto expected_write_size = [](std::size_t n) {
778+ return ResultOf (
779+ " payload size" , [](auto payload) { return payload.size (); }, Eq (n));
780+ };
781+
782+ auto mock1 = std::make_unique<MockAsyncWriterConnection>();
783+ EXPECT_CALL (*mock1, UploadId).WillRepeatedly (Return (" test-upload-id" ));
784+ EXPECT_CALL (*mock1, PersistedState)
785+ .WillRepeatedly (Return (MakePersistedState (0 )));
786+ // The first flush fails, triggering resume.
787+ EXPECT_CALL (*mock1, Flush (expected_write_size (8 * 1024 ))).WillOnce ([&](auto ) {
788+ return sequencer.PushBack (" Flush1" ).then (
789+ [](auto ) { return Status (StatusCode::kUnavailable , " try again" ); });
790+ });
791+
792+ auto mock2 = std::make_unique<MockAsyncWriterConnection>();
793+ EXPECT_CALL (*mock2, UploadId).WillRepeatedly (Return (" test-upload-id" ));
794+ // OnResume will query persisted state. We return 0, meaning the data was
795+ // lost.
796+ EXPECT_CALL (*mock2, PersistedState)
797+ .WillRepeatedly (Return (MakePersistedState (0 )));
798+ // The resumed connection should receive the Flush call again.
799+ EXPECT_CALL (*mock2, Flush (expected_write_size (8 * 1024 ))).WillOnce ([&](auto ) {
800+ return sequencer.PushBack (" Flush2" ).then ([](auto ) { return Status{}; });
801+ });
802+ // After Flush2 succeeds, it will query the status.
803+ EXPECT_CALL (*mock2, Query).WillOnce ([&]() {
804+ return sequencer.PushBack (" Query" ).then ([](auto ) {
805+ return make_status_or (static_cast <std::int64_t >(8 * 1024 ));
806+ });
807+ });
808+
809+ MockFactory mock_factory;
810+ EXPECT_CALL (mock_factory, Call).WillOnce ([&]() {
811+ return sequencer.PushBack (" Resume" ).then ([&](auto ) {
812+ return make_status_or (
813+ std::unique_ptr<storage::AsyncWriterConnection>(std::move (mock2)));
814+ });
815+ });
816+
817+ auto connection = MakeWriterConnectionBuffered (
818+ mock_factory.AsStdFunction (), std::move (mock1), TestOptions ());
819+
820+ auto f = connection->Flush (TestPayload (8 * 1024 ));
821+ ASSERT_FALSE (f.is_ready ());
822+
823+ // Let the first flush fail.
824+ auto next = sequencer.PopFrontWithName ();
825+ EXPECT_EQ (next.second , " Flush1" );
826+ next.first .set_value (true );
827+
828+ // Trigger resume.
829+ next = sequencer.PopFrontWithName ();
830+ EXPECT_EQ (next.second , " Resume" );
831+ next.first .set_value (true );
832+
833+ // The flush promise must not be ready yet, because the data has not been
834+ // flushed on the new connection.
835+ ASSERT_FALSE (f.is_ready ());
836+
837+ // Let the second flush succeed.
838+ next = sequencer.PopFrontWithName ();
839+ EXPECT_EQ (next.second , " Flush2" );
840+ next.first .set_value (true );
841+
842+ // Let the query complete.
843+ next = sequencer.PopFrontWithName ();
844+ EXPECT_EQ (next.second , " Query" );
845+ next.first .set_value (true );
846+
847+ // Now the flush promise should be completed.
848+ ASSERT_TRUE (f.is_ready ());
849+ EXPECT_STATUS_OK (f.get ());
850+ }
851+
774852TEST (WriteConnectionBuffered, CloseEmpty) {
775853 AsyncSequencer<bool > sequencer;
776854 auto mock = std::make_unique<MockAsyncWriterConnection>();
@@ -950,29 +1028,53 @@ TEST(WriteConnectionBuffered, CloseFailsAndResumeSucceedsButNotClosed) {
9501028TEST (WriteConnectionBuffered, CloseFailsAndResumeSucceedsAndFinalized) {
9511029 AsyncSequencer<bool > sequencer;
9521030 auto close_error = TransientError ();
1031+ auto write_error = TransientError ();
1032+ auto resume2_error = PermanentError ();
9531033
954- auto mock = std::make_unique<MockAsyncWriterConnection>();
955- EXPECT_CALL (*mock , UploadId).WillRepeatedly (Return (" test-upload-id" ));
956- EXPECT_CALL (*mock , PersistedState)
1034+ auto mock1 = std::make_unique<MockAsyncWriterConnection>();
1035+ EXPECT_CALL (*mock1 , UploadId).WillRepeatedly (Return (" test-upload-id" ));
1036+ EXPECT_CALL (*mock1 , PersistedState)
9571037 .WillRepeatedly (Return (MakePersistedState (0 )));
958- EXPECT_CALL (*mock , Close).WillOnce ([&](auto ) {
1038+ EXPECT_CALL (*mock1 , Close).WillOnce ([&](auto ) {
9591039 return sequencer.PushBack (" Close" ).then (
9601040 [close_error](auto ) { return close_error; });
9611041 });
9621042
1043+ auto resumed_mock = std::make_unique<MockAsyncWriterConnection>();
1044+ auto * resumed_mock_ptr = resumed_mock.get ();
1045+
1046+ // Mock Write on the resumed connection to fail, to trigger a second resume.
1047+ EXPECT_CALL (*resumed_mock_ptr, Write).WillOnce ([&](auto ) {
1048+ return sequencer.PushBack (" Write2" ).then (
1049+ [write_error](auto ) { return write_error; });
1050+ });
1051+
9631052 MockFactory mock_factory;
964- EXPECT_CALL (mock_factory, Call).WillOnce ([&]() {
965- return sequencer.PushBack (" Resume" ).then ([](auto ) {
966- auto resumed_mock = std::make_unique<MockAsyncWriterConnection>();
967- EXPECT_CALL (*resumed_mock, PersistedState)
968- .WillRepeatedly (Return (TestObject ()));
969- return make_status_or (std::unique_ptr<storage::AsyncWriterConnection>(
970- std::move (resumed_mock)));
1053+ {
1054+ InSequence seq;
1055+ // The resume will succeed and return a new mock that reports the object is
1056+ // already finalized (which implies closed).
1057+ EXPECT_CALL (mock_factory, Call)
1058+ .WillOnce ([&, rm = std::move (resumed_mock)]() mutable {
1059+ return sequencer.PushBack (" Resume" ).then ([rm = std::move (rm)](
1060+ auto ) mutable {
1061+ EXPECT_CALL (*rm, PersistedState)
1062+ .WillRepeatedly (Return (TestObject ()));
1063+ return make_status_or (
1064+ std::unique_ptr<storage::AsyncWriterConnection>(std::move (rm)));
1065+ });
1066+ });
1067+ // The second resume (after write failure) will fail.
1068+ EXPECT_CALL (mock_factory, Call).WillOnce ([&]() {
1069+ return sequencer.PushBack (" Resume2" ).then ([resume2_error](auto ) {
1070+ return StatusOr<std::unique_ptr<storage::AsyncWriterConnection>>(
1071+ resume2_error);
1072+ });
9711073 });
972- });
1074+ }
9731075
9741076 auto connection = MakeWriterConnectionBuffered (
975- mock_factory.AsStdFunction (), std::move (mock ), TestOptions ());
1077+ mock_factory.AsStdFunction (), std::move (mock1 ), TestOptions ());
9761078
9771079 auto close = connection->Close ({});
9781080 ASSERT_FALSE (close.is_ready ());
@@ -986,6 +1088,30 @@ TEST(WriteConnectionBuffered, CloseFailsAndResumeSucceedsAndFinalized) {
9861088 next.first .set_value (true );
9871089
9881090 EXPECT_STATUS_OK (close.get ());
1091+
1092+ // Write to the closed connection. Since HWM is 32KB, writing 16 bytes
1093+ // returns OK immediately because it is buffered.
1094+ auto w = connection->Write (TestPayload (16 ));
1095+ ASSERT_TRUE (w.is_ready ());
1096+ EXPECT_STATUS_OK (w.get ());
1097+
1098+ // However, the background write loop will call resumed_mock->Write.
1099+ // Let that background Write fail.
1100+ next = sequencer.PopFrontWithName ();
1101+ EXPECT_EQ (next.second , " Write2" );
1102+ next.first .set_value (true );
1103+
1104+ // This triggers a second Resume. If resuming_ was not reset in SetClosed,
1105+ // this would hang because Resume would return early and not call
1106+ // mock_factory.
1107+ next = sequencer.PopFrontWithName ();
1108+ EXPECT_EQ (next.second , " Resume2" );
1109+ next.first .set_value (true );
1110+
1111+ // A subsequent write should fail immediately with the second resume error.
1112+ auto w2 = connection->Write (TestPayload (16 ));
1113+ ASSERT_TRUE (w2.is_ready ());
1114+ EXPECT_THAT (w2.get (), StatusIs (resume2_error.code ()));
9891115}
9901116
9911117TEST (WriteConnectionBuffered, Query) {
@@ -1250,11 +1376,9 @@ TEST(WriteConnectionBuffered, MultipleConcurrentFlushesAreQueued) {
12501376 return sequencer.PushBack (" Query1" ).then (
12511377 [](auto ) { return make_status_or (static_cast <std::int64_t >(4096 )); });
12521378 });
1253- // The race causes Write() to be called twice for the remaining
1254- // 8k.
12551379 EXPECT_CALL (*mock, Write (expected_write_size (8192 )))
1256- .Times (2 )
1257- .WillRepeatedly ([&](auto ) {
1380+ .Times (1 )
1381+ .WillOnce ([&](auto ) {
12581382 return sequencer.PushBack (" Write" ).then (
12591383 [](auto ) { return Status{}; });
12601384 });
@@ -1285,10 +1409,7 @@ TEST(WriteConnectionBuffered, MultipleConcurrentFlushesAreQueued) {
12851409 // After the Query, the first Flush future should be completed.
12861410 EXPECT_STATUS_OK (f1.get ());
12871411
1288- // Satisfy the two racy Write calls for the remaining data.
1289- next = sequencer.PopFrontWithName ();
1290- EXPECT_EQ (next.second , " Write" );
1291- next.first .set_value (true );
1412+ // Satisfy the Write call for the remaining data.
12921413 next = sequencer.PopFrontWithName ();
12931414 EXPECT_EQ (next.second , " Write" );
12941415 next.first .set_value (true );
0 commit comments