Skip to content

Commit acf90c3

Browse files
authored
fix(storage): do not retry permanent errors in async writer resume (#16340)
AsyncWriterConnectionResumed and AsyncWriterConnectionBuffered previously unconditionally invoked Resume() when stream operations returned an error, even if the error was a permanent failure such as FAILED_PRECONDITION. For appendable uploads (BidiWriteObject), issuing a new BidiWriteObject RPC on Resume() caused the client to request writer exclusivity again, resulting in concurrent writers endlessly stealing exclusivity back and forth from each other on FAILED_PRECONDITION errors. This change checks if the error status is a permanent failure (via AsyncRetryPolicy / AsyncStatusTraits) before attempting Resume(). If permanent, the upload immediately terminates with the error status.
1 parent ca575c5 commit acf90c3

4 files changed

Lines changed: 216 additions & 20 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
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
#include "google/cloud/storage/internal/async/writer_connection_buffered.h"
16+
#include "google/cloud/storage/async/retry_policy.h"
1617
#include "google/cloud/storage/internal/async/write_payload_impl.h"
1718
#include "google/cloud/future.h"
1819
#include "google/cloud/internal/make_status.h"
@@ -60,14 +61,25 @@ Status MakeFastForwardError(absl::string_view upload_id,
6061
.WithMetadata("gcloud-cpp.storage.persisted_size", returned));
6162
}
6263

64+
bool IsPermanentFailure(Options const& options, Status const& status) {
65+
if (options.has<storage::AsyncRetryPolicyOption>() &&
66+
options.get<storage::AsyncRetryPolicyOption>() != nullptr) {
67+
return options.get<storage::AsyncRetryPolicyOption>()->IsPermanentFailure(
68+
status);
69+
}
70+
return storage::internal::AsyncStatusTraits::IsPermanentFailure(status);
71+
}
72+
6373
class AsyncWriterConnectionBufferedState
6474
: public std::enable_shared_from_this<AsyncWriterConnectionBufferedState> {
6575
public:
6676
AsyncWriterConnectionBufferedState(
6777
WriterConnectionFactory factory,
6878
std::unique_ptr<storage::AsyncWriterConnection> impl,
69-
std::size_t buffer_size_lwm, std::size_t buffer_size_hwm)
79+
Options const& options, std::size_t buffer_size_lwm,
80+
std::size_t buffer_size_hwm)
7081
: factory_(std::move(factory)),
82+
options_(internal::MakeImmutableOptions(options)),
7183
buffer_size_lwm_(buffer_size_lwm),
7284
buffer_size_hwm_(buffer_size_hwm),
7385
impl_(std::move(impl)) {
@@ -422,6 +434,9 @@ class AsyncWriterConnectionBufferedState
422434
if (!s.ok() && cancelled_) {
423435
return SetError(std::move(lk), std::move(s));
424436
}
437+
if (!s.ok() && IsPermanentFailure(*options_, s)) {
438+
return SetError(std::move(lk), std::move(s));
439+
}
425440
// Guard against concurrent resume attempts.
426441
if (resuming_) return;
427442
resuming_ = true;
@@ -630,6 +645,8 @@ class AsyncWriterConnectionBufferedState
630645
// Creates new `impl_` instances when needed.
631646
WriterConnectionFactory const factory_;
632647

648+
google::cloud::internal::ImmutableOptions options_;
649+
633650
// Request a server-side flush if the buffer goes over this threshold.
634651
std::size_t const buffer_size_lwm_;
635652

@@ -780,9 +797,10 @@ class AsyncWriterConnectionBuffered : public storage::AsyncWriterConnection {
780797
explicit AsyncWriterConnectionBuffered(
781798
WriterConnectionFactory factory,
782799
std::unique_ptr<storage::AsyncWriterConnection> impl,
783-
std::size_t buffer_size_lwm, std::size_t buffer_size_hwm)
800+
Options const& options, std::size_t buffer_size_lwm,
801+
std::size_t buffer_size_hwm)
784802
: state_(std::make_shared<AsyncWriterConnectionBufferedState>(
785-
std::move(factory), std::move(impl), buffer_size_lwm,
803+
std::move(factory), std::move(impl), options, buffer_size_lwm,
786804
buffer_size_hwm)) {}
787805

788806
void Cancel() override { return state_->Cancel(); }
@@ -833,7 +851,7 @@ std::unique_ptr<storage::AsyncWriterConnection> MakeWriterConnectionBuffered(
833851
std::unique_ptr<storage::AsyncWriterConnection> impl,
834852
Options const& options) {
835853
return absl::make_unique<AsyncWriterConnectionBuffered>(
836-
std::move(factory), std::move(impl),
854+
std::move(factory), std::move(impl), options,
837855
options.get<storage::BufferedUploadLwmOption>(),
838856
options.get<storage::BufferedUploadHwmOption>());
839857
}

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

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include "google/cloud/storage/internal/async/writer_connection_buffered.h"
1616
#include "google/cloud/storage/async/connection.h"
17+
#include "google/cloud/storage/async/retry_policy.h"
1718
#include "google/cloud/storage/mocks/mock_async_writer_connection.h"
1819
#include "google/cloud/storage/testing/canonical_errors.h"
1920
#include "google/cloud/testing_util/async_sequencer.h"
@@ -707,7 +708,7 @@ TEST(WriteConnectionBuffered, FlushWithEmptyPayload) {
707708

708709
TEST(WriteConnectionBuffered, ErrorFailsPendingFlushes) {
709710
AsyncSequencer<bool> sequencer;
710-
auto flush_error = PermanentError();
711+
auto flush_error = TransientError();
711712
auto resume_error = Status(StatusCode::kAborted, "resume loop failed");
712713

713714
auto mock = std::make_unique<MockAsyncWriterConnection>();
@@ -1681,6 +1682,85 @@ TEST(WriteConnectionBuffered, DuplicateFinalizeFails) {
16811682
EXPECT_THAT(finalize2.get(), StatusIs(StatusCode::kFailedPrecondition));
16821683
}
16831684

1685+
TEST(WriteConnectionBuffered, PermanentErrorNoResume) {
1686+
AsyncSequencer<bool> sequencer;
1687+
auto failed_precondition =
1688+
Status(StatusCode::kFailedPrecondition, "precondition failed");
1689+
1690+
auto mock = std::make_unique<MockAsyncWriterConnection>();
1691+
EXPECT_CALL(*mock, UploadId).WillRepeatedly(Return("test-upload-id"));
1692+
EXPECT_CALL(*mock, PersistedState)
1693+
.WillRepeatedly(Return(MakePersistedState(0)));
1694+
EXPECT_CALL(*mock, Write).WillOnce([&](auto) {
1695+
return sequencer.PushBack("Write").then(
1696+
[failed_precondition](auto) { return failed_precondition; });
1697+
});
1698+
1699+
MockFactory mock_factory;
1700+
// factory_ should NOT be called because error is permanent
1701+
// (FAILED_PRECONDITION).
1702+
EXPECT_CALL(mock_factory, Call).Times(0);
1703+
1704+
auto connection = MakeWriterConnectionBuffered(
1705+
mock_factory.AsStdFunction(), std::move(mock), TestOptions());
1706+
1707+
auto write1 = connection->Write(TestPayload(1));
1708+
EXPECT_STATUS_OK(write1.get());
1709+
1710+
auto next = sequencer.PopFrontWithName();
1711+
EXPECT_THAT(next.second, Eq("Write"));
1712+
next.first.set_value(true);
1713+
1714+
auto write2 = connection->Write(TestPayload(1));
1715+
EXPECT_THAT(write2.get(), StatusIs(StatusCode::kFailedPrecondition));
1716+
}
1717+
1718+
TEST(WriteConnectionBuffered, CustomRetryPolicyOption) {
1719+
struct CustomAsyncRetryPolicy : public storage::AsyncRetryPolicy {
1720+
std::unique_ptr<storage::AsyncRetryPolicy> clone() const override {
1721+
return std::make_unique<CustomAsyncRetryPolicy>();
1722+
}
1723+
bool OnFailure(Status const&) override { return false; }
1724+
bool IsExhausted() const override { return false; }
1725+
bool IsPermanentFailure(Status const& s) const override {
1726+
return s.code() == StatusCode::kInvalidArgument;
1727+
}
1728+
};
1729+
1730+
AsyncSequencer<bool> sequencer;
1731+
auto invalid_argument =
1732+
Status(StatusCode::kInvalidArgument, "custom permanent error");
1733+
1734+
auto mock = std::make_unique<MockAsyncWriterConnection>();
1735+
EXPECT_CALL(*mock, UploadId).WillRepeatedly(Return("test-upload-id"));
1736+
EXPECT_CALL(*mock, PersistedState)
1737+
.WillRepeatedly(Return(MakePersistedState(0)));
1738+
EXPECT_CALL(*mock, Write).WillOnce([&](auto) {
1739+
return sequencer.PushBack("Write").then(
1740+
[invalid_argument](auto) { return invalid_argument; });
1741+
});
1742+
1743+
MockFactory mock_factory;
1744+
// factory_ should NOT be called because error is permanent per custom policy.
1745+
EXPECT_CALL(mock_factory, Call).Times(0);
1746+
1747+
auto options = TestOptions().set<storage::AsyncRetryPolicyOption>(
1748+
std::make_shared<CustomAsyncRetryPolicy>());
1749+
1750+
auto connection = MakeWriterConnectionBuffered(mock_factory.AsStdFunction(),
1751+
std::move(mock), options);
1752+
1753+
auto write1 = connection->Write(TestPayload(1));
1754+
EXPECT_STATUS_OK(write1.get());
1755+
1756+
auto next = sequencer.PopFrontWithName();
1757+
EXPECT_THAT(next.second, Eq("Write"));
1758+
next.first.set_value(true);
1759+
1760+
auto write2 = connection->Write(TestPayload(1));
1761+
EXPECT_THAT(write2.get(), StatusIs(StatusCode::kInvalidArgument));
1762+
}
1763+
16841764
} // namespace
16851765
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
16861766
} // namespace storage_internal

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

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
#include "google/cloud/storage/internal/async/writer_connection_resumed.h"
16+
#include "google/cloud/storage/async/retry_policy.h"
1617
#include "google/cloud/storage/internal/async/write_payload_impl.h"
1718
#include "google/cloud/storage/internal/async/writer_connection_impl.h"
1819
#include "google/cloud/future.h"
@@ -57,6 +58,15 @@ Status MakeFastForwardError(std::int64_t resend_offset,
5758
.WithMetadata("gcloud-cpp.storage.persisted_size", returned));
5859
}
5960

61+
bool IsPermanentFailure(Options const& options, Status const& status) {
62+
if (options.has<storage::AsyncRetryPolicyOption>() &&
63+
options.get<storage::AsyncRetryPolicyOption>() != nullptr) {
64+
return options.get<storage::AsyncRetryPolicyOption>()->IsPermanentFailure(
65+
status);
66+
}
67+
return storage::internal::AsyncStatusTraits::IsPermanentFailure(status);
68+
}
69+
6070
class AsyncWriterConnectionResumedState
6171
: public std::enable_shared_from_this<AsyncWriterConnectionResumedState> {
6272
public:
@@ -72,12 +82,12 @@ class AsyncWriterConnectionResumedState
7282
impl_(std::move(impl)),
7383
initial_request_(std::move(initial_request)),
7484
hash_function_(std::move(hash_function)),
85+
options_(internal::MakeImmutableOptions(options)),
7586
first_response_(std::move(first_response)),
7687
buffer_size_lwm_(buffer_size_lwm),
7788
buffer_size_hwm_(buffer_size_hwm) {
7889
finalized_future_ = finalized_.get_future();
7990
closed_future_ = closed_.get_future();
80-
options_ = internal::MakeImmutableOptions(options);
8191
auto state = impl_->PersistedState();
8292
if (absl::holds_alternative<google::storage::v2::Object>(state)) {
8393
buffer_offset_ = absl::get<google::storage::v2::Object>(state).size();
@@ -419,6 +429,24 @@ class AsyncWriterConnectionResumedState
419429
}
420430

421431
void Resume(Status const& s) {
432+
// Capture the finalization and close state *before* starting the async
433+
// resume.
434+
bool was_finalizing;
435+
bool was_closing;
436+
{
437+
std::unique_lock<std::mutex> lk(mu_);
438+
if (state_ == State::kResuming) return;
439+
was_finalizing = finalizing_;
440+
was_closing = closing_;
441+
if (!s.ok() && cancelled_) {
442+
return SetError(std::move(lk), std::move(s));
443+
}
444+
if (!s.ok() && IsPermanentFailure(*options_, s)) {
445+
return SetError(std::move(lk), std::move(s));
446+
}
447+
state_ = State::kResuming;
448+
}
449+
422450
auto proto_status = ExtractGrpcStatus(s);
423451
auto request = google::storage::v2::BidiWriteObjectRequest{};
424452
auto& append_object_spec = *request.mutable_append_object_spec();
@@ -439,20 +467,6 @@ class AsyncWriterConnectionResumedState
439467
append_object_spec.set_generation(first_response_.resource().generation());
440468
ApplyWriteRedirectErrors(append_object_spec, std::move(proto_status));
441469

442-
// Capture the finalization and close state *before* starting the async
443-
// resume.
444-
bool was_finalizing;
445-
bool was_closing;
446-
{
447-
std::unique_lock<std::mutex> lk(mu_);
448-
if (state_ == State::kResuming) return;
449-
was_finalizing = finalizing_;
450-
was_closing = closing_;
451-
if (!s.ok() && cancelled_) {
452-
return SetError(std::move(lk), std::move(s));
453-
}
454-
state_ = State::kResuming;
455-
}
456470
// Pass the original status `s`, `was_finalizing`, and `was_closing` to the
457471
// callback.
458472
factory_(std::move(request))

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "google/cloud/storage/internal/async/writer_connection_resumed.h"
1515
#include "google/cloud/mocks/mock_async_streaming_read_write_rpc.h"
1616
#include "google/cloud/storage/async/connection.h"
17+
#include "google/cloud/storage/async/retry_policy.h"
1718
#include "google/cloud/storage/internal/grpc/ctype_cord_workaround.h"
1819
#include "google/cloud/storage/mocks/mock_async_writer_connection.h"
1920
#include "google/cloud/storage/testing/canonical_errors.h"
@@ -1234,6 +1235,89 @@ TEST(WriterConnectionResumed, DuplicateFinalizeFails) {
12341235
EXPECT_THAT(finalize2.get(), StatusIs(StatusCode::kFailedPrecondition));
12351236
}
12361237

1238+
TEST(WriteConnectionResumed, PermanentErrorNoResume) {
1239+
AsyncSequencer<bool> sequencer;
1240+
auto initial_request = google::storage::v2::BidiWriteObjectRequest{};
1241+
auto first_response = google::storage::v2::BidiWriteObjectResponse{};
1242+
auto failed_precondition = Status(StatusCode::kFailedPrecondition,
1243+
"another writer became exclusive");
1244+
1245+
auto mock = std::make_unique<MockAsyncWriterConnection>();
1246+
EXPECT_CALL(*mock, UploadId).WillRepeatedly(Return("test-upload-id"));
1247+
EXPECT_CALL(*mock, PersistedState)
1248+
.WillRepeatedly(Return(MakePersistedState(0)));
1249+
EXPECT_CALL(*mock, Flush).WillOnce([&](auto) {
1250+
return sequencer.PushBack("Flush").then(
1251+
[failed_precondition](auto) { return failed_precondition; });
1252+
});
1253+
1254+
MockFactory mock_factory;
1255+
// factory_ should NOT be called because error is permanent
1256+
// (FAILED_PRECONDITION).
1257+
EXPECT_CALL(mock_factory, Call).Times(0);
1258+
1259+
auto connection = MakeWriterConnectionResumed(
1260+
mock_factory.AsStdFunction(), std::move(mock), initial_request, nullptr,
1261+
first_response, Options{});
1262+
1263+
auto write = connection->Write(TestPayload(10));
1264+
ASSERT_FALSE(write.is_ready());
1265+
1266+
auto next = sequencer.PopFrontWithName();
1267+
EXPECT_THAT(next.second, Eq("Flush"));
1268+
next.first.set_value(true);
1269+
1270+
EXPECT_THAT(write.get(), StatusIs(StatusCode::kFailedPrecondition));
1271+
}
1272+
1273+
TEST(WriteConnectionResumed, CustomRetryPolicyOption) {
1274+
struct CustomAsyncRetryPolicy : public storage::AsyncRetryPolicy {
1275+
std::unique_ptr<storage::AsyncRetryPolicy> clone() const override {
1276+
return std::make_unique<CustomAsyncRetryPolicy>();
1277+
}
1278+
bool OnFailure(Status const&) override { return false; }
1279+
bool IsExhausted() const override { return false; }
1280+
bool IsPermanentFailure(Status const& s) const override {
1281+
return s.code() == StatusCode::kInvalidArgument;
1282+
}
1283+
};
1284+
1285+
AsyncSequencer<bool> sequencer;
1286+
auto initial_request = google::storage::v2::BidiWriteObjectRequest{};
1287+
auto first_response = google::storage::v2::BidiWriteObjectResponse{};
1288+
auto invalid_argument =
1289+
Status(StatusCode::kInvalidArgument, "custom permanent error");
1290+
1291+
auto mock = std::make_unique<MockAsyncWriterConnection>();
1292+
EXPECT_CALL(*mock, UploadId).WillRepeatedly(Return("test-upload-id"));
1293+
EXPECT_CALL(*mock, PersistedState)
1294+
.WillRepeatedly(Return(MakePersistedState(0)));
1295+
EXPECT_CALL(*mock, Flush).WillOnce([&](auto) {
1296+
return sequencer.PushBack("Flush").then(
1297+
[invalid_argument](auto) { return invalid_argument; });
1298+
});
1299+
1300+
MockFactory mock_factory;
1301+
// factory_ should NOT be called because error is permanent per custom policy.
1302+
EXPECT_CALL(mock_factory, Call).Times(0);
1303+
1304+
auto options = Options{}.set<storage::AsyncRetryPolicyOption>(
1305+
std::make_shared<CustomAsyncRetryPolicy>());
1306+
1307+
auto connection = MakeWriterConnectionResumed(
1308+
mock_factory.AsStdFunction(), std::move(mock), initial_request, nullptr,
1309+
first_response, options);
1310+
1311+
auto write = connection->Write(TestPayload(10));
1312+
ASSERT_FALSE(write.is_ready());
1313+
1314+
auto next = sequencer.PopFrontWithName();
1315+
EXPECT_THAT(next.second, Eq("Flush"));
1316+
next.first.set_value(true);
1317+
1318+
EXPECT_THAT(write.get(), StatusIs(StatusCode::kInvalidArgument));
1319+
}
1320+
12371321
} // namespace
12381322
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
12391323
} // namespace storage_internal

0 commit comments

Comments
 (0)