Skip to content

Commit 369f461

Browse files
authored
Switch back to botocore _make_api_call under the hood (#1079)
1 parent c5e91ca commit 369f461

File tree

12 files changed

+1775
-1883
lines changed

12 files changed

+1775
-1883
lines changed

bench/benchmark.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import timeit
2+
import io
3+
import logging
4+
import zlib
5+
from datetime import datetime
6+
7+
import urllib3
8+
9+
COUNT = 1000
10+
11+
benchmark_results = []
12+
benchmark_registry = {}
13+
14+
15+
def register_benchmark(testname):
16+
def _wrap(func):
17+
benchmark_registry[testname] = func
18+
return func
19+
return _wrap
20+
21+
22+
def results_new_benchmark(name: str) -> None:
23+
benchmark_results.append((name, {}))
24+
print(name)
25+
26+
27+
def results_record_result(callback, count):
28+
callback_name = callback.__name__
29+
bench_name = callback_name.split('_', 1)[-1]
30+
try:
31+
results = timeit.repeat(
32+
f"{callback_name}()",
33+
setup=f"from __main__ import patch_urllib3, {callback_name}; patch_urllib3()",
34+
repeat=10,
35+
number=count,
36+
)
37+
except Exception:
38+
logging.exception(f"error running {bench_name}")
39+
return
40+
result = count / min(results)
41+
benchmark_results.append((bench_name, str(result)))
42+
43+
print(f"{bench_name}: {result:,.02f} calls/sec")
44+
45+
46+
# =============================================================================
47+
# Monkeypatching
48+
# =============================================================================
49+
50+
def mock_urlopen(self, method, url, body, headers, **kwargs):
51+
target = headers.get('X-Amz-Target')
52+
if target.endswith(b'DescribeTable'):
53+
body = """{
54+
"Table": {
55+
"TableName": "users",
56+
"TableArn": "arn",
57+
"CreationDateTime": "1421866952.062",
58+
"ItemCount": 0,
59+
"TableSizeBytes": 0,
60+
"TableStatus": "ACTIVE",
61+
"ProvisionedThroughput": {
62+
"NumberOfDecreasesToday": 0,
63+
"ReadCapacityUnits": 1,
64+
"WriteCapacityUnits": 25
65+
},
66+
"AttributeDefinitions": [{"AttributeName": "user_name", "AttributeType": "S"}],
67+
"KeySchema": [{"AttributeName": "user_name", "KeyType": "HASH"}],
68+
"LocalSecondaryIndexes": [],
69+
"GlobalSecondaryIndexes": []
70+
}
71+
}
72+
"""
73+
elif target.endswith(b'GetItem'):
74+
# TODO: sometimes raise exc
75+
body = """{
76+
"Item": {
77+
"user_name": {"S": "some_user"},
78+
"email": {"S": "some_user@gmail.com"},
79+
"first_name": {"S": "John"},
80+
"last_name": {"S": "Doe"},
81+
"phone_number": {"S": "4155551111"},
82+
"country": {"S": "USA"},
83+
"preferences": {
84+
"M": {
85+
"timezone": {"S": "America/New_York"},
86+
"allows_notifications": {"BOOL": 1},
87+
"date_of_birth": {"S": "2022-10-26T20:00:00.000000+0000"}
88+
}
89+
},
90+
"last_login": {"S": "2022-10-27T20:00:00.000000+0000"}
91+
}
92+
}
93+
"""
94+
elif target.endswith(b'PutItem'):
95+
body = """{
96+
"Attributes": {
97+
"user_name": {"S": "some_user"},
98+
"email": {"S": "some_user@gmail.com"},
99+
"first_name": {"S": "John"},
100+
"last_name": {"S": "Doe"},
101+
"phone_number": {"S": "4155551111"},
102+
"country": {"S": "USA"},
103+
"preferences": {
104+
"M": {
105+
"timezone": {"S": "America/New_York"},
106+
"allows_notifications": {"BOOL": 1},
107+
"date_of_birth": {"S": "2022-10-26T20:44:49.207740+0000"}
108+
}
109+
},
110+
"last_login": {"S": "2022-10-27T20:00:00.000000+0000"}
111+
}
112+
}
113+
"""
114+
else:
115+
body = ""
116+
117+
body_bytes = body.encode('utf-8')
118+
headers = {
119+
"content-type": "application/x-amz-json-1.0",
120+
"content-length": str(len(body_bytes)),
121+
"x-amz-crc32": str(zlib.crc32(body_bytes)),
122+
"x-amz-requestid": "YB5DURFL1EQ6ULM39GSEEHFTYTPBBUXDJSYPFZPR4EL7M3AYV0RS",
123+
}
124+
125+
# TODO: consumed capacity?
126+
127+
body = io.BytesIO(body_bytes)
128+
resp = urllib3.HTTPResponse(
129+
body,
130+
preload_content=False,
131+
headers=headers,
132+
status=200,
133+
)
134+
resp.chunked = False
135+
return resp
136+
137+
138+
def patch_urllib3():
139+
urllib3.connectionpool.HTTPConnectionPool.urlopen = mock_urlopen
140+
141+
142+
# =============================================================================
143+
# Setup
144+
# =============================================================================
145+
146+
import os
147+
from pynamodb.models import Model
148+
from pynamodb.attributes import UnicodeAttribute, BooleanAttribute, MapAttribute, UTCDateTimeAttribute
149+
150+
151+
os.environ["AWS_ACCESS_KEY_ID"] = "1"
152+
os.environ["AWS_SECRET_ACCESS_KEY"] = "1"
153+
os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
154+
155+
156+
class UserPreferences(MapAttribute):
157+
timezone = UnicodeAttribute()
158+
allows_notifications = BooleanAttribute()
159+
date_of_birth = UTCDateTimeAttribute()
160+
161+
162+
class UserModel(Model):
163+
class Meta:
164+
table_name = 'User'
165+
max_retry_attempts = 0 # TODO: do this conditionally. need to replace the connection object
166+
user_name = UnicodeAttribute(hash_key=True)
167+
first_name = UnicodeAttribute()
168+
last_name = UnicodeAttribute()
169+
phone_number = UnicodeAttribute()
170+
country = UnicodeAttribute()
171+
email = UnicodeAttribute()
172+
preferences = UserPreferences(null=True)
173+
last_login = UTCDateTimeAttribute()
174+
175+
176+
# =============================================================================
177+
# GetItem
178+
# =============================================================================
179+
180+
@register_benchmark("get_item")
181+
def bench_get_item():
182+
UserModel.get("username")
183+
184+
185+
# =============================================================================
186+
# PutItem
187+
# =============================================================================
188+
189+
@register_benchmark("put_item")
190+
def bench_put_item():
191+
UserModel(
192+
"username",
193+
email="some_user@gmail.com",
194+
first_name="John",
195+
last_name="Doe",
196+
phone_number="4155551111",
197+
country="USA",
198+
preferences=UserPreferences(
199+
timezone="America/New_York",
200+
allows_notifications=True,
201+
date_of_birth=datetime.utcnow(),
202+
),
203+
last_login=datetime.utcnow(),
204+
).save()
205+
206+
207+
# =============================================================================
208+
# Benchmarks.
209+
# =============================================================================
210+
211+
def main():
212+
results_new_benchmark("Basic operations")
213+
214+
results_record_result(benchmark_registry["get_item"], COUNT)
215+
results_record_result(benchmark_registry["put_item"], COUNT)
216+
217+
print()
218+
print("Above metrics are in call/sec, larger is better.")
219+
220+
221+
if __name__ == "__main__":
222+
main()

docs/settings.rst

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,6 @@ The number of times to retry certain failed DynamoDB API calls. The most common
3434
retries include ``ProvisionedThroughputExceededException`` and ``5xx`` errors.
3535

3636

37-
base_backoff_ms
38-
---------------
39-
40-
Default: ``25``
41-
42-
The base number of milliseconds used for `exponential backoff and jitter
43-
<https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/>`_ on retries.
44-
45-
4637
region
4738
------
4839

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ ignore_errors = True
1717
# TODO: burn these down
1818
[mypy-tests.*]
1919
ignore_errors = True
20+
21+
[mypy-benchmark]
22+
ignore_errors = True
Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Type-annotates the private botocore APIs that we're currently relying on.
33
"""
4-
from typing import Any, Dict, Optional
4+
from typing import Dict
55

66
import botocore.client
77
import botocore.credentials
@@ -22,25 +22,10 @@ class BotocoreRequestSignerPrivate(botocore.signers.RequestSigner):
2222
class BotocoreBaseClientPrivate(botocore.client.BaseClient):
2323
_endpoint: BotocoreEndpointPrivate
2424
_request_signer: BotocoreRequestSignerPrivate
25-
_service_model: botocore.model.ServiceModel
2625

27-
def _resolve_endpoint_ruleset(
26+
def _make_api_call(
2827
self,
29-
operation_model: botocore.model.OperationModel,
30-
params: Dict[str, Any],
31-
request_context: Dict[str, Any],
32-
ignore_signing_region: bool = ...,
33-
):
34-
raise NotImplementedError
35-
36-
def _convert_to_request_dict(
37-
self,
38-
api_params: Dict[str, Any],
39-
operation_model: botocore.model.OperationModel,
40-
*,
41-
endpoint_url: str = ..., # added in botocore 1.28
42-
context: Optional[Dict[str, Any]] = ...,
43-
headers: Optional[Dict[str, Any]] = ...,
44-
set_user_agent_header: bool = ...,
45-
) -> Dict[str, Any]:
28+
operation_name: str,
29+
operation_kwargs: Dict,
30+
) -> Dict:
4631
raise NotImplementedError

0 commit comments

Comments
 (0)