Skip to content

Commit 7bfc271

Browse files
committed
Add support for async_simple_cache_middleware
- Add async support for simple cache middleware - Refactor ``SessionCache`` as a more generic ``SimpleCache`` class to be used internally as a standardized cache where appropriate. - Use ``SimpleCache`` as the default cache for the simple cache middleware
1 parent 2f7b627 commit 7bfc271

File tree

10 files changed

+418
-210
lines changed

10 files changed

+418
-210
lines changed

docs/providers.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,5 +484,6 @@ Supported Middleware
484484
- :meth:`Gas Price Strategy <web3.middleware.gas_price_strategy_middleware>`
485485
- :meth:`Buffered Gas Estimate Middleware <web3.middleware.buffered_gas_estimate_middleware>`
486486
- :meth:`Stalecheck Middleware <web3.middleware.make_stalecheck_middleware>`
487-
- :meth:`Validation middleware <web3.middleware.validation>`
487+
- :meth:`Validation Middleware <web3.middleware.validation>`
488488
- :ref:`Geth POA Middleware <geth-poa>`
489+
- :meth:`Simple Cache Middleware <web3.middleware.simple_cache_middleware>`

newsfragments/2579.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Async support for caching certain methods via ``async_simple_cache_middleware`` as well as constructing custom async caching middleware via ``async_construct_simple_cache_middleware``.

tests/core/middleware/test_simple_cache_middleware.py

Lines changed: 181 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import itertools
22
import pytest
3+
import threading
34
import uuid
45

56
from web3 import Web3
@@ -11,9 +12,22 @@
1112
construct_result_generator_middleware,
1213
construct_simple_cache_middleware,
1314
)
15+
from web3.middleware.async_cache import (
16+
async_construct_simple_cache_middleware,
17+
)
18+
from web3.middleware.fixture import (
19+
async_construct_error_generator_middleware,
20+
async_construct_result_generator_middleware,
21+
)
1422
from web3.providers.base import (
1523
BaseProvider,
1624
)
25+
from web3.providers.eth_tester import (
26+
AsyncEthereumTesterProvider,
27+
)
28+
from web3.types import (
29+
RPCEndpoint,
30+
)
1731

1832

1933
@pytest.fixture
@@ -25,8 +39,8 @@ def w3_base():
2539
def result_generator_middleware():
2640
return construct_result_generator_middleware(
2741
{
28-
"fake_endpoint": lambda *_: str(uuid.uuid4()),
29-
"not_whitelisted": lambda *_: str(uuid.uuid4()),
42+
RPCEndpoint("fake_endpoint"): lambda *_: str(uuid.uuid4()),
43+
RPCEndpoint("not_whitelisted"): lambda *_: str(uuid.uuid4()),
3044
}
3145
)
3246

@@ -40,13 +54,15 @@ def w3(w3_base, result_generator_middleware):
4054
def test_simple_cache_middleware_pulls_from_cache(w3):
4155
def cache_class():
4256
return {
43-
generate_cache_key(("fake_endpoint", [1])): {"result": "value-a"},
57+
generate_cache_key(f"{threading.get_ident()}:{('fake_endpoint', [1])}"): {
58+
"result": "value-a"
59+
},
4460
}
4561

4662
w3.middleware_onion.add(
4763
construct_simple_cache_middleware(
4864
cache_class=cache_class,
49-
rpc_whitelist={"fake_endpoint"},
65+
rpc_whitelist={RPCEndpoint("fake_endpoint")},
5066
)
5167
)
5268

@@ -57,7 +73,7 @@ def test_simple_cache_middleware_populates_cache(w3):
5773
w3.middleware_onion.add(
5874
construct_simple_cache_middleware(
5975
cache_class=dict,
60-
rpc_whitelist={"fake_endpoint"},
76+
rpc_whitelist={RPCEndpoint("fake_endpoint")},
6177
)
6278
)
6379

@@ -71,22 +87,22 @@ def test_simple_cache_middleware_does_not_cache_none_responses(w3_base):
7187
counter = itertools.count()
7288
w3 = w3_base
7389

74-
def result_cb(method, params):
90+
def result_cb(_method, _params):
7591
next(counter)
7692
return None
7793

7894
w3.middleware_onion.add(
7995
construct_result_generator_middleware(
8096
{
81-
"fake_endpoint": result_cb,
97+
RPCEndpoint("fake_endpoint"): result_cb,
8298
}
8399
)
84100
)
85101

86102
w3.middleware_onion.add(
87103
construct_simple_cache_middleware(
88104
cache_class=dict,
89-
rpc_whitelist={"fake_endpoint"},
105+
rpc_whitelist={RPCEndpoint("fake_endpoint")},
90106
)
91107
)
92108

@@ -101,15 +117,15 @@ def test_simple_cache_middleware_does_not_cache_error_responses(w3_base):
101117
w3.middleware_onion.add(
102118
construct_error_generator_middleware(
103119
{
104-
"fake_endpoint": lambda *_: f"msg-{uuid.uuid4()}",
120+
RPCEndpoint("fake_endpoint"): lambda *_: f"msg-{uuid.uuid4()}",
105121
}
106122
)
107123
)
108124

109125
w3.middleware_onion.add(
110126
construct_simple_cache_middleware(
111127
cache_class=dict,
112-
rpc_whitelist={"fake_endpoint"},
128+
rpc_whitelist={RPCEndpoint("fake_endpoint")},
113129
)
114130
)
115131

@@ -125,11 +141,165 @@ def test_simple_cache_middleware_does_not_cache_endpoints_not_in_whitelist(w3):
125141
w3.middleware_onion.add(
126142
construct_simple_cache_middleware(
127143
cache_class=dict,
128-
rpc_whitelist={"fake_endpoint"},
144+
rpc_whitelist={RPCEndpoint("fake_endpoint")},
129145
)
130146
)
131147

132148
result_a = w3.manager.request_blocking("not_whitelisted", [])
133149
result_b = w3.manager.request_blocking("not_whitelisted", [])
134150

135151
assert result_a != result_b
152+
153+
154+
# -- async -- #
155+
156+
157+
async def _async_simple_cache_middleware_for_testing(make_request, async_w3):
158+
middleware = await async_construct_simple_cache_middleware(
159+
cache_class=dict,
160+
rpc_whitelist={RPCEndpoint("fake_endpoint")},
161+
)
162+
return await middleware(make_request, async_w3)
163+
164+
165+
@pytest.fixture
166+
def async_w3():
167+
return Web3(
168+
provider=AsyncEthereumTesterProvider(),
169+
middlewares=[
170+
(_async_simple_cache_middleware_for_testing, "simple_cache"),
171+
],
172+
)
173+
174+
175+
@pytest.mark.asyncio
176+
async def test_async_simple_cache_middleware_pulls_from_cache(async_w3):
177+
# remove the pre-loaded simple cache middleware to replace with test-specific:
178+
async_w3.middleware_onion.remove("simple_cache")
179+
180+
def cache_class():
181+
return {
182+
generate_cache_key(f"{threading.get_ident()}:{('fake_endpoint', [1])}"): {
183+
"result": "value-a"
184+
},
185+
}
186+
187+
async def _properly_awaited_middleware(make_request, _async_w3):
188+
middleware = await async_construct_simple_cache_middleware(
189+
cache_class=cache_class,
190+
rpc_whitelist={RPCEndpoint("fake_endpoint")},
191+
)
192+
return await middleware(make_request, _async_w3)
193+
194+
async_w3.middleware_onion.inject(
195+
_properly_awaited_middleware,
196+
"for_this_test_only",
197+
layer=0,
198+
)
199+
200+
_result = await async_w3.manager.coro_request("fake_endpoint", [1])
201+
assert _result == "value-a"
202+
203+
# -- teardown -- #
204+
async_w3.middleware_onion.remove("for_this_test_only")
205+
# add back the pre-loaded simple cache middleware:
206+
async_w3.middleware_onion.add(
207+
_async_simple_cache_middleware_for_testing, "simple_cache"
208+
)
209+
210+
211+
@pytest.mark.asyncio
212+
async def test_async_simple_cache_middleware_populates_cache(async_w3):
213+
async_w3.middleware_onion.inject(
214+
await async_construct_result_generator_middleware(
215+
{
216+
RPCEndpoint("fake_endpoint"): lambda *_: str(uuid.uuid4()),
217+
}
218+
),
219+
"result_generator",
220+
layer=0,
221+
)
222+
223+
result = await async_w3.manager.coro_request("fake_endpoint", [])
224+
225+
_empty_params = await async_w3.manager.coro_request("fake_endpoint", [])
226+
_non_empty_params = await async_w3.manager.coro_request("fake_endpoint", [1])
227+
228+
assert _empty_params == result
229+
assert _non_empty_params != result
230+
231+
# -- teardown -- #
232+
async_w3.middleware_onion.remove("result_generator")
233+
234+
235+
@pytest.mark.asyncio
236+
async def test_async_simple_cache_middleware_does_not_cache_none_responses(async_w3):
237+
counter = itertools.count()
238+
239+
def result_cb(_method, _params):
240+
next(counter)
241+
return None
242+
243+
async_w3.middleware_onion.inject(
244+
await async_construct_result_generator_middleware(
245+
{
246+
RPCEndpoint("fake_endpoint"): result_cb,
247+
},
248+
),
249+
"result_generator",
250+
layer=0,
251+
)
252+
253+
await async_w3.manager.coro_request("fake_endpoint", [])
254+
await async_w3.manager.coro_request("fake_endpoint", [])
255+
256+
assert next(counter) == 2
257+
258+
# -- teardown -- #
259+
async_w3.middleware_onion.remove("result_generator")
260+
261+
262+
@pytest.mark.asyncio
263+
async def test_async_simple_cache_middleware_does_not_cache_error_responses(async_w3):
264+
async_w3.middleware_onion.inject(
265+
await async_construct_error_generator_middleware(
266+
{
267+
RPCEndpoint("fake_endpoint"): lambda *_: f"msg-{uuid.uuid4()}",
268+
}
269+
),
270+
"error_generator",
271+
layer=0,
272+
)
273+
274+
with pytest.raises(ValueError) as err_a:
275+
await async_w3.manager.coro_request("fake_endpoint", [])
276+
with pytest.raises(ValueError) as err_b:
277+
await async_w3.manager.coro_request("fake_endpoint", [])
278+
279+
assert str(err_a) != str(err_b)
280+
281+
# -- teardown -- #
282+
async_w3.middleware_onion.remove("error_generator")
283+
284+
285+
@pytest.mark.asyncio
286+
async def test_async_simple_cache_middleware_does_not_cache_non_whitelist_endpoints(
287+
async_w3,
288+
):
289+
async_w3.middleware_onion.inject(
290+
await async_construct_result_generator_middleware(
291+
{
292+
RPCEndpoint("not_whitelisted"): lambda *_: str(uuid.uuid4()),
293+
}
294+
),
295+
"result_generator",
296+
layer=0,
297+
)
298+
299+
result_a = await async_w3.manager.coro_request("not_whitelisted", [])
300+
result_b = await async_w3.manager.coro_request("not_whitelisted", [])
301+
302+
assert result_a != result_b
303+
304+
# -- teardown -- #
305+
async_w3.middleware_onion.remove("result_generator")

tests/core/utilities/test_request.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
request,
2626
)
2727
from web3._utils.caching import (
28+
SimpleCache,
2829
generate_cache_key,
2930
)
3031
from web3._utils.request import (
31-
SessionCache,
3232
cache_and_return_async_session,
3333
cache_and_return_session,
3434
)
@@ -127,7 +127,7 @@ def test_precached_session(mocker):
127127

128128

129129
def test_cache_session_class():
130-
cache = SessionCache(2)
130+
cache = SimpleCache(2)
131131
evicted_items = cache.cache("1", "Hello1")
132132
assert cache.get_cache_entry("1") == "Hello1"
133133
assert evicted_items is None
@@ -164,7 +164,7 @@ def test_cache_does_not_close_session_before_a_call_when_multithreading():
164164
timeout_default = request.DEFAULT_TIMEOUT
165165

166166
# set cache size to 1 + set future session close thread time to 0.01s
167-
request._session_cache = SessionCache(1)
167+
request._session_cache = SimpleCache(1)
168168
_timeout_for_testing = 0.01
169169
request.DEFAULT_TIMEOUT = _timeout_for_testing
170170

@@ -242,7 +242,7 @@ async def test_async_cache_does_not_close_session_before_a_call_when_multithread
242242
timeout_default = request.DEFAULT_TIMEOUT
243243

244244
# set cache size to 1 + set future session close thread time to 0.01s
245-
request._async_session_cache = SessionCache(1)
245+
request._async_session_cache = SimpleCache(1)
246246
_timeout_for_testing = 0.01
247247
request.DEFAULT_TIMEOUT = _timeout_for_testing
248248

web3/_utils/async_caching.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import asyncio
2+
from concurrent.futures import (
3+
ThreadPoolExecutor,
4+
)
5+
import contextlib
6+
import threading
7+
from typing import (
8+
AsyncGenerator,
9+
)
10+
11+
12+
@contextlib.asynccontextmanager
13+
async def async_lock(
14+
pool: ThreadPoolExecutor, lock: threading.Lock
15+
) -> AsyncGenerator[None, None]:
16+
loop = asyncio.get_event_loop()
17+
await loop.run_in_executor(pool, lock.acquire)
18+
try:
19+
yield
20+
finally:
21+
lock.release()

0 commit comments

Comments
 (0)