Skip to content

Commit 1695119

Browse files
feat: add caching to GapicCallable (#666)
* feat: optimize _GapicCallable * cleaned up metadata lines * chore: avoid type checks in error wrapper * Revert "chore: avoid type checks in error wrapper" This reverts commit c97a636. * add default wrapped function * fixed decorator order * fixed spacing * fixed comment typo * fixed spacing * fixed spacing * removed unneeded helpers * use caching * improved metadata parsing * improved docstring * fixed logic * added benchmark test * update threshold * run benchmark in loop for testing * use verbose logs * Revert testing * used smaller value * changed threshold * removed link in comment * use list type for metadata * add types to docstring * fixed lint * convert to list at init time --------- Co-authored-by: Daniel Sanche <[email protected]> Co-authored-by: Daniel Sanche <[email protected]>
1 parent 3c69867 commit 1695119

File tree

2 files changed

+57
-44
lines changed

2 files changed

+57
-44
lines changed

google/api_core/gapic_v1/method.py

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,6 @@ class _MethodDefault(enum.Enum):
4242
so the default should be used."""
4343

4444

45-
def _is_not_none_or_false(value):
46-
return value is not None and value is not False
47-
48-
49-
def _apply_decorators(func, decorators):
50-
"""Apply a list of decorators to a given function.
51-
52-
``decorators`` may contain items that are ``None`` or ``False`` which will
53-
be ignored.
54-
"""
55-
filtered_decorators = filter(_is_not_none_or_false, reversed(decorators))
56-
57-
for decorator in filtered_decorators:
58-
func = decorator(func)
59-
60-
return func
61-
62-
6345
class _GapicCallable(object):
6446
"""Callable that applies retry, timeout, and metadata logic.
6547
@@ -91,44 +73,53 @@ def __init__(
9173
):
9274
self._target = target
9375
self._retry = retry
76+
if isinstance(timeout, (int, float)):
77+
timeout = TimeToDeadlineTimeout(timeout=timeout)
9478
self._timeout = timeout
9579
self._compression = compression
96-
self._metadata = metadata
80+
self._metadata = list(metadata) if metadata is not None else None
9781

9882
def __call__(
9983
self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs
10084
):
10185
"""Invoke the low-level RPC with retry, timeout, compression, and metadata."""
10286

103-
if retry is DEFAULT:
104-
retry = self._retry
105-
106-
if timeout is DEFAULT:
107-
timeout = self._timeout
108-
10987
if compression is DEFAULT:
11088
compression = self._compression
111-
112-
if isinstance(timeout, (int, float)):
113-
timeout = TimeToDeadlineTimeout(timeout=timeout)
114-
115-
# Apply all applicable decorators.
116-
wrapped_func = _apply_decorators(self._target, [retry, timeout])
89+
if compression is not None:
90+
kwargs["compression"] = compression
11791

11892
# Add the user agent metadata to the call.
11993
if self._metadata is not None:
120-
metadata = kwargs.get("metadata", [])
121-
# Due to the nature of invocation, None should be treated the same
122-
# as not specified.
123-
if metadata is None:
124-
metadata = []
125-
metadata = list(metadata)
126-
metadata.extend(self._metadata)
127-
kwargs["metadata"] = metadata
128-
if self._compression is not None:
129-
kwargs["compression"] = compression
94+
try:
95+
# attempt to concatenate default metadata with user-provided metadata
96+
kwargs["metadata"] = [*kwargs["metadata"], *self._metadata]
97+
except (KeyError, TypeError):
98+
# if metadata is not provided, use just the default metadata
99+
kwargs["metadata"] = self._metadata
100+
101+
call = self._build_wrapped_call(timeout, retry)
102+
return call(*args, **kwargs)
103+
104+
@functools.lru_cache(maxsize=4)
105+
def _build_wrapped_call(self, timeout, retry):
106+
"""
107+
Build a wrapped callable that applies retry, timeout, and metadata logic.
108+
"""
109+
wrapped_func = self._target
110+
if timeout is DEFAULT:
111+
timeout = self._timeout
112+
elif isinstance(timeout, (int, float)):
113+
timeout = TimeToDeadlineTimeout(timeout=timeout)
114+
if timeout is not None:
115+
wrapped_func = timeout(wrapped_func)
116+
117+
if retry is DEFAULT:
118+
retry = self._retry
119+
if retry is not None:
120+
wrapped_func = retry(wrapped_func)
130121

131-
return wrapped_func(*args, **kwargs)
122+
return wrapped_func
132123

133124

134125
def wrap_method(
@@ -202,8 +193,9 @@ def get_topic(name, timeout=None):
202193
203194
Args:
204195
func (Callable[Any]): The function to wrap. It should accept an
205-
optional ``timeout`` argument. If ``metadata`` is not ``None``, it
206-
should accept a ``metadata`` argument.
196+
optional ``timeout`` (google.api_core.timeout.Timeout) argument.
197+
If ``metadata`` is not ``None``, it should accept a ``metadata``
198+
(Sequence[Tuple[str, str]]) argument.
207199
default_retry (Optional[google.api_core.Retry]): The default retry
208200
strategy. If ``None``, the method will not retry by default.
209201
default_timeout (Optional[google.api_core.Timeout]): The default

tests/unit/gapic/test_method.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,24 @@ def test_wrap_method_with_call_not_supported():
222222
with pytest.raises(ValueError) as exc_info:
223223
google.api_core.gapic_v1.method.wrap_method(method, with_call=True)
224224
assert "with_call=True is only supported for unary calls" in str(exc_info.value)
225+
226+
227+
def test_benchmark_gapic_call():
228+
"""
229+
Ensure the __call__ method performance does not regress from expectations
230+
231+
__call__ builds a new wrapped function using passed-in timeout and retry, but
232+
subsequent calls are cached
233+
234+
Note: The threshold has been tuned for the CI workers. Test may flake on
235+
slower hardware
236+
"""
237+
from google.api_core.gapic_v1.method import _GapicCallable
238+
from google.api_core.retry import Retry
239+
from timeit import timeit
240+
241+
gapic_callable = _GapicCallable(
242+
lambda *a, **k: 1, retry=Retry(), timeout=1010, compression=False
243+
)
244+
avg_time = timeit(lambda: gapic_callable(), number=10_000)
245+
assert avg_time < 0.4

0 commit comments

Comments
 (0)