Repository navigation
feat(storage): add adaptive bounded range reads for open-ended downloads - #16586
rajeevpodar wants to merge 1 commit into
Conversation
Add the experimental `storage_experimental::AdaptiveReadRangeOption`. When it is set, a `ReadObject()` download that does not bound its range (the full object, or `ReadFromOffset()`) is requested as a series of growing, bounded windows (1 MiB, doubling up to 64 MiB) instead of a single unbounded GET. Applications that read the beginning of an object and then close the stream leave at most one small window outstanding, so the service sends far fewer bytes that are then discarded, and the connection can be reused instead of torn down. - `AdaptiveObjectReadSource` is a new `ObjectReadSource` decorator. Each window is a regular, retried download of a `ReadRange()`, so a retry resumes inside the window. The next window is opened on a background thread once a read reaches `prefetch_threshold` (default 50%) of the current window. - Every window pins the generation returned by the first window. `ObjectReadStream::size()` and the headers describe the full download, and full-object CRC32C/MD5 validation covers all the windows. - If the first window shows that the service ignored the range (decompressive transcoding, or a JSON API response without `content-range`), the download switches to a regular unbounded read, at the cost of one extra request. - `ReadRange()` and `ReadLast()` downloads are unchanged. The windows are not hedged. - With the JSON API, window requests add `gccl-feat/adaptive-read` to the `x-goog-api-client` header. - `GOOGLE_CLOUD_CPP_STORAGE_ENABLE_ADAPTIVE_READ_RANGE` (`true` or `false`) turns the feature on or off without code changes, for applications where a library creates the `Client`.
There was a problem hiding this comment.
Code Review
This pull request implements adaptive bounded range reads for Google Cloud Storage, allowing open-ended downloads to be fetched in growing, bounded windows. This feature is configurable via AdaptiveReadRangeOption and the GOOGLE_CLOUD_CPP_STORAGE_ENABLE_ADAPTIVE_READ_RANGE environment variable. The feedback primarily addresses style guide violations, specifically recommending the use of std::string_view over absl::string_view and requesting explicit type declarations instead of auto where domain types, StatusOr, or function return types are obscured.
| #include "google/cloud/internal/make_status.h" | ||
| #include "absl/strings/numbers.h" | ||
| #include "absl/strings/str_cat.h" | ||
| #include "absl/strings/string_view.h" |
There was a problem hiding this comment.
The repository style guide prefers std::string_view over absl::string_view. Please include <string_view> instead of "absl/strings/string_view.h".
#include <string_view>References
- Use std::string_view instead of absl::string_view. (link)
| // The size is `*` when the service does not know it, `SimpleAtoi()` rejects | ||
| // that value. | ||
| std::int64_t size = 0; | ||
| if (!absl::SimpleAtoi(absl::string_view(f->second).substr(pos + 1), &size)) { |
There was a problem hiding this comment.
The repository style guide prefers std::string_view over absl::string_view. Please use std::string_view here.
if (!absl::SimpleAtoi(std::string_view(f->second).substr(pos + 1), &size)) {References
- Use std::string_view instead of absl::string_view. (link)
| std::int64_t limit) { | ||
| std::vector<char> buffer(static_cast<std::size_t>( | ||
| std::clamp<std::int64_t>(limit, 1, kMaxDiscardBuffer))); | ||
| auto metadata = EmptyResult(); |
There was a problem hiding this comment.
The repository style guide disallows using auto when it hides domain objects or function return types. Please explicitly type metadata as ReadSourceResult.
ReadSourceResult metadata = EmptyResult();References
- Reject auto when it hides StatusOr, domain objects, or function return types. (link)
| // All the data in this window has been delivered. Reading the end of the | ||
| // response completes the request, so the connection can be reused. Errors | ||
| // here cannot lose any data, and are ignored. | ||
| auto metadata = Discard(*source, 0); |
There was a problem hiding this comment.
The repository style guide disallows using auto when it hides StatusOr<T>, domain objects, or function return types. Please explicitly type metadata as StatusOr<ReadSourceResult>.
StatusOr<ReadSourceResult> metadata = Discard(*source, 0);References
- Reject auto when it hides StatusOr, domain objects, or function return types. (link)
| auto const nominal = NextAdaptiveWindowSize(window_nominal_, settings_); | ||
| auto const window = MakeWindow(offset_, nominal); | ||
| StatusOr<std::unique_ptr<ObjectReadSource>> source = [&] { | ||
| auto prefetched = TakePrefetch(window); |
There was a problem hiding this comment.
The repository style guide disallows using auto when it hides StatusOr<T>, domain objects, or function return types. Please explicitly type prefetched as std::optional<StatusOr<std::unique_ptr<ObjectReadSource>>>.
std::optional<StatusOr<std::unique_ptr<ObjectReadSource>>> prefetched = TakePrefetch(window);References
- Reject auto when it hides StatusOr, domain objects, or function return types. (link)
| AdaptiveObjectReadSource::Executor executor, AdaptiveReadSettings settings, | ||
| std::int64_t offset, std::optional<std::int64_t> generation) { | ||
| AdaptiveObjectReadSource::Window first{ | ||
| offset, |
There was a problem hiding this comment.
The repository style guide disallows using auto when it hides StatusOr<T>, domain objects, or function return types. Please explicitly type source as StatusOr<std::unique_ptr<ObjectReadSource>>.
StatusOr<std::unique_ptr<ObjectReadSource>> source = (*factory)(first);References
- Reject auto when it hides StatusOr, domain objects, or function return types. (link)
| @@ -406,9 +409,20 @@ StatusOr<ObjectMetadata> StorageConnectionImpl::GetObjectMetadata( | |||
| StatusOr<std::unique_ptr<ObjectReadSource>> StorageConnectionImpl::ReadObject( | |||
| ReadObjectRangeRequest const& request) { | |||
| auto current = google::cloud::internal::SaveCurrentOptions(); | |||
There was a problem hiding this comment.
The repository style guide disallows using auto when it hides domain objects or function return types. Please explicitly type current as google::cloud::internal::ImmutableOptions.
google::cloud::internal::ImmutableOptions current = google::cloud::internal::SaveCurrentOptions();References
- Reject auto when it hides StatusOr, domain objects, or function return types. (link)
| auto const settings = MakeAdaptiveReadSettings( | ||
| current->get<storage_experimental::AdaptiveReadRangeOption>()); |
There was a problem hiding this comment.
The repository style guide disallows using auto when it hides domain objects or function return types. Please explicitly type settings as AdaptiveReadSettings const.
AdaptiveReadSettings const settings = MakeAdaptiveReadSettings(current->get<storage_experimental::AdaptiveReadRangeOption>());References
- Reject auto when it hides StatusOr, domain objects, or function return types. (link)
| auto const adaptive_reads = | ||
| GetEnv("GOOGLE_CLOUD_CPP_STORAGE_ENABLE_ADAPTIVE_READ_RANGE"); |
There was a problem hiding this comment.
The repository style guide disallows using auto when it hides std::optional<T>, domain objects, or function return types. Please explicitly type adaptive_reads as std::optional<std::string> const.
std::optional<std::string> const adaptive_reads = GetEnv("GOOGLE_CLOUD_CPP_STORAGE_ENABLE_ADAPTIVE_READ_RANGE");References
- Reject auto when it hides StatusOr, domain objects, or function return types. (link)
| request.bucket_name(), "/o/", | ||
| UrlEncode(request.object_name())), | ||
| AdaptiveReadApiClientHeader(options)); | ||
| auto headers = AddHeaders(options, builder); |
There was a problem hiding this comment.
The repository style guide disallows using auto when it hides StatusOr<T>, domain objects, or function return types. Please explicitly type headers as Status.
Status headers = AddHeaders(options, builder);References
- Reject auto when it hides StatusOr, domain objects, or function return types. (link)
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #16586 +/- ##
==========================================
+ Coverage 92.38% 92.44% +0.05%
==========================================
Files 2266 2270 +4
Lines 219732 221543 +1811
==========================================
+ Hits 203009 204798 +1789
- Misses 16723 16745 +22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
When an application reads a whole object, or from an offset to the end (
ReadFromOffset(N)), the client sends one unbounded GET. Many readers (Parquet, ArrayRecord, Riegeli, ...) only need the start of the download and then close the stream. Closing early aborts the transfer: the connection is torn down, and every byte the service already sent is discarded.This PR adds the experimental
storage_experimental::AdaptiveReadRangeOption. With the option set, open-ended downloads are requested as a series of growing, bounded windows instead of one unbounded GET:Changes
AdaptiveObjectReadSource(internal/adaptive_object_read_source.{h,cc}): a newObjectReadSourcedecorator. Each window is a regular, retried download of aReadRange(), so a retry resumes inside the window. The next window opens on a background thread once a read reachesprefetch_threshold(default 50%) of the current window.ObjectReadStream::size()and the headers describe the full download, and full-object CRC32C/MD5 validation covers all the windows.content-range), the download switches to a regular unbounded read, at the cost of one extra request.ReadRange()andReadLast()downloads are unchanged. Windows are not hedged.gccl-feat/adaptive-readtox-goog-api-client.GOOGLE_CLOUD_CPP_STORAGE_ENABLE_ADAPTIVE_READ_RANGE=true|falseturns the feature on, or off as a kill switch, without code changes. This is for applications where a library creates theClient.Trade-off: a full sequential read takes more requests, 22 for a 1 GiB object with the defaults.
Usage
Testing
internal/adaptive_object_read_source_test.cc(58) andinternal/connection_impl_adaptive_read_test.cc(12), plus new cases ininternal/rest/stub_test.cc,client_test.ccandinternal/grpc/default_options_test.cc. All pass locally (CMake, GCC 15). The two new suites also passed 30 shuffled repeats.tests/object_read_range_integration_test.ccandtests/decompressive_transcoding_integration_test.cc. They compile, but I haven't run them locally (no testbench), so I'm relying on CI.Before this is ready for review
Close()at 1 MiB, whateverinitial_windowis.IfMetagenerationMatch/IfMetagenerationNotMatchafter the first window. The pinned generation already keeps the data consistent.CallContextto prefetches, so their spans are children of theReadObjectspan.Follow-ups in separate PRs: gRPC attribution (needs a generator change) and
AsyncClient::ReadObject.