Skip to content

Commit

Permalink
Add support for async_simple_cache_middleware
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
fselmo committed Oct 26, 2022
1 parent 2f7b627 commit 7bfc271
Show file tree
Hide file tree
Showing 10 changed files with 418 additions and 210 deletions.
3 changes: 2 additions & 1 deletion docs/providers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -484,5 +484,6 @@ Supported Middleware
- :meth:`Gas Price Strategy <web3.middleware.gas_price_strategy_middleware>`
- :meth:`Buffered Gas Estimate Middleware <web3.middleware.buffered_gas_estimate_middleware>`
- :meth:`Stalecheck Middleware <web3.middleware.make_stalecheck_middleware>`
- :meth:`Validation middleware <web3.middleware.validation>`
- :meth:`Validation Middleware <web3.middleware.validation>`
- :ref:`Geth POA Middleware <geth-poa>`
- :meth:`Simple Cache Middleware <web3.middleware.simple_cache_middleware>`
1 change: 1 addition & 0 deletions newsfragments/2579.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +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``.
192 changes: 181 additions & 11 deletions tests/core/middleware/test_simple_cache_middleware.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import itertools
import pytest
import threading
import uuid

from web3 import Web3
Expand All @@ -11,9 +12,22 @@
construct_result_generator_middleware,
construct_simple_cache_middleware,
)
from web3.middleware.async_cache import (
async_construct_simple_cache_middleware,
)
from web3.middleware.fixture import (
async_construct_error_generator_middleware,
async_construct_result_generator_middleware,
)
from web3.providers.base import (
BaseProvider,
)
from web3.providers.eth_tester import (
AsyncEthereumTesterProvider,
)
from web3.types import (
RPCEndpoint,
)


@pytest.fixture
Expand All @@ -25,8 +39,8 @@ def w3_base():
def result_generator_middleware():
return construct_result_generator_middleware(
{
"fake_endpoint": lambda *_: str(uuid.uuid4()),
"not_whitelisted": lambda *_: str(uuid.uuid4()),
RPCEndpoint("fake_endpoint"): lambda *_: str(uuid.uuid4()),
RPCEndpoint("not_whitelisted"): lambda *_: str(uuid.uuid4()),
}
)

Expand All @@ -40,13 +54,15 @@ def w3(w3_base, result_generator_middleware):
def test_simple_cache_middleware_pulls_from_cache(w3):
def cache_class():
return {
generate_cache_key(("fake_endpoint", [1])): {"result": "value-a"},
generate_cache_key(f"{threading.get_ident()}:{('fake_endpoint', [1])}"): {
"result": "value-a"
},
}

w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=cache_class,
rpc_whitelist={"fake_endpoint"},
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)

Expand All @@ -57,7 +73,7 @@ def test_simple_cache_middleware_populates_cache(w3):
w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=dict,
rpc_whitelist={"fake_endpoint"},
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)

Expand All @@ -71,22 +87,22 @@ def test_simple_cache_middleware_does_not_cache_none_responses(w3_base):
counter = itertools.count()
w3 = w3_base

def result_cb(method, params):
def result_cb(_method, _params):
next(counter)
return None

w3.middleware_onion.add(
construct_result_generator_middleware(
{
"fake_endpoint": result_cb,
RPCEndpoint("fake_endpoint"): result_cb,
}
)
)

w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=dict,
rpc_whitelist={"fake_endpoint"},
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)

Expand All @@ -101,15 +117,15 @@ def test_simple_cache_middleware_does_not_cache_error_responses(w3_base):
w3.middleware_onion.add(
construct_error_generator_middleware(
{
"fake_endpoint": lambda *_: f"msg-{uuid.uuid4()}",
RPCEndpoint("fake_endpoint"): lambda *_: f"msg-{uuid.uuid4()}",
}
)
)

w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=dict,
rpc_whitelist={"fake_endpoint"},
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)

Expand All @@ -125,11 +141,165 @@ def test_simple_cache_middleware_does_not_cache_endpoints_not_in_whitelist(w3):
w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=dict,
rpc_whitelist={"fake_endpoint"},
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)

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

assert result_a != result_b


# -- async -- #


async def _async_simple_cache_middleware_for_testing(make_request, async_w3):
middleware = await async_construct_simple_cache_middleware(
cache_class=dict,
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
return await middleware(make_request, async_w3)


@pytest.fixture
def async_w3():
return Web3(
provider=AsyncEthereumTesterProvider(),
middlewares=[
(_async_simple_cache_middleware_for_testing, "simple_cache"),
],
)


@pytest.mark.asyncio
async def test_async_simple_cache_middleware_pulls_from_cache(async_w3):
# remove the pre-loaded simple cache middleware to replace with test-specific:
async_w3.middleware_onion.remove("simple_cache")

def cache_class():
return {
generate_cache_key(f"{threading.get_ident()}:{('fake_endpoint', [1])}"): {
"result": "value-a"
},
}

async def _properly_awaited_middleware(make_request, _async_w3):
middleware = await async_construct_simple_cache_middleware(
cache_class=cache_class,
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
return await middleware(make_request, _async_w3)

async_w3.middleware_onion.inject(
_properly_awaited_middleware,
"for_this_test_only",
layer=0,
)

_result = await async_w3.manager.coro_request("fake_endpoint", [1])
assert _result == "value-a"

# -- teardown -- #
async_w3.middleware_onion.remove("for_this_test_only")
# add back the pre-loaded simple cache middleware:
async_w3.middleware_onion.add(
_async_simple_cache_middleware_for_testing, "simple_cache"
)


@pytest.mark.asyncio
async def test_async_simple_cache_middleware_populates_cache(async_w3):
async_w3.middleware_onion.inject(
await async_construct_result_generator_middleware(
{
RPCEndpoint("fake_endpoint"): lambda *_: str(uuid.uuid4()),
}
),
"result_generator",
layer=0,
)

result = await async_w3.manager.coro_request("fake_endpoint", [])

_empty_params = await async_w3.manager.coro_request("fake_endpoint", [])
_non_empty_params = await async_w3.manager.coro_request("fake_endpoint", [1])

assert _empty_params == result
assert _non_empty_params != result

# -- teardown -- #
async_w3.middleware_onion.remove("result_generator")


@pytest.mark.asyncio
async def test_async_simple_cache_middleware_does_not_cache_none_responses(async_w3):
counter = itertools.count()

def result_cb(_method, _params):
next(counter)
return None

async_w3.middleware_onion.inject(
await async_construct_result_generator_middleware(
{
RPCEndpoint("fake_endpoint"): result_cb,
},
),
"result_generator",
layer=0,
)

await async_w3.manager.coro_request("fake_endpoint", [])
await async_w3.manager.coro_request("fake_endpoint", [])

assert next(counter) == 2

# -- teardown -- #
async_w3.middleware_onion.remove("result_generator")


@pytest.mark.asyncio
async def test_async_simple_cache_middleware_does_not_cache_error_responses(async_w3):
async_w3.middleware_onion.inject(
await async_construct_error_generator_middleware(
{
RPCEndpoint("fake_endpoint"): lambda *_: f"msg-{uuid.uuid4()}",
}
),
"error_generator",
layer=0,
)

with pytest.raises(ValueError) as err_a:
await async_w3.manager.coro_request("fake_endpoint", [])
with pytest.raises(ValueError) as err_b:
await async_w3.manager.coro_request("fake_endpoint", [])

assert str(err_a) != str(err_b)

# -- teardown -- #
async_w3.middleware_onion.remove("error_generator")


@pytest.mark.asyncio
async def test_async_simple_cache_middleware_does_not_cache_non_whitelist_endpoints(
async_w3,
):
async_w3.middleware_onion.inject(
await async_construct_result_generator_middleware(
{
RPCEndpoint("not_whitelisted"): lambda *_: str(uuid.uuid4()),
}
),
"result_generator",
layer=0,
)

result_a = await async_w3.manager.coro_request("not_whitelisted", [])
result_b = await async_w3.manager.coro_request("not_whitelisted", [])

assert result_a != result_b

# -- teardown -- #
async_w3.middleware_onion.remove("result_generator")
8 changes: 4 additions & 4 deletions tests/core/utilities/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
request,
)
from web3._utils.caching import (
SimpleCache,
generate_cache_key,
)
from web3._utils.request import (
SessionCache,
cache_and_return_async_session,
cache_and_return_session,
)
Expand Down Expand Up @@ -127,7 +127,7 @@ def test_precached_session(mocker):


def test_cache_session_class():
cache = SessionCache(2)
cache = SimpleCache(2)
evicted_items = cache.cache("1", "Hello1")
assert cache.get_cache_entry("1") == "Hello1"
assert evicted_items is None
Expand Down Expand Up @@ -164,7 +164,7 @@ def test_cache_does_not_close_session_before_a_call_when_multithreading():
timeout_default = request.DEFAULT_TIMEOUT

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

Expand Down Expand Up @@ -242,7 +242,7 @@ async def test_async_cache_does_not_close_session_before_a_call_when_multithread
timeout_default = request.DEFAULT_TIMEOUT

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

Expand Down
21 changes: 21 additions & 0 deletions web3/_utils/async_caching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import asyncio
from concurrent.futures import (
ThreadPoolExecutor,
)
import contextlib
import threading
from typing import (
AsyncGenerator,
)


@contextlib.asynccontextmanager
async def async_lock(
pool: ThreadPoolExecutor, lock: threading.Lock
) -> AsyncGenerator[None, None]:
loop = asyncio.get_event_loop()
await loop.run_in_executor(pool, lock.acquire)
try:
yield
finally:
lock.release()
Loading

0 comments on commit 7bfc271

Please sign in to comment.