Skip to content

Commit a82ff3b

Browse files
authored
fix(storage): avoid premature flush completion and concurrent writes in AsyncWriterConnectionBuffered (#16169)
1 parent 9079480 commit a82ff3b

2 files changed

Lines changed: 164 additions & 25 deletions

File tree

google/cloud/storage/internal/async/writer_connection_buffered.cc

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,19 @@ class AsyncWriterConnectionBufferedState
350350
}
351351
// If the buffer is small enough, collect all the handlers to notify them.
352352
auto const handlers = ClearHandlersIfEmpty(lk);
353+
if (is_resume) {
354+
// We are resuming. The pending flush promises (if any) should not be
355+
// satisfied yet, because we haven't actually flushed the data on the new
356+
// connection. The `WriteLoop` will trigger a flush (potentially empty)
357+
// if `flush_` is still true, which will satisfy the promises when it
358+
// completes. However, we still need to notify any handlers waiting for
359+
// the buffer to shrink, and we need to restart the write loop.
360+
resuming_ = false;
361+
lk.unlock();
362+
for (auto const& h : handlers) h->Execute(Status{});
363+
WriteLoop(std::unique_lock<std::mutex>(mu_));
364+
return;
365+
}
353366
// SetFlushed will release the lock before returning.
354367
SetFlushed(std::move(lk), Status{});
355368
// Re-acquire the lock to re-enter the write loop.
@@ -388,6 +401,9 @@ class AsyncWriterConnectionBufferedState
388401
if (!s.ok() && cancelled_) {
389402
return SetError(std::move(lk), std::move(s));
390403
}
404+
// Guard against concurrent resume attempts.
405+
if (resuming_) return;
406+
resuming_ = true;
391407
}
392408
// Pass the original status `s`, `was_finalizing`, and `was_closing` to the
393409
// callback.
@@ -463,6 +479,7 @@ class AsyncWriterConnectionBufferedState
463479
finalize_ = false;
464480
finalizing_ = false; // Reset finalizing flag
465481
flush_ = false;
482+
resuming_ = false; // Reset resuming flag
466483
// Check if the promise has already been completed.
467484
if (finalized_promise_completed_) {
468485
// Since the lock is passed by value, no explicit unlock is needed.
@@ -487,6 +504,7 @@ class AsyncWriterConnectionBufferedState
487504
close_ = false;
488505
closing_ = false;
489506
flush_ = false;
507+
resuming_ = false;
490508
// Check if the promise has already been completed.
491509
if (closed_promise_completed_) {
492510
return;
@@ -526,10 +544,6 @@ class AsyncWriterConnectionBufferedState
526544
// lock.
527545
for (auto& h : handlers) h->Execute(Status{});
528546
flushed.set_value(result);
529-
// Restart the write loop ONLY if we are not already finalizing.
530-
// If finalizing_ is true, the completion will be handled by OnFinalize.
531-
std::unique_lock<std::mutex> loop_lk(mu_);
532-
if (!finalizing_) WriteLoop(std::move(loop_lk));
533547
}
534548

535549
void SetError(std::unique_lock<std::mutex> lk, Status const& status) {
@@ -540,6 +554,7 @@ class AsyncWriterConnectionBufferedState
540554
close_ = false;
541555
closing_ = false; // Reset closing flag
542556
flush_ = false;
557+
resuming_ = false; // Reset resuming flag
543558

544559
// Always clear handlers and pending flushes on error.
545560
auto handlers = ClearHandlers(lk);
@@ -685,6 +700,9 @@ class AsyncWriterConnectionBufferedState
685700

686701
// Tracks if the final promise (`finalized_`) has been completed.
687702
bool finalized_promise_completed_ = false;
703+
704+
// True if the resume loop is running. Prevents re-entry.
705+
bool resuming_ = false;
688706
};
689707

690708
/**

google/cloud/storage/internal/async/writer_connection_buffered_test.cc

Lines changed: 142 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
774852
TEST(WriteConnectionBuffered, CloseEmpty) {
775853
AsyncSequencer<bool> sequencer;
776854
auto mock = std::make_unique<MockAsyncWriterConnection>();
@@ -950,29 +1028,53 @@ TEST(WriteConnectionBuffered, CloseFailsAndResumeSucceedsButNotClosed) {
9501028
TEST(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

9911117
TEST(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

Comments
 (0)