Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 2f2fb5d

Browse files
authored
feat: precache wrapped rpcs (#553)
During transport construction, cache the wrapped methods that the client will eventually use when invoking rpcs. This has a ~7.4% time impact in synthetic benchmarks.
1 parent 5239ca8 commit 2f2fb5d

10 files changed

Lines changed: 109 additions & 61 deletions

File tree

gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
211211
client_cert_source=client_options.client_cert_source,
212212
)
213213

214+
214215
{% for method in service.methods.values() -%}
215216
def {{ method.name|snake_case }}(self,
216217
{%- if not method.client_streaming %}
@@ -307,25 +308,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
307308

308309
# Wrap the RPC method; this adds retry and timeout information,
309310
# and friendly error handling.
310-
rpc = gapic_v1.method.wrap_method(
311-
self._transport.{{ method.name|snake_case }},
312-
{%- if method.retry %}
313-
default_retry=retries.Retry(
314-
{% if method.retry.initial_backoff %}initial={{ method.retry.initial_backoff }},{% endif %}
315-
{% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},{% endif %}
316-
{% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},{% endif %}
317-
predicate=retries.if_exception_type(
318-
{%- filter sort_lines %}
319-
{%- for ex in method.retry.retryable_exceptions %}
320-
exceptions.{{ ex.__name__ }},
321-
{%- endfor %}
322-
{%- endfilter %}
323-
),
324-
),
325-
{%- endif %}
326-
default_timeout={{ method.timeout }},
327-
client_info=_client_info,
328-
)
311+
rpc = self._transport._wrapped_methods[self._transport.{{ method.name|snake_case}}]
329312
{%- if method.field_headers %}
330313

331314
# Certain fields should be provided within the metadata header;
@@ -381,16 +364,6 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
381364
{% endfor %}
382365

383366

384-
try:
385-
_client_info = gapic_v1.client_info.ClientInfo(
386-
gapic_version=pkg_resources.get_distribution(
387-
'{{ api.naming.warehouse_package_name }}',
388-
).version,
389-
)
390-
except pkg_resources.DistributionNotFound:
391-
_client_info = gapic_v1.client_info.ClientInfo()
392-
393-
394367
__all__ = (
395368
'{{ service.client_name }}',
396369
)

gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/base.py.j2

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
{% block content %}
44
import abc
55
import typing
6+
import pkg_resources
67

78
from google import auth
9+
from google.api_core import gapic_v1 # type: ignore
810
{%- if service.has_lro %}
911
from google.api_core import operations_v1 # type: ignore
1012
{%- endif %}
@@ -17,6 +19,16 @@ from google.auth import credentials # type: ignore
1719
{% endfor -%}
1820
{% endfilter %}
1921

22+
try:
23+
_client_info = gapic_v1.client_info.ClientInfo(
24+
gapic_version=pkg_resources.get_distribution(
25+
'{{ api.naming.warehouse_package_name }}',
26+
).version,
27+
)
28+
except pkg_resources.DistributionNotFound:
29+
_client_info = gapic_v1.client_info.ClientInfo()
30+
31+
2032
class {{ service.name }}Transport(metaclass=abc.ABCMeta):
2133
"""Abstract transport class for {{ service.name }}."""
2234

@@ -54,6 +66,37 @@ class {{ service.name }}Transport(metaclass=abc.ABCMeta):
5466

5567
# Save the credentials.
5668
self._credentials = credentials
69+
70+
# Lifted into its own function so it can be stubbed out during tests.
71+
self._prep_wrapped_messages()
72+
73+
def _prep_wrapped_messages(self):
74+
# Precomputed wrapped methods
75+
self._wrapped_methods = {
76+
{% for method in service.methods.values() -%}
77+
self.{{ method.name|snake_case }}: gapic_v1.method.wrap_method(
78+
self.{{ method.name|snake_case }},
79+
{%- if method.retry %}
80+
default_retry=retries.Retry(
81+
{% if method.retry.initial_backoff %}initial={{ method.retry.initial_backoff }},{% endif %}
82+
{% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},{% endif %}
83+
{% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},{% endif %}
84+
predicate=retries.if_exception_type(
85+
{%- filter sort_lines %}
86+
{%- for ex in method.retry.retryable_exceptions %}
87+
exceptions.{{ ex.__name__ }},
88+
{%- endfor %}
89+
{%- endfilter %}
90+
),
91+
),
92+
{%- endif %}
93+
default_timeout={{ method.timeout }},
94+
client_info=_client_info,
95+
),
96+
{% endfor %} {# precomputed wrappers loop #}
97+
}
98+
99+
57100
{%- if service.has_lro %}
58101

59102
@property

gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/grpc.py.j2

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
9898
scopes=self.AUTH_SCOPES,
9999
)
100100

101+
self._stubs = {} # type: Dict[str, Callable]
102+
101103
# Run the base constructor.
102104
super().__init__(host=host, credentials=credentials)
103-
self._stubs = {} # type: Dict[str, Callable]
104105

105106

106107
@classmethod

gapic/ads-templates/setup.py.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ setuptools.setup(
1919
'google-api-core >= 1.17.0, < 2.0.0dev',
2020
'googleapis-common-protos >= 1.5.8',
2121
'grpcio >= 1.10.0',
22-
'proto-plus >= 1.1.0',
22+
'proto-plus >= 1.4.0',
2323
{%- if api.requires_package(('google', 'iam', 'v1')) %}
2424
'grpc-google-iam-v1',
2525
{%- endif %}

gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def test_{{ service.client_name|snake_case }}_client_options():
187187

188188

189189
def test_{{ service.client_name|snake_case }}_client_options_from_dict():
190-
with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}GrpcTransport.__init__') as grpc_transport:
190+
with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}Transport.__init__') as grpc_transport:
191191
grpc_transport.return_value = None
192192
client = {{ service.client_name }}(
193193
client_options={'api_endpoint': 'squid.clam.whelk'}
@@ -556,9 +556,11 @@ def test_transport_grpc_default():
556556

557557
def test_{{ service.name|snake_case }}_base_transport():
558558
# Instantiate the base transport.
559-
transport = transports.{{ service.name }}Transport(
560-
credentials=credentials.AnonymousCredentials(),
561-
)
559+
with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}GrpcTransport.__init__') as Transport:
560+
Transport.return_value = None
561+
transport = transports.{{ service.name }}Transport(
562+
credentials=credentials.AnonymousCredentials(),
563+
)
562564

563565
# Every method on the transport should just blindly
564566
# raise NotImplementedError.

gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
221221
quota_project_id=client_options.quota_project_id,
222222
)
223223

224+
224225
{% for method in service.methods.values() -%}
225226
def {{ method.name|snake_case }}(self,
226227
{%- if not method.client_streaming %}
@@ -317,25 +318,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
317318

318319
# Wrap the RPC method; this adds retry and timeout information,
319320
# and friendly error handling.
320-
rpc = gapic_v1.method.wrap_method(
321-
self._transport.{{ method.name|snake_case }},
322-
{%- if method.retry %}
323-
default_retry=retries.Retry(
324-
{% if method.retry.initial_backoff %}initial={{ method.retry.initial_backoff }},{% endif %}
325-
{% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},{% endif %}
326-
{% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},{% endif %}
327-
predicate=retries.if_exception_type(
328-
{%- filter sort_lines %}
329-
{%- for ex in method.retry.retryable_exceptions %}
330-
exceptions.{{ ex.__name__ }},
331-
{%- endfor %}
332-
{%- endfilter %}
333-
),
334-
),
335-
{%- endif %}
336-
default_timeout={{ method.timeout }},
337-
client_info=_client_info,
338-
)
321+
rpc = self._transport._wrapped_methods[self._transport.{{ method.name|snake_case}}]
339322
{%- if method.field_headers %}
340323

341324
# Certain fields should be provided within the metadata header;

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
{% block content %}
44
import abc
55
import typing
6+
import pkg_resources
67

78
from google import auth
89
from google.api_core import exceptions # type: ignore
10+
from google.api_core import gapic_v1 # type: ignore
911
{%- if service.has_lro %}
1012
from google.api_core import operations_v1 # type: ignore
1113
{%- endif %}
@@ -22,6 +24,15 @@ from google.iam.v1 import policy_pb2 as policy # type: ignore
2224
{% endif %}
2325
{% endfilter %}
2426

27+
try:
28+
_client_info = gapic_v1.client_info.ClientInfo(
29+
gapic_version=pkg_resources.get_distribution(
30+
'{{ api.naming.warehouse_package_name }}',
31+
).version,
32+
)
33+
except pkg_resources.DistributionNotFound:
34+
_client_info = gapic_v1.client_info.ClientInfo()
35+
2536
class {{ service.name }}Transport(abc.ABC):
2637
"""Abstract transport class for {{ service.name }}."""
2738

@@ -79,6 +90,38 @@ class {{ service.name }}Transport(abc.ABC):
7990

8091
# Save the credentials.
8192
self._credentials = credentials
93+
94+
# Lifted into its own function so it can be stubbed out during tests.
95+
self._prep_wrapped_messages()
96+
97+
98+
def _prep_wrapped_messages(self):
99+
# Precompute the wrapped methods.
100+
self._wrapped_methods = {
101+
{% for method in service.methods.values() -%}
102+
self.{{ method.name|snake_case }}: gapic_v1.method.wrap_method(
103+
self.{{ method.name|snake_case }},
104+
{%- if method.retry %}
105+
default_retry=retries.Retry(
106+
{% if method.retry.initial_backoff %}initial={{ method.retry.initial_backoff }},{% endif %}
107+
{% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},{% endif %}
108+
{% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},{% endif %}
109+
predicate=retries.if_exception_type(
110+
{%- filter sort_lines %}
111+
{%- for ex in method.retry.retryable_exceptions %}
112+
exceptions.{{ ex.__name__ }},
113+
{%- endfor %}
114+
{%- endfilter %}
115+
),
116+
),
117+
{%- endif %}
118+
default_timeout={{ method.timeout }},
119+
client_info=_client_info,
120+
),
121+
{% endfor %} {# precomputed wrappers loop #}
122+
}
123+
124+
82125
{%- if service.has_lro %}
83126

84127
@property

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
118118
quota_project_id=quota_project_id,
119119
)
120120

121+
self._stubs = {} # type: Dict[str, Callable]
122+
121123
# Run the base constructor.
122124
super().__init__(
123125
host=host,
@@ -127,8 +129,6 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
127129
quota_project_id=quota_project_id,
128130
)
129131

130-
self._stubs = {} # type: Dict[str, Callable]
131-
132132
@classmethod
133133
def create_channel(cls,
134134
host: str{% if service.host %} = '{{ service.host }}'{% endif %},

gapic/templates/setup.py.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ setuptools.setup(
1818
install_requires=(
1919
'google-api-core[grpc] >= 1.22.0, < 2.0.0dev',
2020
'libcst >= 0.2.5',
21-
'proto-plus >= 1.1.0',
21+
'proto-plus >= 1.4.0',
2222
{%- if api.requires_package(('google', 'iam', 'v1')) or opts.add_iam_methods %}
2323
'grpc-google-iam-v1',
2424
{%- endif %}

gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,9 +1008,11 @@ def test_{{ service.name|snake_case }}_base_transport_error():
10081008

10091009
def test_{{ service.name|snake_case }}_base_transport():
10101010
# Instantiate the base transport.
1011-
transport = transports.{{ service.name }}Transport(
1012-
credentials=credentials.AnonymousCredentials(),
1013-
)
1011+
with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}Transport.__init__') as Transport:
1012+
Transport.return_value = None
1013+
transport = transports.{{ service.name }}Transport(
1014+
credentials=credentials.AnonymousCredentials(),
1015+
)
10141016

10151017
# Every method on the transport should just blindly
10161018
# raise NotImplementedError.
@@ -1038,7 +1040,8 @@ def test_{{ service.name|snake_case }}_base_transport():
10381040

10391041
def test_{{ service.name|snake_case }}_base_transport_with_credentials_file():
10401042
# Instantiate the base transport with a credentials file
1041-
with mock.patch.object(auth, 'load_credentials_from_file') as load_creds:
1043+
with mock.patch.object(auth, 'load_credentials_from_file') as load_creds, mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}Transport._prep_wrapped_messages') as Transport:
1044+
Transport.return_value = None
10421045
load_creds.return_value = (credentials.AnonymousCredentials(), None)
10431046
transport = transports.{{ service.name }}Transport(
10441047
credentials_file="credentials.json",

0 commit comments

Comments
 (0)