Skip to content

Conversation

daniel-sanche
Copy link
Contributor

This PR adds a new grpc interceptor to the client, to be used for metrics collection. Currently, the interceptor is a no-op. Interception logic will be added in a future change.

@daniel-sanche daniel-sanche requested review from a team as code owners September 9, 2025 20:57
@product-auto-label product-auto-label bot added the size: l Pull request size is large. label Sep 9, 2025
@product-auto-label product-auto-label bot added the api: bigtable Issues related to the googleapis/python-bigtable API. label Sep 9, 2025
@daniel-sanche daniel-sanche added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 30, 2025
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 30, 2025
@daniel-sanche daniel-sanche added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Oct 1, 2025
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Oct 1, 2025
chalmerlowe
chalmerlowe previously approved these changes Oct 7, 2025
Copy link

@chalmerlowe chalmerlowe left a comment

Choose a reason for hiding this comment

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

I am approving, pending:

  • See the #QUESTIONs and determine whether any changes need to be made, if no, then carry on.
  • See the #PREFERENCEs and #NITs and see if there is merit to including them. If no, then carry on.

AsyncSwappableChannel as SwappableChannelType,
)
from google.cloud.bigtable.data._async.metrics_interceptor import (
AsyncBigtableMetricsInterceptor as MetricInterceptorType,

Choose a reason for hiding this comment

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

#QUESTION

Are we purposefully using the singular here?

Should it be:

MetricInterceptorType

OR

MetricsInterceptorType

If plural, refactor globally. There are a number of references to it, I will not point them all out.

Suggested change
AsyncBigtableMetricsInterceptor as MetricInterceptorType,
AsyncBigtableMetricsInterceptor as MetricsInterceptorType,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch, fixed

Comment on lines 276 to 298
if self._emulator_host is not None:
# emulators use insecure channel
create_channel_fn = partial(insecure_channel, self._emulator_host)
else:
elif CrossSync.is_async:
create_channel_fn = partial(TransportType.create_channel, *args, **kwargs)
return AsyncSwappableChannel(create_channel_fn)
else:
# attach sync interceptors in create_channel_fn
def create_channel_fn():
return intercept_channel(
TransportType.create_channel(*args, **kwargs),
self._metrics_interceptor,
)

new_channel = SwappableChannelType(create_channel_fn)
if CrossSync.is_async:
# attach async interceptors

Choose a reason for hiding this comment

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

#PREFERENCE
This is a preference as I believe it may enhance readability (albeit only a small bit). Feel free to ignore if you disagree:

  • I updated or added comments for each block.
  • I updated the naming of one function (sync_create_channel_fn()) to provide a bit more context.
        if self._emulator_host is not None:
            # Emulators use insecure channels.
            create_channel_fn = partial(insecure_channel, self._emulator_host)
        else:
        elif CrossSync.is_async:
            # For async environments, use the default create_channel.
            create_channel_fn = partial(TransportType.create_channel, *args, **kwargs)
        return AsyncSwappableChannel(create_channel_fn)
        else:
            # For sync environments, wrap create_channel with interceptors.
            def sync_create_channel_fn():
                return intercept_channel(
                    TransportType.create_channel(*args, **kwargs),
                    self._metrics_interceptor,
                )
            create_channel = sync_create_channel_fn

        # Instantiate SwappableChannelType with the determined creation function.
        new_channel = SwappableChannelType(create_channel_fn)
        if CrossSync.is_async:
            # Attach async interceptors to the channel instance itself.

Why?

  • The code clearly separates the logic for determining how the create_channel_fn will be defined based on _emulator_host and CrossSync.is_async.
  • For the synchronous case (else block), a nested function sync_create_channel_fn encapsulates the intercept_channel logic. This improves readability by more clearly showing what happens in the synchronous path.

Choose a reason for hiding this comment

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

IF you choose to include the above comments, ensure that matching comments go into the similar construct below in the _sync_autogen section of the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, those look like good changes.

Unfortunately comments won't be copied over to the auto-generated sync code, since it's not part of the ast tree. But I'll leave those comments on the async side (which is where developers should be looking anyway)

row_key = b"bulk_mutate"
new_value = uuid.uuid4().hex.encode()
row_key, mutation = self._create_row_and_mutation(
(row_key, mutation) = self._create_row_and_mutation(

Choose a reason for hiding this comment

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

#PREFERENCE

Why are we encapsulating all of these values in parens? To the best of my knowledge, this is not required by either PEP 8 or the Google Python Style Guide nor have I heard it referred to as a best practice. I don't see any other examples of this practice anywhere else in this codebase.

Feels unnecessary. I recommend ditching them unless we have a really good reason to add them.

Suggested change
(row_key, mutation) = self._create_row_and_mutation(
row_key, mutation = self._create_row_and_mutation(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code auto-generated, so this wasn't a conscious choice. Something must have changed in how black is post processing the generated code.

It feels unnecessary to me too, but it doesn't bother me enough to fight with the generation system to change it back

\#\ WITHOUT\ WARRANTIES\ OR\ CONDITIONS\ OF\ ANY\ KIND,\ either\ express\ or\ implied\.\n
\#\ See\ the\ License\ for\ the\ specific\ language\ governing\ permissions\ and\n
\#\ limitations\ under\ the\ License\.
\#\ limitations\ under\ the\ License

Choose a reason for hiding this comment

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

#QUESTION

I know it is a little thing, but why are we deciding to skip the check for the presence of a trailing period in the license agreement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So this test is for the code generation system. It's checking to make sure the generated code matches the input, but it's not not strictly meant to be testing the licenses themselves.

If the code generator is doing an async->sync transformation, and it carries over a license in the async file that's missing the trailing period, that should still count as a successful generation. We want the generator to be a bit forgiving about this kind of thing, so we can easily apply it to more codebases in the future.

If we decide we want to be more strict about the licenses, that could add a separate stand-alone test, to make sure it covers all the async code too.


@CrossSync.pytest
async def test_unary_unary_interceptor_failure(self):
"""test a failed RpcError with metadata"""

Choose a reason for hiding this comment

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

#NIT

Ensure docstrings start with a capital letter. This happens in at least two places, maybe more.

@CrossSync.convert
async def _streaming_generator_wrapper(call):
"""
Wrapped generator to be returned by intercept_unary_stream

Choose a reason for hiding this comment

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

#NIT:

One liners should end in a period, exclamation mark, or question mark.
I recommend updating the docstrings as necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: bigtable Issues related to the googleapis/python-bigtable API. size: l Pull request size is large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants