Skip to content

Commit 6251eab

Browse files
feat: add caching to routing header calculation (#526)
Co-authored-by: Anthonios Partheniou <[email protected]>
1 parent 2c16868 commit 6251eab

File tree

2 files changed

+60
-13
lines changed

2 files changed

+60
-13
lines changed

google/api_core/gapic_v1/routing_header.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,45 +20,42 @@
2020
Generally, these headers are specified as gRPC metadata.
2121
"""
2222

23+
import functools
2324
from enum import Enum
2425
from urllib.parse import urlencode
2526

2627
ROUTING_METADATA_KEY = "x-goog-request-params"
28+
# This is the value for the `maxsize` argument of @functools.lru_cache
29+
# https://docs.python.org/3/library/functools.html#functools.lru_cache
30+
# This represents the number of recent function calls to store.
31+
ROUTING_PARAM_CACHE_SIZE = 32
2732

2833

2934
def to_routing_header(params, qualified_enums=True):
3035
"""Returns a routing header string for the given request parameters.
3136
3237
Args:
33-
params (Mapping[str, Any]): A dictionary containing the request
38+
params (Mapping[str, str | bytes | Enum]): A dictionary containing the request
3439
parameters used for routing.
3540
qualified_enums (bool): Whether to represent enum values
3641
as their type-qualified symbol names instead of as their
3742
unqualified symbol names.
3843
3944
Returns:
4045
str: The routing header string.
41-
4246
"""
47+
tuples = params.items() if isinstance(params, dict) else params
4348
if not qualified_enums:
44-
if isinstance(params, dict):
45-
tuples = params.items()
46-
else:
47-
tuples = params
48-
params = [(x[0], x[1].name) if isinstance(x[1], Enum) else x for x in tuples]
49-
return urlencode(
50-
params,
51-
# Per Google API policy (go/api-url-encoding), / is not encoded.
52-
safe="/",
53-
)
49+
tuples = [(x[0], x[1].name) if isinstance(x[1], Enum) else x for x in tuples]
50+
return "&".join([_urlencode_param(*t) for t in tuples])
5451

5552

5653
def to_grpc_metadata(params, qualified_enums=True):
5754
"""Returns the gRPC metadata containing the routing headers for the given
5855
request parameters.
5956
6057
Args:
61-
params (Mapping[str, Any]): A dictionary containing the request
58+
params (Mapping[str, str | bytes | Enum]): A dictionary containing the request
6259
parameters used for routing.
6360
qualified_enums (bool): Whether to represent enum values
6461
as their type-qualified symbol names instead of as their
@@ -69,3 +66,22 @@ def to_grpc_metadata(params, qualified_enums=True):
6966
and value.
7067
"""
7168
return (ROUTING_METADATA_KEY, to_routing_header(params, qualified_enums))
69+
70+
71+
# use caching to avoid repeated computation
72+
@functools.lru_cache(maxsize=ROUTING_PARAM_CACHE_SIZE)
73+
def _urlencode_param(key, value):
74+
"""Cacheable wrapper over urlencode
75+
76+
Args:
77+
key (str): The key of the parameter to encode.
78+
value (str | bytes | Enum): The value of the parameter to encode.
79+
80+
Returns:
81+
str: The encoded parameter.
82+
"""
83+
return urlencode(
84+
{key: value},
85+
# Per Google API policy (go/api-url-encoding), / is not encoded.
86+
safe="/",
87+
)

tests/unit/gapic/test_routing_header.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,34 @@ def test_to_grpc_metadata():
7070
params = [("name", "meep"), ("book.read", "1")]
7171
metadata = routing_header.to_grpc_metadata(params)
7272
assert metadata == (routing_header.ROUTING_METADATA_KEY, "name=meep&book.read=1")
73+
74+
75+
@pytest.mark.parametrize(
76+
"key,value,expected",
77+
[
78+
("book.read", "1", "book.read=1"),
79+
("name", "me/ep", "name=me/ep"),
80+
("\\", "=", "%5C=%3D"),
81+
(b"hello", "world", "hello=world"),
82+
("✔️", "✌️", "%E2%9C%94%EF%B8%8F=%E2%9C%8C%EF%B8%8F"),
83+
],
84+
)
85+
def test__urlencode_param(key, value, expected):
86+
result = routing_header._urlencode_param(key, value)
87+
assert result == expected
88+
89+
90+
def test__urlencode_param_caching_performance():
91+
import time
92+
93+
key = "key" * 100
94+
value = "value" * 100
95+
# time with empty cache
96+
start_time = time.perf_counter()
97+
routing_header._urlencode_param(key, value)
98+
duration = time.perf_counter() - start_time
99+
second_start_time = time.perf_counter()
100+
routing_header._urlencode_param(key, value)
101+
second_duration = time.perf_counter() - second_start_time
102+
# second call should be approximately 10 times faster
103+
assert second_duration < duration / 10

0 commit comments

Comments
 (0)