Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3b4feff
added pre-wrapping to async client
daniel-sanche Sep 6, 2023
4e9cc05
copied over optimization for input proto
daniel-sanche Sep 6, 2023
90d41ec
avoid compiling regex on each call
daniel-sanche Sep 6, 2023
8001304
Revert "avoid compiling regex on each call"
daniel-sanche Sep 6, 2023
6a73c13
Merge branch 'main' into async-optimizations
daniel-sanche Oct 6, 2023
1ac92d5
Merge branch 'main' into async-optimizations
daniel-sanche Mar 18, 2024
6113869
use AsyncRetry
daniel-sanche Mar 18, 2024
2ce9300
added missing imports
daniel-sanche Mar 18, 2024
0b3daa0
use async_retries
daniel-sanche Mar 18, 2024
7bdcee2
added missing if block
daniel-sanche Mar 18, 2024
266fbb4
updated goldens
daniel-sanche Mar 19, 2024
b6835fe
Merge branch 'main' into async-optimizations
daniel-sanche Mar 19, 2024
9e3bee5
added test for wrappers
daniel-sanche Mar 19, 2024
6477533
regenerated goldens
daniel-sanche Mar 20, 2024
e7d6363
add fix for operation rpcs
daniel-sanche Mar 20, 2024
25636ff
only generate rest test on supported clients
daniel-sanche Mar 20, 2024
af85299
updated goldens
daniel-sanche Mar 20, 2024
0a402ad
updated method name calculation for tests
daniel-sanche Mar 20, 2024
6cfe1fa
addressed comments
daniel-sanche Mar 20, 2024
8f72ea4
updated goldens
daniel-sanche Mar 21, 2024
ac43d7f
Merge branch 'main' into async-optimizations
daniel-sanche Apr 4, 2024
bbddf0a
add bullets
daniel-sanche Apr 4, 2024
ce68134
improved indentation
daniel-sanche Apr 4, 2024
f0b3466
update docstring
daniel-sanche Apr 4, 2024
e26a910
fix comment
daniel-sanche Apr 4, 2024
a5bd36e
only add operation comment on operation tests
daniel-sanche Apr 4, 2024
2c04216
changed sync comments to match async
daniel-sanche Apr 4, 2024
5aebfb3
updated goldens
daniel-sanche Apr 4, 2024
153d0b7
added asyncio mark to test
daniel-sanche Apr 5, 2024
11366ca
fixed formatting
daniel-sanche Apr 5, 2024
85b1645
added extended lro check
daniel-sanche Apr 5, 2024
0796c82
improving async tests
daniel-sanche Apr 5, 2024
8b9aa4c
remove full_extended_lro
daniel-sanche Apr 5, 2024
95e51ac
trying different async mock method
daniel-sanche Apr 5, 2024
7862e6c
changed async mock
daniel-sanche Apr 5, 2024
61a23bf
capture args
daniel-sanche Apr 5, 2024
4a80a44
updated goldens
daniel-sanche Apr 5, 2024
8c5f860
call mock
daniel-sanche Apr 5, 2024
db060fb
use awaitable subclass
daniel-sanche Apr 5, 2024
880432c
removed type hint
daniel-sanche Apr 5, 2024
3336a01
simplified method calling
daniel-sanche Apr 5, 2024
60b5fce
moved test
daniel-sanche Apr 5, 2024
f895f7d
use proper method names
daniel-sanche Apr 5, 2024
b0068f7
moved test
daniel-sanche Apr 5, 2024
10fb69f
moved test back
daniel-sanche Apr 5, 2024
7ecbe6f
updated goldens
daniel-sanche Apr 5, 2024
73e006e
Merge branch 'main' into async-optimizations
daniel-sanche Apr 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,12 @@ class {{ service.async_client_name }}:
request = {{ method.input.ident }}({% if method.input.ident.package != method.ident.package %}{% for f in method.flattened_fields.values() %}{{ f.name }}={{ f.name }}, {% endfor %}{% endif %})
{% endif %}{# Cross-package req and flattened fields #}
{% else %}
request = {{ method.input.ident }}(request)
# Minor optimization to avoid making a copy if the user passes
# in a {{ method.input.ident }}.
# There's no risk of modifying the input as we've already verified
# there are no flattened fields.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
# Minor optimization to avoid making a copy if the user passes
# in a {{ method.input.ident }}.
# There's no risk of modifying the input as we've already verified
# there are no flattened fields.
# - Use the request object if provided (there's no risk of modifying the input as
# there are no flattened fields), or create one.

I have the - bullet to signify this is a sub-point of "Create or coerce a protobuf request object"

Could you add similar bullets to "Quick check" and "The request isn't ..." above (for the same reason)?

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.

Sure, but keep in mind this was taken directly from the sync version. Should we update this there too?

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 went ahead and changed the sync version to match. Let me know if that works

if not isinstance(request, {{ method.input.ident }}):
request = {{ method.input.ident }}(request)
{% endif %} {# different request package #}

{# Vanilla python protobuf wrapper types cannot _set_ repeated fields #}
Expand All @@ -353,26 +358,9 @@ class {{ service.async_client_name }}:

# Wrap the RPC method; this adds retry and timeout information,
# and friendly error handling.
rpc = gapic_v1.method_async.wrap_method(
self._client._transport.{{ method.transport_safe_name|snake_case }},
{% if method.retry %}
default_retry=retries.AsyncRetry(
{% if method.retry.initial_backoff %}initial={{ method.retry.initial_backoff }},{% endif %}
{% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},{% endif %}
{% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},{% endif %}
predicate=retries.if_exception_type(
{% for ex in method.retry.retryable_exceptions|sort(attribute="__name__") %}
core_exceptions.{{ ex.__name__ }},
{% endfor %}
),
deadline={{ method.timeout }},
),
{% endif %}
default_timeout={{ method.timeout }},
client_info=DEFAULT_CLIENT_INFO,
)
{% if method.field_headers %}
rpc = self._client._transport._wrapped_methods[self._client._transport.{{ method.transport_safe_name|snake_case }}]

{% if method.field_headers %}
# Certain fields should be provided within the metadata header;
# add these here.
metadata = tuple(metadata) + (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union

from google.api_core import gapic_v1
from google.api_core import grpc_helpers_async
from google.api_core import exceptions as core_exceptions
from google.api_core import retry_async as retries
{% if service.has_lro %}
from google.api_core import operations_v1
{% endif %}
Expand Down Expand Up @@ -378,6 +380,32 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
return self._stubs["test_iam_permissions"]
{% endif %}

def _prep_wrapped_messages(self, client_info):
# Precompute the wrapped methods.
# override base class to use async wrappers
Comment thread
daniel-sanche marked this conversation as resolved.
Outdated
self._wrapped_methods = {
{% for method in service.methods.values() %}
self.{{ method.transport_safe_name|snake_case }}: gapic_v1.method_async.wrap_method(
self.{{ method.transport_safe_name|snake_case }},
{% if method.retry %}
default_retry=retries.AsyncRetry(
{% if method.retry.initial_backoff %}initial={{ method.retry.initial_backoff }},{% endif %}
{% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},{% endif %}
{% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},{% endif %}
predicate=retries.if_exception_type(
{% for ex in method.retry.retryable_exceptions|sort(attribute='__name__') %}
core_exceptions.{{ ex.__name__ }},
{% endfor %}
),
deadline={{ method.timeout }},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: to fix indentation in generated code (see my comment on the generated file), maybe something like this?

Suggested change
{% if method.retry.initial_backoff %}initial={{ method.retry.initial_backoff }},{% endif %}
{% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},{% endif %}
{% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},{% endif %}
predicate=retries.if_exception_type(
{% for ex in method.retry.retryable_exceptions|sort(attribute='__name__') %}
core_exceptions.{{ ex.__name__ }},
{% endfor %}
),
deadline={{ method.timeout }},
{% if method.retry.initial_backoff %}initial={{ method.retry.initial_backoff }},
{% endif %}
{% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},
{% endif %}
{% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},
{% endif %}
predicate=retries.if_exception_type(
{% for ex in method.retry.retryable_exceptions|sort(attribute='__name__') %}
core_exceptions.{{ ex.__name__ }},
{% endfor %}
),
deadline={{ method.timeout }},

And maybe do this in /transports/base.py.j2 as well, while you're at it?

),
{% endif %}
default_timeout={{ method.timeout }},
client_info=client_info,
),
{% endfor %} {# precomputed wrappers loop #}
Comment thread
daniel-sanche marked this conversation as resolved.
Outdated
}

def close(self):
return self.grpc_channel.close()

Expand Down
127 changes: 127 additions & 0 deletions gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,89 @@ def test_{{ method_name }}_empty_call():
{% endif %}
{% endif %}

def test_{{ method_name }}_use_cached_wrapped_rpc():
# Clients should use _prep_wrapped_messages to create cached wrapped rpcs,
# instead of constructing them on each call
with mock.patch("google.api_core.gapic_v1.method.wrap_method") as wrapper_fn:
client = {{ service.client_name }}(
credentials=ga_credentials.AnonymousCredentials(),
transport="grpc",
)

# Should wrap all calls on client creation
assert wrapper_fn.call_count > 0
wrapper_fn.reset_mock()

# Ensure method has been cached
assert client._transport.{{method.transport_safe_name|snake_case}} in client._transport._wrapped_methods

# Replace cached wrapped function with mock
mock_rpc = mock.Mock()
client._transport._wrapped_methods[client._transport.{{method.transport_safe_name|snake_case}}] = mock_rpc

request = {}
{% if method.client_streaming %}
requests = [request]
client.{{ method.safe_name|snake_case }}(iter(requests))
{% else %}
client.{{ method_name }}(request)
{% endif %}

# Establish that the underlying gRPC stub method was called.
assert mock_rpc.call_count == 1

# Operation methods build a cached wrapper on first rpc call
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we make this stanza conditional on the method in fact being an Operation method?

# subsequent calls should use the cached wrapper
wrapper_fn.reset_mock()
{% if method.client_streaming %}
client.{{ method.safe_name|snake_case }}(iter(requests))
{% else %}
client.{{ method_name }}(request)
{% endif %}

# Establish that a new wrapper was not created for this call
assert wrapper_fn.call_count == 0
assert mock_rpc.call_count == 2


async def test_{{ method_name }}_async_use_cached_wrapped_rpc(transport: str = "grpc_asyncio"):
# Clients should use _prep_wrapped_messages to create cached wrapped rpcs,
# instead of constructing them on each call
with mock.patch("google.api_core.gapic_v1.method_async.wrap_method") as wrapper_fn:
client = {{ service.async_client_name }}(
credentials=ga_credentials.AnonymousCredentials(),
transport=transport,
)

# Should wrap all calls on client creation
assert wrapper_fn.call_count > 0
wrapper_fn.reset_mock()

# Ensure method has been cached
assert client._client._transport.{{method.transport_safe_name|snake_case}} in client._client._transport._wrapped_methods

# Replace cached wrapped function with mock
mock_rpc = mock.AsyncMock()
client._client._transport._wrapped_methods[client._client._transport.{{method.transport_safe_name|snake_case}}] = mock_rpc

request = {}
{% if method.client_streaming %}
requests = [request]
{% endif %}
{% if method.client_streaming and method.server_streaming %}
await client.{{ method.name|snake_case }}(iter(requests))
{% elif method.client_streaming and not method.server_streaming %}
await (await client.{{ method.name|snake_case }}(iter(requests)))
{% else %}
await client.{{ method_name }}(request)
{% endif %}

# Establish that the underlying gRPC stub method was called.
assert mock_rpc.call_count == 1

# Establish that a new wrapper was not created for this call
assert wrapper_fn.call_count == 0

{% if not full_extended_lro %}
@pytest.mark.asyncio
async def test_{{ method_name }}_async(transport: str = 'grpc_asyncio', request_type={{ method.input.ident }}):
Expand Down Expand Up @@ -1069,6 +1152,50 @@ def test_{{ method_name }}_rest(request_type):
{% endfor %}
{% endif %}

def test_{{ method_name }}_rest_use_cached_wrapped_rpc():
# Clients should use _prep_wrapped_messages to create cached wrapped rpcs,
# instead of constructing them on each call
with mock.patch("google.api_core.gapic_v1.method.wrap_method") as wrapper_fn:
client = {{ service.client_name }}(
credentials=ga_credentials.AnonymousCredentials(),
transport="rest",
)

# Should wrap all calls on client creation
assert wrapper_fn.call_count > 0
wrapper_fn.reset_mock()

# Ensure method has been cached
assert client._transport.{{method.transport_safe_name|snake_case}} in client._transport._wrapped_methods

# Replace cached wrapped function with mock
mock_rpc = mock.Mock()
client._transport._wrapped_methods[client._transport.{{method.transport_safe_name|snake_case}}] = mock_rpc

request = {}
{% if method.client_streaming %}
requests = [request]
client.{{ method.safe_name|snake_case }}(iter(requests))
{% else %}
client.{{ method_name }}(request)
{% endif %}

# Establish that the underlying gRPC stub method was called.
assert mock_rpc.call_count == 1

# Operation methods build a cached wrapper on first rpc call
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we make this stanza conditional on the method in fact being an Operation method?

# subsequent calls should use the cached wrapper
wrapper_fn.reset_mock()
{% if method.client_streaming %}
client.{{ method.safe_name|snake_case }}(iter(requests))
{% else %}
client.{{ method_name }}(request)
{% endif %}

# Establish that a new wrapper was not created for this call
assert wrapper_fn.call_count == 0
assert mock_rpc.call_count == 2


{% if method.input.required_fields %}
def test_{{ method_name }}_rest_required_fields(request_type={{ method.input.ident }}):
Expand Down
Loading