Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(common): add support for call-tree-specific options #7669

Merged
merged 2 commits into from
Dec 1, 2021

Conversation

devbww
Copy link
Contributor

@devbww devbww commented Nov 28, 2021

Provide access to the prevailing Options for an operation without having
to plumb function parameters through the various and sundry layers, like
connection.

This also means that helper libraries can determine things like whether a
tracing component is enabled, without having to distort their APIs.

For example, if all ServiceClient::Operation() calls are implemented like

class ServiceClient {
 public
  ServiceClient(std::shared_ptr<ServiceConnection> connection,
                Options options = {})
      : connection_(std::move(connection)),
        options_(ServiceDefaultOptions(std::move(options))) {}

  T Operation(..., Options options = {}) {
    internal::OptionsSpan span(internal::MergeOptions(options, options_));
    ...
  }

 private:
  std::shared_ptr<ServiceConnection> connection_;
  Options options_;
};

then the connection layer, or any other internal library, can retrieve the
prevailing options using internal::CurrentOptions(). If the operation is
asynchronous, the invocation-time Options are also installed during its
callbacks.


This change is Reviewable

Provide access to the prevailing `Options` for an operation without having
to plumb function parameters through the various and sundry layers, like
connection.

This also means that helper libraries can determine things like whether a
tracing component is enabled, without having to distort their APIs.

For example, if all `ServiceClient::Operation()` calls are implemented like
```
class ServiceClient {
 public
  ServiceClient(std::shared_ptr<ServiceConnection> connection,
                Options options = {})
      : connection_(std::move(connection)),
        options_(ServiceDefaultOptions(std::move(options))) {}

  T Operation(..., Options options = {}) {
    internal::OptionsSpan span(internal::MergeOptions(options, options_));
    ...
  }

 private:
  std::shared_ptr<ServiceConnection> connection_;
  Options options_;
};
```
then the connection layer, or any other internal library, can retrieve the
prevailing options using `internal::CurrentOptions()`.  If the operation is
asynchronous, the invocation-time `Options` are also installed during its
callbacks.
@google-cla google-cla bot added the cla: yes This human has signed the Contributor License Agreement. label Nov 28, 2021
@google-cloud-cpp-bot
Copy link
Collaborator

Google Cloud Build Logs
For commit: f7a5ca793c7624c2a8f6d9960b03e02397c94501

ℹ️ NOTE: Kokoro logs are linked from "Details" below.

@codecov
Copy link

codecov bot commented Nov 28, 2021

Codecov Report

Merging #7669 (7284576) into main (8d5c432) will decrease coverage by 0.00%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #7669      +/-   ##
==========================================
- Coverage   95.28%   95.28%   -0.01%     
==========================================
  Files        1254     1254              
  Lines      113312   113401      +89     
==========================================
+ Hits       107969   108052      +83     
- Misses       5343     5349       +6     
Impacted Files Coverage Δ
google/cloud/options.h 97.43% <ø> (ø)
google/cloud/internal/async_connection_ready.cc 88.23% <100.00%> (+0.73%) ⬆️
google/cloud/internal/async_read_stream_impl.h 98.71% <100.00%> (+0.06%) ⬆️
...ogle/cloud/internal/async_read_write_stream_impl.h 93.05% <100.00%> (+0.51%) ⬆️
google/cloud/internal/async_retry_loop_test.cc 99.65% <100.00%> (+0.06%) ⬆️
google/cloud/internal/async_rpc_details.h 94.44% <100.00%> (+0.32%) ⬆️
...le/cloud/internal/default_completion_queue_impl.cc 97.15% <100.00%> (+0.15%) ⬆️
google/cloud/options.cc 100.00% <100.00%> (ø)
google/cloud/options_test.cc 100.00% <100.00%> (ø)
...e/cloud/testing_util/fake_completion_queue_impl.cc 94.04% <100.00%> (+0.22%) ⬆️
... and 9 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8d5c432...7284576. Read the comment docs.

@devbww devbww marked this pull request as ready for review November 28, 2021 06:58
@devbww devbww requested a review from a team as a code owner November 28, 2021 06:58
Copy link
Contributor

@coryan coryan left a comment

Choose a reason for hiding this comment

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

A suggestion for a different implementation which might be (maybe) a more targeted change and easier to maintain as we add asynchronous operations.

@@ -150,10 +151,12 @@ class AsyncReadStreamImpl
private:
void Cancel() override {} // LCOV_EXCL_LINE
bool Notify(bool ok) override {
OptionsSpan span(options_);
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should set these spans in the completion queue instead. The Start() function is a good place to capture the current options:

void DefaultCompletionQueueImpl::StartOperation(
std::unique_lock<std::mutex> lk, std::shared_ptr<AsyncGrpcOperation> op,
absl::FunctionRef<void(void*)> start) {
void* tag = op.get();

and the notifications would be a good place to create the span:

auto op = FindOperation(tag);
++notify_counter_;
if (op->Notify(ok)) {
ForgetOperation(tag);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered that, but thought that changing the tag map (which is really just a collection of AsyncGrpcOperations) to a map of AsyncGrpcOperation/Options pairs was both a little more complicated, and belied the fact that the Options logically belonged to the operation.

It is certainly the case that the behavior is now distributed to the AsyncGrpcOperation sub-classes, but I've been thinking of that as a feature. For example, the existence of the internal::CompletionQueueImpl interface implies that this behavior should not belong in a particular instantiation (the DefaultCompletionQueueImpl) of that interface.

Perhaps experience with this will lead to a better understanding, but for now I'd vote for leaving it as is.

Copy link
Contributor

Choose a reason for hiding this comment

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

For example, the existence of the internal::CompletionQueueImpl interface implies that this behavior should not belong in a particular instantiation (the DefaultCompletionQueueImpl) of that interface.

FWIW, the only reason for the interface is mocking 🤷

Perhaps experience with this will lead to a better understanding, but for now I'd vote for leaving it as is.

SGTM.

// The prevailing options for the current operation. Thread local, so
// additional propagation must be done whenever work for the operation
// is done in another thread.
thread_local Options current_options;
Copy link
Contributor

Choose a reason for hiding this comment

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

This obviously has a non-trivial destructor. I am certain you know better than I do whether this is accepted by the Google Style Guide. Just asking to maybe learn something.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The non-trivial destructor for the namespace-scoped object is not a problem because the object is only accessible to its thread, and is not destroyed until the thread terminates. So, there is no issue with destruction order, and indeed the Style Guide says as much.

What the Style Guide still does require for namespace-scoped objects, thread local or not, is a compile-time-constant initializer. In the thread-local case I guess this is to guard against threads being created before global initialization is complete. The workaround is the same in both cases ... to convert the namespace-scoped object to a function local, so that it is initialized on the first call, no matter when that occurs. I've done that. PTAL.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ack, thanks for looking into it.

Copy link
Contributor

@devjgm devjgm left a comment

Choose a reason for hiding this comment

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

Looks like a nice solution that's hidden from our users but will make our implementation plumbing much easier. Thanks! Just a couple small questions. The PR LGTM, but I'll let Carlos do the real review.

~OptionsSpan();

private:
Options opts_;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: If I understand this correctly, opts_ is actually the previous options that will be restored after the span goes out of scope. That is, opts_ is not the same as opts, which was passed to the c'tor. If you think this distinction is worth capturing, consider renaming this data member to something like restore_ or old_opts_ or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had considered that, but concluded that the meaning of the member changes with time. For the majority of its life it is indeed the "options to restore", but after the initializer list is complete it is the "options to install" (i.e., it is the opts passed to the constructor), and after restoration it is a "moved from options". All in all, it seemed better to give it a generic name, and let the code specify its meaning at any point.

*
* @param opts the `Options` to install.
*/
class ABSL_MUST_USE_RESULT OptionsSpan {
Copy link
Contributor

Choose a reason for hiding this comment

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

Q: Is ABSL_MUST_USE_RESULT needed here, or nice to have, or... what's this here for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is there to try to call out usages like OptionsSpan(opts); where you forget the variable name, and the temporary span dies at the semicolon. For example, see https://github.com/abseil/abseil-cpp/blob/master/absl/base/attributes.h#L410-L414. (Previously we might have used a macro trick to do something similar.)

That said, when I tried to test that, it didn't seem to trigger a warning. But I'm not sure that wasn't just an artifact of which compiler+version I used, so I decided to leave it anyway. But, happy to remove also.

@google-cloud-cpp-bot
Copy link
Collaborator

Google Cloud Build Logs
For commit: 7284576d0c7c6223f8d7495a561ae5efae59e896

ℹ️ NOTE: Kokoro logs are linked from "Details" below.

@devbww devbww merged commit 823c121 into googleapis:main Dec 1, 2021
@devbww devbww deleted the options-span branch December 1, 2021 21:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes This human has signed the Contributor License Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants