Skip to content

Commit 01cb07e

Browse files
authored
Do not throw exceptions in WriteObject(). (#2400)
We were still throwing an exception in WriteObject() for some types of errors. This fixes that problem and adds integration tests to verify we do not throw exceptions in other media operations (downloads, uploads, etc.)
1 parent 9f154a3 commit 01cb07e

6 files changed

Lines changed: 115 additions & 54 deletions

File tree

google/cloud/storage/client.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ ObjectReadStream Client::ReadObjectImpl(
5959
return ObjectReadStream(*std::move(streambuf));
6060
}
6161

62+
ObjectWriteStream Client::WriteObjectImpl(
63+
internal::InsertObjectStreamingRequest const& request) {
64+
auto streambuf = raw_client_->WriteObject(request);
65+
if (!streambuf) {
66+
ObjectWriteStream error_stream(google::cloud::internal::make_unique<
67+
internal::ObjectWriteErrorStreambuf>(
68+
std::move(streambuf).status()));
69+
error_stream.setstate(std::ios::badbit | std::ios::eofbit);
70+
error_stream.Close();
71+
return error_stream;
72+
}
73+
return ObjectWriteStream(*std::move(streambuf));
74+
}
75+
6276
bool Client::UseSimpleUpload(std::string const& file_name) const {
6377
auto status = google::cloud::internal::status(file_name);
6478
if (!is_regular(status)) {

google/cloud/storage/client.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,7 @@ class Client {
892892
Options&&... options) {
893893
internal::ReadObjectRangeRequest request(bucket_name, object_name);
894894
request.set_multiple_options(std::forward<Options>(options)...);
895-
return ReadObjectImpl(std::move(request));
895+
return ReadObjectImpl(request);
896896
}
897897

898898
/**
@@ -958,7 +958,7 @@ class Client {
958958
Options&&... options) {
959959
internal::InsertObjectStreamingRequest request(bucket_name, object_name);
960960
request.set_multiple_options(std::forward<Options>(options)...);
961-
return ObjectWriteStream(raw_client_->WriteObject(request).value());
961+
return WriteObjectImpl(request);
962962
}
963963

964964
/**
@@ -2786,6 +2786,9 @@ class Client {
27862786
ObjectReadStream ReadObjectImpl(
27872787
internal::ReadObjectRangeRequest const& request);
27882788

2789+
ObjectWriteStream WriteObjectImpl(
2790+
internal::InsertObjectStreamingRequest const& request);
2791+
27892792
// The version of UploadFile() where UseResumableUploadSession is one of the
27902793
// options. Note how this does not use InsertObjectMedia at all.
27912794
template <typename... Options>

google/cloud/storage/client_write_object_test.cc

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ TEST_F(WriteObjectTest, WriteObject) {
8383
EXPECT_CALL(*mock_result, DoClose())
8484
.WillRepeatedly(Return(internal::HttpResponse{200, text, {}}));
8585
EXPECT_CALL(*mock_result, IsOpen()).WillRepeatedly(Return(true));
86+
EXPECT_CALL(*mock_result, ValidateHash(_))
87+
.WillRepeatedly(Return(true));
8688
std::unique_ptr<internal::ObjectWriteStreambuf> result(mock_result);
8789
return make_status_or(std::move(result));
8890
}));
@@ -102,54 +104,29 @@ TEST_F(WriteObjectTest, WriteObjectTooManyFailures) {
102104
return StatusOr<std::unique_ptr<internal::ObjectWriteStreambuf>>(
103105
TransientError());
104106
};
105-
#if GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
106107
EXPECT_CALL(*mock, WriteObject(_))
107108
.WillOnce(Invoke(returner))
108109
.WillOnce(Invoke(returner))
109110
.WillOnce(Invoke(returner));
110-
EXPECT_THROW(
111-
try {
112-
client.WriteObject("test-bucket-name", "test-object-name").Close();
113-
} catch (std::runtime_error const& ex) {
114-
EXPECT_THAT(ex.what(), HasSubstr("Retry policy exhausted"));
115-
EXPECT_THAT(ex.what(), HasSubstr("WriteObject"));
116-
throw;
117-
},
118-
std::runtime_error);
119-
#else
120-
// With EXPECT_DEATH*() the mocking framework cannot detect how many times the
121-
// operation is called.
122-
EXPECT_CALL(*mock, WriteObject(_)).WillRepeatedly(Invoke(returner));
123-
EXPECT_DEATH_IF_SUPPORTED(
124-
client.WriteObject("test-bucket-name", "test-object-name").Close(),
125-
"exceptions are disabled");
126-
#endif // GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
111+
112+
auto stream = client.WriteObject("test-bucket-name", "test-object-name");
113+
EXPECT_TRUE(stream.bad());
114+
EXPECT_FALSE(stream.metadata().status().ok());
115+
EXPECT_EQ(TransientError().code(), stream.metadata().status().code())
116+
<< ", status=" << stream.metadata().status();
127117
}
128118

129119
TEST_F(WriteObjectTest, WriteObjectPermanentFailure) {
130120
auto returner = [](internal::InsertObjectStreamingRequest const&) {
131121
return StatusOr<std::unique_ptr<internal::ObjectWriteStreambuf>>(
132122
PermanentError());
133123
};
134-
#if GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
135124
EXPECT_CALL(*mock, WriteObject(_)).WillOnce(Invoke(returner));
136-
EXPECT_THROW(
137-
try {
138-
client->WriteObject("test-bucket-name", "test-object-name").Close();
139-
} catch (std::runtime_error const& ex) {
140-
EXPECT_THAT(ex.what(), HasSubstr("Permanent error"));
141-
EXPECT_THAT(ex.what(), HasSubstr("WriteObject"));
142-
throw;
143-
},
144-
std::runtime_error);
145-
#else
146-
// With EXPECT_DEATH*() the mocking framework cannot detect how many times the
147-
// operation is called.
148-
EXPECT_CALL(*mock, WriteObject(_)).WillRepeatedly(Invoke(returner));
149-
EXPECT_DEATH_IF_SUPPORTED(
150-
client->WriteObject("test-bucket-name", "test-object-name").Close(),
151-
"exceptions are disabled");
152-
#endif // GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
125+
auto stream = client->WriteObject("test-bucket-name", "test-object-name");
126+
EXPECT_TRUE(stream.bad());
127+
EXPECT_FALSE(stream.metadata().status().ok());
128+
EXPECT_EQ(PermanentError().code(), stream.metadata().status().code())
129+
<< ", status=" << stream.metadata().status();
153130
}
154131

155132
} // namespace

google/cloud/storage/internal/curl_client_test.cc

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,6 @@ TEST_P(CurlClientTest, InsertObjectMediaSimple) {
195195
}
196196

197197
TEST_P(CurlClientTest, InsertObjectMediaMultipart) {
198-
std::string const error_type = GetParam();
199-
if (error_type != "credentials-failure") {
200-
// TODO(#1735) - enable this test when ObjectWriteStream uses StatusOr.
201-
return;
202-
}
203198
auto actual = client_
204199
->InsertObjectMedia(
205200
InsertObjectMediaRequest("bkt", "obj", "contents"))

google/cloud/storage/tests/object_media_integration_test.cc

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ TEST_F(ObjectMediaIntegrationTest, ReadRangeXml) {
692692
EXPECT_STATUS_OK(status);
693693
}
694694

695-
TEST_F(ObjectMediaIntegrationTest, ConnectionFailureJSON) {
695+
TEST_F(ObjectMediaIntegrationTest, ConnectionFailureReadJSON) {
696696
Client client{ClientOptions(oauth2::CreateAnonymousCredentials())
697697
.set_endpoint("http://localhost:0"),
698698
LimitedErrorCountRetryPolicy(2)};
@@ -713,7 +713,7 @@ TEST_F(ObjectMediaIntegrationTest, ConnectionFailureJSON) {
713713
<< ", status=" << stream.status();
714714
}
715715

716-
TEST_F(ObjectMediaIntegrationTest, ConnectionFailureXML) {
716+
TEST_F(ObjectMediaIntegrationTest, ConnectionFailureReadXML) {
717717
google::cloud::internal::SetEnv("CLOUD_STORAGE_TESTBENCH_ENDPOINT",
718718
"http://localhost:0");
719719
Client client{ClientOptions(oauth2::CreateAnonymousCredentials())
@@ -732,6 +732,81 @@ TEST_F(ObjectMediaIntegrationTest, ConnectionFailureXML) {
732732
<< ", status=" << stream.status();
733733
}
734734

735+
TEST_F(ObjectMediaIntegrationTest, ConnectionFailureWriteJSON) {
736+
Client client{ClientOptions(oauth2::CreateAnonymousCredentials())
737+
.set_endpoint("http://localhost:0"),
738+
LimitedErrorCountRetryPolicy(2)};
739+
740+
std::string bucket_name = flag_bucket_name;
741+
auto object_name = MakeRandomObjectName();
742+
743+
// We force the library to use the JSON API by adding the
744+
// `IfGenerationNotMatch()` parameter, both JSON and XML use the same code to
745+
// download, but controlling the endpoint for JSON is easier.
746+
auto stream = client.WriteObject(
747+
bucket_name, object_name, IfGenerationMatch(0), IfGenerationNotMatch(7));
748+
EXPECT_TRUE(stream.bad());
749+
EXPECT_FALSE(stream.metadata().status().ok());
750+
EXPECT_EQ(StatusCode::kUnavailable, stream.metadata().status().code())
751+
<< ", status=" << stream.metadata().status();
752+
}
753+
754+
TEST_F(ObjectMediaIntegrationTest, ConnectionFailureWriteXML) {
755+
google::cloud::internal::SetEnv("CLOUD_STORAGE_TESTBENCH_ENDPOINT",
756+
"http://localhost:0");
757+
Client client{ClientOptions(oauth2::CreateAnonymousCredentials())
758+
.set_endpoint("http://localhost:0"),
759+
LimitedErrorCountRetryPolicy(2)};
760+
761+
std::string bucket_name = flag_bucket_name;
762+
auto object_name = MakeRandomObjectName();
763+
764+
auto stream = client.WriteObject(
765+
bucket_name, object_name, IfGenerationMatch(0), IfGenerationNotMatch(7));
766+
EXPECT_TRUE(stream.bad());
767+
EXPECT_FALSE(stream.metadata().status().ok());
768+
EXPECT_EQ(StatusCode::kUnavailable, stream.metadata().status().code())
769+
<< ", status=" << stream.metadata().status();
770+
}
771+
772+
TEST_F(ObjectMediaIntegrationTest, ConnectionFailureDownloadFile) {
773+
google::cloud::internal::SetEnv("CLOUD_STORAGE_TESTBENCH_ENDPOINT",
774+
"http://localhost:0");
775+
Client client{ClientOptions(oauth2::CreateAnonymousCredentials())
776+
.set_endpoint("http://localhost:0"),
777+
LimitedErrorCountRetryPolicy(2)};
778+
779+
std::string bucket_name = flag_bucket_name;
780+
auto object_name = MakeRandomObjectName();
781+
auto file_name = MakeRandomObjectName();
782+
783+
Status status = client.DownloadToFile(bucket_name, object_name, file_name);
784+
EXPECT_FALSE(status.ok());
785+
EXPECT_EQ(StatusCode::kUnavailable, status.code()) << ", status=" << status;
786+
}
787+
788+
TEST_F(ObjectMediaIntegrationTest, ConnectionFailureUploadFile) {
789+
google::cloud::internal::SetEnv("CLOUD_STORAGE_TESTBENCH_ENDPOINT",
790+
"http://localhost:0");
791+
Client client{ClientOptions(oauth2::CreateAnonymousCredentials())
792+
.set_endpoint("http://localhost:0"),
793+
LimitedErrorCountRetryPolicy(2)};
794+
795+
std::string bucket_name = flag_bucket_name;
796+
auto object_name = MakeRandomObjectName();
797+
auto file_name = MakeRandomObjectName();
798+
799+
std::ofstream(file_name) << LoremIpsum();
800+
801+
StatusOr<ObjectMetadata> meta =
802+
client.UploadFile(file_name, bucket_name, object_name);
803+
EXPECT_FALSE(meta.ok()) << "value=" << meta.value();
804+
EXPECT_EQ(StatusCode::kUnavailable, meta.status().code())
805+
<< ", status=" << meta.status();
806+
807+
EXPECT_EQ(0, std::remove(file_name.c_str()));
808+
}
809+
735810
} // anonymous namespace
736811
} // namespace STORAGE_CLIENT_NS
737812
} // namespace storage

google/cloud/storage/tests/object_resumable_write_integration_test.cc

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,12 @@ TEST_F(ObjectResumableWriteIntegrationTest, WriteWithContentTypeFailure) {
8181
std::ostringstream expected;
8282

8383
// Create the object, but only if it does not exist already.
84-
TestPermanentFailure([&] {
85-
auto os = client->WriteObject(
86-
bucket_name, object_name, IfGenerationMatch(0),
87-
WithObjectMetadata(ObjectMetadata().set_content_type("text/plain")));
88-
os.exceptions(std::ios_base::failbit);
89-
os << LoremIpsum();
90-
os.Close();
91-
ObjectMetadata meta = os.metadata().value();
92-
});
84+
auto os = client->WriteObject(
85+
bucket_name, object_name, IfGenerationMatch(0),
86+
WithObjectMetadata(ObjectMetadata().set_content_type("text/plain")));
87+
EXPECT_TRUE(os.bad());
88+
EXPECT_FALSE(os.metadata().status().ok())
89+
<< ", status=" << os.metadata().status();
9390
}
9491

9592
TEST_F(ObjectResumableWriteIntegrationTest, WriteWithUseResumable) {

0 commit comments

Comments
 (0)