Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new retry logging mechanism (retry_logging.h and retry_logging.cc) to provide diagnostics for silently retried transient errors in GCS operations, integrating it into ReadObject, UploadChunk, and RetryObjectReadSource. Feedback on the changes highlights style guide violations, specifically recommending the use of std::string_view instead of absl::string_view and avoiding auto for primitive types.
v-pratap
force-pushed
the
retry-logging-sync
branch
from
September 21, 2026 05:01
0958b42 to
f154e51
Compare
v-pratap
marked this pull request as ready for review
September 21, 2026 05:07
When GCS returns a transient error the client backs off and retries with
no output at all. From the application's point of view a read just takes
a long time. There is nothing to grep for, so an operator cannot tell a
slow network from a server that is shedding load, and the only way to
see the retries is to enable the `rawclient` tracing component and pay
for a full dump of every request and response.
This adds one WARNING record per retried attempt on the synchronous
paths, which cover both the JSON/REST and the gRPC transports (GCS gRPC
sync goes through `RestRetryLoop`, because `GenericStub` takes a
`RestContext&` regardless of transport).
[gcs-retry] ReadObject/open: transient error on attempt 1,
resource=my-bucket/my-object, status=UNAVAILABLE: ...
Call sites:
- `ConnectionImpl::ReadObject`, the retry lambda passed to
`RestRetryLoop` -- an attempt failed to open the stream.
- `RetryObjectReadSource::Read` -- the stream broke mid-download and is
being resumed from the last received byte.
- `ConnectionImpl::UploadChunk` -- an attempt to upload a chunk of a
resumable upload failed.
The severity is WARNING and the record is not gated behind a tracing
component. `StdClogBackend`, the default backend, drops everything below
FATAL, so this is invisible to a default OSS build; it becomes visible
to anyone who installs a `LogBackend`. A log that has to be turned on
first is not useful during the first occurrence of an incident, which is
exactly when it is needed.
Two details worth calling out:
- The status is flattened to a single line. GCS error bodies end with a
newline, which would otherwise push the backend's `(file:line)` suffix
onto a second line and make the record unusable with `grep`. It also
removes a log-injection hazard, since the message is service-supplied.
- `UploadChunkRequest` only exposes the resumable session URL, and the
`upload_id` in it is a bearer capability: anyone holding it can append
to or finalize the upload for the lifetime of the session. Only the
bucket and an 8-character prefix of the id are logged, which is enough
to tell concurrent uploads apart.
Also makes `LoggingStub::ReadObject` report the status on failure. It
cannot use `LogWrapper` because `unique_ptr<ObjectReadSource>` has no
`operator<<`, so it previously logged nothing about why the call failed.
Verified against the storage-testbench using its fault-injection API, in
addition to the unit tests.
v-pratap
force-pushed
the
retry-logging-sync
branch
from
September 21, 2026 05:31
f154e51 to
1b80cff
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #16467 +/- ##
==========================================
+ Coverage 92.34% 92.35% +0.01%
==========================================
Files 2246 2248 +2
Lines 214568 214806 +238
==========================================
+ Hits 198136 198385 +249
+ Misses 16432 16421 -11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When GCS returns a transient error (429, 503, 500,
DEADLINE_EXCEEDED) theclient backs off and retries with no output at all. From the application's
point of view a read just takes a long time.
There is nothing to grep for, so an operator cannot tell a slow network from a
server that is shedding load. The only way to see the retries today is to enable
the
rawclienttracing component and pay for a full dump of every request andresponse — which you can only do after you already suspect retries, i.e. not
during the first occurrence of an incident.
This came out of a real incident where reads on a large fleet appeared to stall
and the client produced nothing to correlate against.
What this does
One
WARNINGrecord per retried attempt, nothing on success:This PR covers the synchronous paths, which serve both the JSON/REST and the
gRPC transports — GCS gRPC sync also goes through
RestRetryLoop, becauseGenericStubtakes aRestContext®ardless of transport.whereConnectionImpl::ReadObject(retry lambda)ReadObject/openRetryObjectReadSource::ReadReadObject/resumeConnectionImpl::UploadChunkUploadChunkNew helper
google/cloud/storage/internal/retry_logging.{h,cc}(
IsTransientError,LogTransientRetry,RetryLogResource,RetryLogUploadResource) so the call sites stay one line each.Also makes
LoggingStub::ReadObjectreport the status on failure. It cannot useLogWrapperbecauseunique_ptr<ObjectReadSource>has nooperator<<, so itpreviously logged nothing about why the call failed.
Design notes
Why WARNING, and why not behind a tracing component
LogSink::minimum_severity_defaults toTRACE, but the defaultStdClogBackenddrops everything belowFATAL(log.cc#L228-L231).
So this is invisible in a default OSS build and becomes visible to anyone
who installs their own
LogBackend. A diagnostic that has to be turned on firstis not useful during the first occurrence of an incident, which is exactly when
it is needed.
No changes to
google/cloud/internal/**orlog.cc; everything is insidegoogle/cloud/storage/**.The status is flattened to one line
GCS error bodies end with a newline. Streaming the
Statusstraight intoGCP_LOGcarried that newline through, pushing the backend's(file:line)suffix onto a second line — so
grep '\[gcs-retry\]'returned a truncatedrecord, defeating the point of the greppable prefix.
It is also a small log-injection hazard, since the message is
service-supplied.
OneLine()flattens\n,\r,\t.Pinned by
RetryLogging.StatusIsFlattenedToASingleLine.The upload session URL is redacted
UploadChunkRequestonly exposesupload_session_url(), and theupload_idinit is a bearer capability — anyone holding it can append to or finalize that
upload with no other credential, for the lifetime of the session (~7 days).
The request's own
operator<<does print it, but that is behind the opt-inrawclientcomponent, whereas this record is always on and lands in logaggregators that are usually readable by more people than the bucket itself.
RetryLogUploadResource()keeps the bucket and an 8-character prefix of the id— enough to tell concurrent uploads apart, not enough to hijack one.
Attempt numbers at sites that run before the retry policy
ReadObject/openruns inside the retry lambda, i.e. before the retry policy isconsulted, so it also reports the final give-up attempt. That attempt is real and
did fail; it just was not followed by another try. Noted in a comment at the call
site.
ReadObject/resumeandUploadChunkare exact.The backoff delay is deliberately not logged: reading it requires
backoff_policy.OnCompletion(), which advances the policy.Testbench verification
End-to-end against
storage-testbench
v0.61.0using its fault-injection API (
POST /retry_test, id passed asx-retry-test-id), withGOOGLE_CLOUD_CPP_ENABLE_CLOG=yes.ReadObject, open failsstorage.objects.get: 503 ×2ReadObject/openReadObject, stream breaksreturn-broken-stream-after-256KReadObject/resumestorage.objects.insert: return-503-after-256KUploadChunkThe trailing
(retry_logging.cc:87)is added by the library's own log backend(
operator<<(std::ostream&, LogRecord const&)inlog.cc), not by this change.Note
The testbench run found two defects that the unit tests did not: the newline
split and the
upload_iddisclosure described above. Both are fixed here andre-verified.
Tests
bazel test //google/cloud/storage:all→ 175 / 175 PASSED on this branchalone.
New coverage in
retry_logging_test.cc,connection_impl_object_test.cc,retry_object_read_source_test.cc,logging_stub_test.cc— including negativetests asserting nothing is logged on success or on a permanent error.
Follow-ups
UploadChunk. ARetryLoggingStubGenericStubdecorator installed ingeneric_stub_factory.ccwould cover every unary RPC from a single place;worth doing separately rather than growing this PR.
429 storm could produce a lot of lines. Would you prefer attempt 1 and then
powers of two? There is no
LOG_FIRST_Nequivalent in the library today.Internal ref: b/547926601