Skip to content

Commit

Permalink
Remove dictionary cache support for simple-cache-middleware
Browse files Browse the repository at this point in the history
- Remove dictionary cache support altogether, for simple cache middleware, in favor of using the ``SimpleCache`` class.
- Handle missing keys in ``SimpleCache`` a bit more gracefully than throwing a ``KeyError``, just return ``None`` if the key is not in the cache.
  • Loading branch information
fselmo committed Oct 31, 2022
1 parent 3f20e8c commit 98a9b42
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 161 deletions.
12 changes: 11 additions & 1 deletion docs/web3.utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Utils

.. py:module:: web3.utils
The ``utils`` module houses public utility and helper functions.
The ``utils`` module houses public utility functions and classes.

ABI
---
Expand All @@ -18,6 +18,16 @@ ABI
Return the ``output`` names an ABI function or event.


Caching
-------

.. py:class:: Utils.SimpleCache
The main internal cache class being used internally by web3.py. In some cases, it
may prove useful to set your own cache size and pass in your own instance of this
class where supported.


Exception Handling
------------------

Expand Down
59 changes: 12 additions & 47 deletions tests/core/middleware/test_simple_cache_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from web3 import Web3
from web3._utils.caching import (
SimpleCache,
generate_cache_key,
)
from web3.middleware import (
Expand All @@ -29,6 +28,9 @@
from web3.types import (
RPCEndpoint,
)
from web3.utils.caching import (
SimpleCache,
)


@pytest.fixture
Expand All @@ -52,17 +54,7 @@ def w3(w3_base, result_generator_middleware):
return w3_base


def dict_cache_class_return_value_a():
# test dictionary-based cache
return {
generate_cache_key(f"{threading.get_ident()}:{('fake_endpoint', [1])}"): {
"result": "value-a"
},
}


def simple_cache_class_return_value_a():
# test `SimpleCache` class cache
def simple_cache_return_value_a():
_cache = SimpleCache()
_cache.cache(
generate_cache_key(f"{threading.get_ident()}:{('fake_endpoint', [1])}"),
Expand All @@ -71,30 +63,20 @@ def simple_cache_class_return_value_a():
return _cache


@pytest.mark.parametrize(
"cache_class",
(
dict_cache_class_return_value_a,
simple_cache_class_return_value_a,
),
)
def test_simple_cache_middleware_pulls_from_cache(w3, cache_class):

def test_simple_cache_middleware_pulls_from_cache(w3):
w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=cache_class,
cache=simple_cache_return_value_a(),
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)

assert w3.manager.request_blocking("fake_endpoint", [1]) == "value-a"


@pytest.mark.parametrize("cache_class", (dict, SimpleCache))
def test_simple_cache_middleware_populates_cache(w3, cache_class):
def test_simple_cache_middleware_populates_cache(w3):
w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=cache_class,
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)
Expand All @@ -105,8 +87,7 @@ def test_simple_cache_middleware_populates_cache(w3, cache_class):
assert w3.manager.request_blocking("fake_endpoint", [1]) != result


@pytest.mark.parametrize("cache_class", (dict, SimpleCache))
def test_simple_cache_middleware_does_not_cache_none_responses(w3_base, cache_class):
def test_simple_cache_middleware_does_not_cache_none_responses(w3_base):
counter = itertools.count()
w3 = w3_base

Expand All @@ -124,7 +105,6 @@ def result_cb(_method, _params):

w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=cache_class,
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)
Expand All @@ -135,8 +115,7 @@ def result_cb(_method, _params):
assert next(counter) == 2


@pytest.mark.parametrize("cache_class", (dict, SimpleCache))
def test_simple_cache_middleware_does_not_cache_error_responses(w3_base, cache_class):
def test_simple_cache_middleware_does_not_cache_error_responses(w3_base):
w3 = w3_base
w3.middleware_onion.add(
construct_error_generator_middleware(
Expand All @@ -148,7 +127,6 @@ def test_simple_cache_middleware_does_not_cache_error_responses(w3_base, cache_c

w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=cache_class,
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)
Expand All @@ -161,14 +139,9 @@ def test_simple_cache_middleware_does_not_cache_error_responses(w3_base, cache_c
assert str(err_a) != str(err_b)


@pytest.mark.parametrize("cache_class", (dict, SimpleCache))
def test_simple_cache_middleware_does_not_cache_endpoints_not_in_whitelist(
w3,
cache_class,
):
def test_simple_cache_middleware_does_not_cache_endpoints_not_in_whitelist(w3):
w3.middleware_onion.add(
construct_simple_cache_middleware(
cache_class=cache_class,
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
)
Expand All @@ -184,7 +157,6 @@ def test_simple_cache_middleware_does_not_cache_endpoints_not_in_whitelist(

async def _async_simple_cache_middleware_for_testing(make_request, async_w3):
middleware = await async_construct_simple_cache_middleware(
cache_class=SimpleCache,
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
return await middleware(make_request, async_w3)
Expand All @@ -201,17 +173,10 @@ def async_w3():


@pytest.mark.asyncio
@pytest.mark.parametrize(
"cache_class",
(
dict_cache_class_return_value_a,
simple_cache_class_return_value_a,
),
)
async def test_async_simple_cache_middleware_pulls_from_cache(async_w3, cache_class):
async def test_async_simple_cache_middleware_pulls_from_cache(async_w3):
async def _properly_awaited_middleware(make_request, _async_w3):
middleware = await async_construct_simple_cache_middleware(
cache_class=cache_class,
cache=simple_cache_return_value_a(),
rpc_whitelist={RPCEndpoint("fake_endpoint")},
)
return await middleware(make_request, _async_w3)
Expand Down
11 changes: 6 additions & 5 deletions tests/core/utilities/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
request,
)
from web3._utils.caching import (
SimpleCache,
generate_cache_key,
)
from web3._utils.request import (
cache_and_return_async_session,
cache_and_return_session,
)
from web3.utils.caching import (
SimpleCache,
)


class MockedResponse:
Expand Down Expand Up @@ -149,10 +151,9 @@ def test_cache_session_class():
assert "1" not in cache
assert "1" in evicted_items

with pytest.raises(KeyError):
# This should throw a KeyError since the cache size was 2 and 3 were inserted
# the first inserted cached item was removed and returned in evicted items
cache.get_cache_entry("1")
# Cache size is `3`. We should have "2" and "3" in the cache and "1" should have
# been evicted.
assert cache.get_cache_entry("1") is None

# clear cache
request._session_cache.clear()
Expand Down
68 changes: 0 additions & 68 deletions web3/_utils/caching.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import collections
from collections import (
OrderedDict,
)
import hashlib
from typing import (
Any,
Dict,
Union,
)

from eth_utils import (
Expand Down Expand Up @@ -39,66 +34,3 @@ def generate_cache_key(value: Any) -> str:
raise TypeError(
f"Cannot generate cache key for value {value} of type {type(value)}"
)


class SimpleCache:
def __init__(self, size: int = 100):
self._size = size
self._data: OrderedDict[str, Any] = OrderedDict()

def cache(self, key: str, value: Any) -> Dict[str, Any]:
evicted_items = None
# If the key is already in the OrderedDict just update it
# and don't evict any values. Ideally, we could still check to see
# if there are too many items in the OrderedDict but that may rearrange
# the order it should be unlikely that the size could grow over the limit
if key not in self._data:
while len(self._data) >= self._size:
if evicted_items is None:
evicted_items = {}
k, v = self._data.popitem(last=False)
evicted_items[k] = v
self._data[key] = value
return evicted_items

def get_cache_entry(self, key: str) -> Any:
return self._data[key]

def clear(self) -> None:
self._data.clear()

def items(self) -> Dict[str, Any]:
return self._data

def __contains__(self, item: str) -> bool:
return item in self._data

def __len__(self) -> int:
return len(self._data)


def type_aware_cache_entry(
cache: Union[Dict[str, Any], SimpleCache], cache_key: str, value: Any
) -> None:
"""
Commit an entry to a dictionary-based cache or to a `SimpleCache` class.
"""
if isinstance(cache, SimpleCache):
cache.cache(cache_key, value)
else:
cache[cache_key] = value


def type_aware_get_cache_entry(
cache: Union[Dict[str, Any], SimpleCache],
cache_key: str,
) -> Any:
"""
Get a cache entry, with `cache_key`, from a dictionary-based cache or a
`SimpleCache` class.
"""
if isinstance(cache, SimpleCache) and cache_key in cache.items():
return cache.get_cache_entry(cache_key)
elif isinstance(cache, dict) and cache_key in cache:
return cache[cache_key]
return None
8 changes: 5 additions & 3 deletions web3/_utils/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
async_lock,
)
from web3._utils.caching import (
SimpleCache,
generate_cache_key,
)
from web3.utils.caching import (
SimpleCache,
)

logger = logging.getLogger(__name__)

Expand All @@ -40,7 +42,7 @@ def get_default_http_endpoint() -> URI:
return URI(os.environ.get("WEB3_HTTP_PROVIDER_URI", "http://localhost:8545"))


_session_cache = SimpleCache(size=100)
_session_cache = SimpleCache()
_session_cache_lock = threading.Lock()


Expand Down Expand Up @@ -111,7 +113,7 @@ def _close_evicted_sessions(evicted_sessions: List[requests.Session]) -> None:
# --- async --- #


_async_session_cache = SimpleCache(size=100)
_async_session_cache = SimpleCache()
_async_session_cache_lock = threading.Lock()
_async_session_pool = ThreadPoolExecutor(max_workers=1)

Expand Down
Loading

0 comments on commit 98a9b42

Please sign in to comment.