Skip to content

feat(common): add StartWithCorkedInitialMetadata. - #16569

Merged
scotthart merged 4 commits into
googleapis:mainfrom
cjc25:start-with-corked-initial-metadata
Oct 16, 2026
Merged

scotthart merged 4 commits into
googleapis:mainfrom
cjc25:start-with-corked-initial-metadata

Conversation

@cjc25

@cjc25 cjc25 commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

StartWithCorkedInitialMetadata() allows callers that immediately send an initial Write() after starting a bidirectional stream to save a network send and a grpc::CompletionQueue::Next() call on stream startup.

When a stream is started after calling
grpc::ClientContext::set_initial_metadata_corked(true), it does not send the initial metadata until the first Write() call, coalescing it with the first message's network write. It also does not bounce a tag for starting the stream through the grpc::CompletionQueue. RPCs that can tolerate this behavior have fewer thread hops and less contention on grpc::CompletionQueue::Next().

Unfortunately there's no getter for initial metadata in the grpc::ClientContext (and anyway we don't plumb the ClientContext into google::cloud::internal::CompletionQueueImpl). So we have to implement this at a higher level where the ClientContext is available.

I personally will use this to optimize bidirectional streaming RPCs, so I put it in AsyncStreamingReadWriteRpc for the purposes of review. But analogous and mechanical changes could be done in
AsyncStreamingReadRpc and AsyncStreamingWriteRpc.

See b/573273671

`StartWithCorkedInitialMetadata()` allows callers that immediately send
an initial `Write()` after starting a bidirectional stream to save a
network send and a `grpc::CompletionQueue::Next()` call on stream
startup.

When a stream is started after calling
`grpc::ClientContext::set_initial_metadata_corked(true)`, it does not
send the initial metadata until the first `Write()` call, coalescing it
with the first message's network write. It also does not bounce a tag
for starting the stream through the CompletionQueue. RPCs that can
tolerate this behavior have fewer thread hops and less contention on
`grpc::CompletionQueue::Next()`.

Unfortunately there's no getter for initial metadata in the
`grpc::ClientContext` (and anyway we don't plumb the `ClientContext`
into `google::cloud::internal::CompletionQueueImpl`). So we have to
implement this at a higher level where the `ClientContext` is available.

I personally will use this to optimize bidirectional streaming RPCs, so
I put it in `AsyncStreamingReadWriteRpc` for the purposes of review. But
analogous and mechanical changes could be done in
`AsyncStreamingReadRpc` and `AsyncStreamingWriteRpc`.
@cjc25
cjc25 requested a review from a team as a code owner October 15, 2026 14:20

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the StartWithCorkedInitialMetadata() method to the AsyncStreamingReadWriteRpc interface and implements it across various stream decorators, wrappers, and mock classes. This allows starting a streaming RPC with corked initial metadata, delaying its transmission until the first write call. The review feedback suggests simplifying the template arguments in the tracing wrapper by using the base class name directly instead of decltype(impl_)::element_type, and explicitly typing the lambda parameter in the timeout wrapper instead of using auto to improve readability.

Comment on lines 45 to 53
future<bool> Start() override {
// It is sufficient to set `span_` as the parent of `start_span`, because
// the lower levels do not create any spans.
opentelemetry::trace::StartSpanOptions options;
options.parent = span_->GetContext();
auto start_span = internal::MakeSpan("Start", options);
return impl_->Start().then(
[this, ss = std::move(start_span)](future<bool> f) {
EndSpan(*ss);
auto started = f.get();
span_->SetAttribute("gl-cpp.stream_started", started);
started_ = started;
return started;
});
return StartInternal<&decltype(impl_)::element_type::Start>("Start");
}

future<bool> StartWithCorkedInitialMetadata() override {
return StartInternal<
&decltype(impl_)::element_type::StartWithCorkedInitialMetadata>(
"StartWithCorkedInitialMetadata");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using decltype(impl_)::element_type is unnecessarily verbose and complex. Since AsyncStreamingReadWriteRpcTracing inherits from AsyncStreamingReadWriteRpc<Request, Response>, you can refer to the base class type directly, which is much cleaner and more readable.

  future<bool> Start() override {
    return StartInternal<&AsyncStreamingReadWriteRpc<Request, Response>::Start>("Start");
  }

  future<bool> StartWithCorkedInitialMetadata() override {
    return StartInternal<
        &AsyncStreamingReadWriteRpc<Request, Response>::StartWithCorkedInitialMetadata>(
        "StartWithCorkedInitialMetadata");
  }
References
  1. Reject "Code Poetry": Dismantle complex abstractions used for simple tasks. Prefer simplicity over cleverness. (link)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree that it is "less verbose" to spell decltype(impl_)::element_type as AsyncStreamingReadWriteRpc<Request, Response> (30 chars -> 46 chars), but I agree that decltype is unnecessary and can confuse people. I've reworded to the base class name.

Comment on lines +116 to +125
future<bool> StartWithCorkedInitialMetadata() {
auto watchdog = CreateWatchdog(start_timeout);
return child->StartWithCorkedInitialMetadata().then(
[watchdog = std::move(watchdog), w = WeakFromThis()](auto f) mutable {
if (auto self = w.lock()) {
return self->OnStart(std::move(watchdog), f.get());
}
return make_ready_future(false);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using auto for the lambda parameter f here. Specifying the explicit type future<bool> is more readable, avoids creating a generic lambda unnecessarily, and is consistent with the other stream wrappers in this PR (such as the tracing and logging wrappers).

    future<bool> StartWithCorkedInitialMetadata() {
      auto watchdog = CreateWatchdog(start_timeout);
      return child->StartWithCorkedInitialMetadata().then(
          [watchdog = std::move(watchdog), w = WeakFromThis()](future<bool> f) mutable {
            if (auto self = w.lock()) {
              return self->OnStart(std::move(watchdog), f.get());
            }
            return make_ready_future(false);
          });
    }
References
  1. Reject Obscured Domain & Return Types: Flag and reject auto when it hides StatusOr, domain objects, protobuf messages/fields, or function return types. (link)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (auto f as the lambda parameter) is verbatim the approach used in all the other methods in this class, so IMO it would be more confusing to have just this one specify future<bool>.

@scotthart

Copy link
Copy Markdown
Member

/gcbrun

@cjc25

cjc25 commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

The zizmor findings are unrelated to this PR:

$ zizmor --gh-token=$(gh auth token) .github/workflows
 INFO zizmor: 🌈 zizmor v1.25.2
 INFO audit: zizmor: 🌈 completed .github/workflows/codeql-analysis.yml
 INFO audit: zizmor: 🌈 completed .github/workflows/external-account-integration.yml
 INFO audit: zizmor: 🌈 completed .github/workflows/macos-bazel.yml
 INFO audit: zizmor: 🌈 completed .github/workflows/macos-cmake.yml
 INFO audit: zizmor: 🌈 completed .github/workflows/test-runner.yml
 INFO audit: zizmor: 🌈 completed .github/workflows/update-compute-discovery.yml
 INFO audit: zizmor: 🌈 completed .github/workflows/windows-bazel.yml
 INFO audit: zizmor: 🌈 completed .github/workflows/windows-cmake.yml
warning[ref-version-mismatch]: action's hash pin has mismatched or missing version comment
  --> .github/workflows/codeql-analysis.yml:76:84
   |
76 |         uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
   |         ------------------------------------------------------------------------   ^^ points to commit 6f2ac81f5cd8
   |         |
   |         is pointed to by tag v4.38.2
   |
   = note: audit confidence → High
   = note: this finding has an auto-fix

warning[ref-version-mismatch]: action's hash pin has mismatched or missing version comment
  --> .github/workflows/codeql-analysis.yml:97:87
   |
97 |         uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
   |         ---------------------------------------------------------------------------   ^^ points to commit 6f2ac81f5cd8
   |         |
   |         is pointed to by tag v4.38.2
   |
   = note: audit confidence → High
   = note: this finding has an auto-fix

54 findings (6 ignored, 46 suppressed, 2 unsafe fixes): 0 informational, 0 low, 2 medium, 0 high

@cjc25

cjc25 commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

I'm fixing the tests now - there were many subclasses of AsyncStreamingReadWriteRpc that I missed, sorry.

@scotthart

Copy link
Copy Markdown
Member

/gcbrun

@codecov

codecov Bot commented Oct 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.95122% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.38%. Comparing base (c0e675a) to head (82dd1bf).

Files with missing lines Patch % Lines
...ogle/cloud/internal/async_read_write_stream_auth.h 93.33% 1 Missing ⚠️
...loud/internal/async_read_write_stream_impl_test.cc 95.45% 1 Missing ⚠️
...e/cloud/internal/async_read_write_stream_timeout.h 90.90% 1 Missing ⚠️
...e/cloud/internal/async_read_write_stream_tracing.h 94.73% 1 Missing ⚠️
google/cloud/pubsub/testing/mock_subscriber_stub.h 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #16569    +/-   ##
========================================
  Coverage   92.37%   92.38%            
========================================
  Files        2266     2266            
  Lines      219493   219636   +143     
========================================
+ Hits       202765   202902   +137     
- Misses      16728    16734     +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cjc25

cjc25 commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Another recent PR (#16571) makes unrelated changes and has the same asan-pr and xsan-pr failures as this one, so I think those failures aren't part of this PR. I can investigate further tomorrow though.

@scotthart

Copy link
Copy Markdown
Member

/gharun

@scotthart
scotthart merged commit 8a074dc into googleapis:main Oct 16, 2026
67 checks passed

This branch was successfully deployed

1 active deployment
false — 82dd1bf3 Deployed Oct 15, 2026 by cjc25 via Save PR ref #12473
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants