Skip to content

Commit 443e254

Browse files
authored
feat(storage): expose initial read ranges in AsyncClient (#16341)
1 parent abc95d5 commit 443e254

4 files changed

Lines changed: 164 additions & 0 deletions

File tree

google/cloud/storage/async/client.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "google/cloud/storage/internal/async/connection_impl.h"
1717
#include "google/cloud/storage/internal/async/connection_tracing.h"
1818
#include "google/cloud/storage/internal/async/default_options.h"
19+
#include "google/cloud/storage/internal/async/options.h"
1920
#include "google/cloud/storage/internal/grpc/stub.h"
2021
#include "google/cloud/grpc_options.h"
2122
#include <memory>
@@ -84,6 +85,27 @@ future<StatusOr<ObjectDescriptor>> AsyncClient::Open(
8485
});
8586
}
8687

88+
future<StatusOr<ObjectDescriptor>> AsyncClient::Open(
89+
BucketName const& bucket_name, std::string object_name,
90+
InitialReadRanges const& config, Options opts) {
91+
auto spec = google::storage::v2::BidiReadObjectSpec{};
92+
spec.set_bucket(bucket_name.FullName());
93+
spec.set_object(std::move(object_name));
94+
95+
// Convert the user-facing `InitialReadRanges` to the internal
96+
// `ReadRangesOption` so it can be propagated down to the connection
97+
// implementation.
98+
if (!config.initial_ranges.empty()) {
99+
std::vector<storage_internal::ReadRangeConfig> internal_ranges;
100+
internal_ranges.reserve(config.initial_ranges.size());
101+
for (auto const& r : config.initial_ranges) {
102+
internal_ranges.push_back({r.offset, r.length});
103+
}
104+
opts.set<storage_internal::ReadRangesOption>(std::move(internal_ranges));
105+
}
106+
return Open(std::move(spec), std::move(opts));
107+
}
108+
87109
future<StatusOr<std::pair<AsyncReader, AsyncToken>>> AsyncClient::ReadObject(
88110
BucketName const& bucket_name, std::string object_name, Options opts) {
89111
auto request = google::storage::v2::ReadObjectRequest{};

google/cloud/storage/async/client.h

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,27 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
8383
*/
8484
class AsyncClient {
8585
public:
86+
/**
87+
* Specifies a byte range for a read request.
88+
*/
89+
struct ByteRange {
90+
std::int64_t offset = 0;
91+
std::int64_t length = 0;
92+
};
93+
94+
/**
95+
* Specifies initial byte ranges to request concurrently when opening an
96+
* object.
97+
*
98+
* Passing initial read ranges allows the client to begin fetching expected
99+
* byte ranges during connection setup, which may improve first-byte retrieval
100+
* times for known access patterns.
101+
*/
102+
struct InitialReadRanges {
103+
// The initial read ranges.
104+
std::vector<ByteRange> initial_ranges;
105+
};
106+
86107
/// Create a new client configured with @p options.
87108
explicit AsyncClient(Options options = {});
88109
/// Create a new client using @p connection. This is often used for mocking.
@@ -273,6 +294,29 @@ class AsyncClient {
273294
std::string object_name,
274295
Options opts = {});
275296

297+
/**
298+
* Open an object descriptor, requesting specified initial read ranges
299+
* concurrently.
300+
*
301+
* @par Example
302+
* @snippet storage_async_samples.cc open-object-initial-read-ranges
303+
*
304+
* @par Idempotency
305+
* This is a read-only operation and is always idempotent. The operation will
306+
* retry until the descriptor is successfully created. The descriptor itself
307+
* will resume any incomplete ranged reads if the connection(s) are
308+
* interrupted. Use `ResumePolicyOption` and `ResumePolicy` to control this.
309+
*
310+
* @param bucket_name the name of the bucket that contains the object.
311+
* @param object_name the name of the object to be read.
312+
* @param config initial byte ranges to request during connection setup.
313+
* @param opts options controlling the behavior of this RPC.
314+
*/
315+
future<StatusOr<ObjectDescriptor>> Open(BucketName const& bucket_name,
316+
std::string object_name,
317+
InitialReadRanges const& config,
318+
Options opts = {});
319+
276320
/**
277321
* Open an object descriptor to perform one or more ranged reads.
278322
*

google/cloud/storage/async/client_test.cc

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

1515
#include "google/cloud/storage/async/client.h"
16+
#include "google/cloud/storage/internal/async/options.h"
1617
#include "google/cloud/storage/mocks/mock_async_connection.h"
1718
#include "google/cloud/storage/mocks/mock_async_object_descriptor_connection.h"
1819
#include "google/cloud/storage/mocks/mock_async_reader_connection.h"
@@ -290,6 +291,43 @@ TEST(AsyncClient, Open) {
290291
"empty response", [](auto const& p) { return p.size(); }, 0)));
291292
}
292293

294+
TEST(AsyncClient, OpenWithInitialReadRanges) {
295+
auto constexpr kExpectedRequest = R"pb(
296+
bucket: "projects/_/buckets/test-bucket"
297+
object: "test-object"
298+
)pb";
299+
auto mock = std::make_shared<MockAsyncConnection>();
300+
EXPECT_CALL(*mock, options).WillRepeatedly(Return(Options{}));
301+
302+
EXPECT_CALL(*mock, Open).WillOnce([&](AsyncConnection::OpenParams const& p) {
303+
EXPECT_TRUE(p.options.has<storage_internal::ReadRangesOption>());
304+
auto const& ranges = p.options.get<storage_internal::ReadRangesOption>();
305+
EXPECT_EQ(ranges.size(), 2);
306+
if (ranges.size() >= 2) {
307+
EXPECT_EQ(ranges[0].offset, 0);
308+
EXPECT_EQ(ranges[0].length, 100);
309+
EXPECT_EQ(ranges[1].offset, 1000);
310+
EXPECT_EQ(ranges[1].length, 200);
311+
}
312+
313+
auto expected = google::storage::v2::BidiReadObjectSpec{};
314+
EXPECT_TRUE(TextFormat::ParseFromString(kExpectedRequest, &expected));
315+
EXPECT_THAT(p.read_spec, IsProtoEqual(expected));
316+
317+
auto descriptor = std::make_shared<MockAsyncObjectDescriptorConnection>();
318+
return make_ready_future(make_status_or(
319+
std::shared_ptr<ObjectDescriptorConnection>(std::move(descriptor))));
320+
});
321+
322+
auto client = AsyncClient(mock);
323+
AsyncClient::InitialReadRanges config;
324+
config.initial_ranges = {{0, 100}, {1000, 200}};
325+
auto descriptor =
326+
client.Open(BucketName("test-bucket"), "test-object", std::move(config))
327+
.get();
328+
ASSERT_STATUS_OK(descriptor);
329+
}
330+
293331
TEST(AsyncClient, OpenWithInvalidBucket) {
294332
auto constexpr kExpectedRequest = R"pb(
295333
bucket: "test-only-invalid"

google/cloud/storage/examples/storage_async_samples.cc

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,55 @@ void OpenObjectMultipleRangedRead(google::cloud::storage::AsyncClient& client,
246246
std::cout << "The ranges contain " << count << " newlines\n";
247247
}
248248

249+
void OpenObjectWithInitialReadRanges(
250+
google::cloud::storage::AsyncClient& client,
251+
std::vector<std::string> const& argv) {
252+
//! [open-object-initial-read-ranges]
253+
// [START storage_open_object_initial_read_ranges]
254+
namespace gcs = google::cloud::storage;
255+
256+
// Helper coroutine to count newlines returned by an AsyncReader.
257+
auto count_newlines =
258+
[](gcs::AsyncReader reader,
259+
gcs::AsyncToken token) -> google::cloud::future<std::uint64_t> {
260+
std::uint64_t count = 0;
261+
while (token.valid()) {
262+
auto [payload, t] = (co_await reader.Read(std::move(token))).value();
263+
token = std::move(t);
264+
for (auto const& buffer : payload.contents()) {
265+
count += std::count(buffer.begin(), buffer.end(), '\n');
266+
}
267+
}
268+
co_return count;
269+
};
270+
271+
auto coro =
272+
[&count_newlines](
273+
gcs::AsyncClient& client, std::string bucket_name,
274+
std::string object_name) -> google::cloud::future<std::uint64_t> {
275+
gcs::AsyncClient::InitialReadRanges config;
276+
config.initial_ranges = {{0, 1024}, {1024, 1024}};
277+
278+
auto descriptor =
279+
(co_await client.Open(gcs::BucketName(std::move(bucket_name)),
280+
std::move(object_name), std::move(config)))
281+
.value();
282+
283+
auto [r1, t1] = descriptor.Read(0, 1024);
284+
auto [r2, t2] = descriptor.Read(1024, 1024);
285+
286+
auto c1 = count_newlines(std::move(r1), std::move(t1));
287+
auto c2 = count_newlines(std::move(r2), std::move(t2));
288+
co_return (co_await std::move(c1)) + (co_await std::move(c2));
289+
};
290+
// [END storage_open_object_initial_read_ranges]
291+
//! [open-object-initial-read-ranges]
292+
// The example is easier to test and run if we call the coroutine and block
293+
// until it completes.
294+
auto const count = coro(client, argv.at(0), argv.at(1)).get();
295+
std::cout << "The pre-warmed ranges contain " << count << " newlines\n";
296+
}
297+
249298
void OpenObjectReadFullObject(google::cloud::storage::AsyncClient& client,
250299
std::vector<std::string> const& argv) {
251300
//! [open-object-read-full-object]
@@ -997,6 +1046,11 @@ void OpenObjectMultipleRangedRead(google::cloud::storage::AsyncClient&,
9971046
std::cerr << "AsyncClient::Open() example requires coroutines\n";
9981047
}
9991048

1049+
void OpenObjectWithInitialReadRanges(google::cloud::storage::AsyncClient&,
1050+
std::vector<std::string> const&) {
1051+
std::cerr << "AsyncClient::Open() example requires coroutines\n";
1052+
}
1053+
10001054
void OpenMultipleObjectsRangedRead(google::cloud::storage::AsyncClient&,
10011055
std::vector<std::string> const&) {
10021056
std::cerr << "AsyncClient::Open() example requires coroutines\n";
@@ -1306,6 +1360,10 @@ void AutoRun(std::vector<std::string> const& argv) {
13061360
<< std::endl;
13071361
OpenObjectMultipleRangedRead(client, {bucket_name, composed_name});
13081362

1363+
std::cout << "Running the OpenObjectWithInitialReadRanges() example"
1364+
<< std::endl;
1365+
OpenObjectWithInitialReadRanges(client, {bucket_name, composed_name});
1366+
13091367
std::cout << "Running the OpenMultipleObjectsRangedRead() example"
13101368
<< std::endl;
13111369
auto const multi_read_o1 =
@@ -1563,6 +1621,8 @@ int main(int argc, char* argv[]) try {
15631621
OpenObjectSingleRangedRead),
15641622
make_entry("open-object-multiple-ranged-read", {},
15651623
OpenObjectMultipleRangedRead),
1624+
make_entry("open-object-initial-read-ranges", {},
1625+
OpenObjectWithInitialReadRanges),
15661626
make_entry("open-object-read-full-object", {}, OpenObjectReadFullObject),
15671627
make_entry("open-multiple-objects-ranged-read",
15681628
{"<object-name-1>", "<object-name-2>", "<object-name-3>"},

0 commit comments

Comments
 (0)