Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make SimpleMemoryBackend store state in instance instead of class #562

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 28 additions & 29 deletions aiocache/backends/memory.py
Expand Up @@ -10,32 +10,32 @@ class SimpleMemoryBackend(BaseCache):
Wrapper around dict operations to use it as a cache backend
"""

_cache: Dict[str, object] = {}
_handlers: Dict[str, asyncio.TimerHandle] = {}

def __init__(self, **kwargs):
super().__init__(**kwargs)

self._cache: Dict[str, object] = {}
self._handlers: Dict[str, asyncio.TimerHandle] = {}

async def _get(self, key, encoding="utf-8", _conn=None):
return SimpleMemoryBackend._cache.get(key)
return self._cache.get(key)

async def _gets(self, key, encoding="utf-8", _conn=None):
return await self._get(key, encoding=encoding, _conn=_conn)

async def _multi_get(self, keys, encoding="utf-8", _conn=None):
return [SimpleMemoryBackend._cache.get(key) for key in keys]
return [self._cache.get(key) for key in keys]

async def _set(self, key, value, ttl=None, _cas_token=None, _conn=None):
if _cas_token is not None and _cas_token != SimpleMemoryBackend._cache.get(key):
if _cas_token is not None and _cas_token != self._cache.get(key):
return 0

if key in SimpleMemoryBackend._handlers:
SimpleMemoryBackend._handlers[key].cancel()
if key in self._handlers:
self._handlers[key].cancel()

SimpleMemoryBackend._cache[key] = value
self._cache[key] = value
if ttl:
loop = asyncio.get_running_loop()
SimpleMemoryBackend._handlers[key] = loop.call_later(ttl, self.__delete, key)
self._handlers[key] = loop.call_later(ttl, self.__delete, key)
return True

async def _multi_set(self, pairs, ttl=None, _conn=None):
Expand All @@ -44,33 +44,33 @@ async def _multi_set(self, pairs, ttl=None, _conn=None):
return True

async def _add(self, key, value, ttl=None, _conn=None):
if key in SimpleMemoryBackend._cache:
if key in self._cache:
raise ValueError("Key {} already exists, use .set to update the value".format(key))

await self._set(key, value, ttl=ttl)
return True

async def _exists(self, key, _conn=None):
return key in SimpleMemoryBackend._cache
return key in self._cache

async def _increment(self, key, delta, _conn=None):
if key not in SimpleMemoryBackend._cache:
SimpleMemoryBackend._cache[key] = delta
if key not in self._cache:
self._cache[key] = delta
else:
try:
SimpleMemoryBackend._cache[key] = int(SimpleMemoryBackend._cache[key]) + delta
self._cache[key] = int(self._cache[key]) + delta
except ValueError:
raise TypeError("Value is not an integer") from None
return SimpleMemoryBackend._cache[key]
return self._cache[key]

async def _expire(self, key, ttl, _conn=None):
if key in SimpleMemoryBackend._cache:
handle = SimpleMemoryBackend._handlers.pop(key, None)
if key in self._cache:
handle = self._handlers.pop(key, None)
if handle:
handle.cancel()
if ttl:
loop = asyncio.get_running_loop()
SimpleMemoryBackend._handlers[key] = loop.call_later(ttl, self.__delete, key)
self._handlers[key] = loop.call_later(ttl, self.__delete, key)
return True

return False
Expand All @@ -80,27 +80,26 @@ async def _delete(self, key, _conn=None):

async def _clear(self, namespace=None, _conn=None):
if namespace:
for key in list(SimpleMemoryBackend._cache):
for key in list(self._cache):
if key.startswith(namespace):
self.__delete(key)
else:
SimpleMemoryBackend._cache = {}
SimpleMemoryBackend._handlers = {}
self._cache = {}
self._handlers = {}
return True

async def _raw(self, command, *args, encoding="utf-8", _conn=None, **kwargs):
return getattr(SimpleMemoryBackend._cache, command)(*args, **kwargs)
return getattr(self._cache, command)(*args, **kwargs)

async def _redlock_release(self, key, value):
if SimpleMemoryBackend._cache.get(key) == value:
SimpleMemoryBackend._cache.pop(key)
if self._cache.get(key) == value:
self._cache.pop(key)
return 1
return 0

@classmethod
def __delete(cls, key):
if cls._cache.pop(key, None) is not None:
handle = cls._handlers.pop(key, None)
def __delete(self, key):
if self._cache.pop(key, None) is not None:
handle = self._handlers.pop(key, None)
if handle:
handle.cancel()
return 1
Expand Down
7 changes: 3 additions & 4 deletions tests/acceptance/test_plugins.py
@@ -1,6 +1,5 @@
import pytest

from aiocache.backends.memory import SimpleMemoryBackend
from aiocache.plugins import HitMissRatioPlugin, TimingPlugin


Expand All @@ -17,7 +16,7 @@ class TestHitMissRatioPlugin:
async def test_get_hit_miss_ratio(self, memory_cache, data, ratio):
keys = ["a", "b", "c", "d", "e", "f"]
memory_cache.plugins = [HitMissRatioPlugin()]
SimpleMemoryBackend._cache = data
memory_cache._cache = data

for key in keys:
await memory_cache.get(key)
Expand All @@ -41,7 +40,7 @@ async def test_get_hit_miss_ratio(self, memory_cache, data, ratio):
async def test_multi_get_hit_miss_ratio(self, memory_cache, data, ratio):
keys = ["a", "b", "c", "d", "e", "f"]
memory_cache.plugins = [HitMissRatioPlugin()]
SimpleMemoryBackend._cache = data
memory_cache._cache = data

for key in keys:
await memory_cache.multi_get([key])
Expand Down Expand Up @@ -76,7 +75,7 @@ class TestTimingPlugin:
async def test_get_avg_min_max(self, memory_cache, data, ratio):
keys = ["a", "b", "c", "d", "e", "f"]
memory_cache.plugins = [TimingPlugin()]
SimpleMemoryBackend._cache = data
memory_cache._cache = data

for key in keys:
await memory_cache.get(key)
Expand Down
99 changes: 49 additions & 50 deletions tests/ut/backends/test_memory.py
Expand Up @@ -11,16 +11,15 @@

@pytest.fixture
def memory(mocker):
SimpleMemoryBackend._handlers = {}
SimpleMemoryBackend._cache = {}
mocker.spy(SimpleMemoryBackend, "_cache")
return SimpleMemoryBackend()
memory = SimpleMemoryBackend()
mocker.spy(memory, "_cache")
return memory


class TestSimpleMemoryBackend:
async def test_get(self, memory):
await memory._get(Keys.KEY)
SimpleMemoryBackend._cache.get.assert_called_with(Keys.KEY)
memory._cache.get.assert_called_with(Keys.KEY)

async def test_gets(self, mocker, memory):
mocker.spy(memory, "_get")
Expand All @@ -29,7 +28,7 @@ async def test_gets(self, mocker, memory):

async def test_set(self, memory):
await memory._set(Keys.KEY, "value")
SimpleMemoryBackend._cache.__setitem__.assert_called_with(Keys.KEY, "value")
memory._cache.__setitem__.assert_called_with(Keys.KEY, "value")

async def test_set_no_ttl_no_handle(self, memory):
await memory._set(Keys.KEY, "value", ttl=0)
Expand All @@ -54,89 +53,89 @@ async def test_set_ttl_handle(self, memory):
async def test_set_cas_token(self, memory):
memory._cache.get.return_value = "old_value"
assert await memory._set(Keys.KEY, "value", _cas_token="old_value") == 1
SimpleMemoryBackend._cache.__setitem__.assert_called_with(Keys.KEY, "value")
memory._cache.__setitem__.assert_called_with(Keys.KEY, "value")

async def test_set_cas_fail(self, memory):
memory._cache.get.return_value = "value"
assert await memory._set(Keys.KEY, "value", _cas_token="old_value") == 0
assert SimpleMemoryBackend._cache.__setitem__.call_count == 0
assert memory._cache.__setitem__.call_count == 0

async def test_multi_get(self, memory):
await memory._multi_get([Keys.KEY, Keys.KEY_1])
SimpleMemoryBackend._cache.get.assert_any_call(Keys.KEY)
SimpleMemoryBackend._cache.get.assert_any_call(Keys.KEY_1)
memory._cache.get.assert_any_call(Keys.KEY)
memory._cache.get.assert_any_call(Keys.KEY_1)

async def test_multi_set(self, memory):
await memory._multi_set([(Keys.KEY, "value"), (Keys.KEY_1, "random")])
SimpleMemoryBackend._cache.__setitem__.assert_any_call(Keys.KEY, "value")
SimpleMemoryBackend._cache.__setitem__.assert_any_call(Keys.KEY_1, "random")
memory._cache.__setitem__.assert_any_call(Keys.KEY, "value")
memory._cache.__setitem__.assert_any_call(Keys.KEY_1, "random")

async def test_add(self, memory, mocker):
mocker.spy(memory, "_set")
await memory._add(Keys.KEY, "value")
memory._set.assert_called_with(Keys.KEY, "value", ttl=None)

async def test_add_existing(self, memory):
SimpleMemoryBackend._cache.__contains__.return_value = True
memory._cache.__contains__.return_value = True
with pytest.raises(ValueError):
await memory._add(Keys.KEY, "value")

async def test_exists(self, memory):
await memory._exists(Keys.KEY)
SimpleMemoryBackend._cache.__contains__.assert_called_with(Keys.KEY)
memory._cache.__contains__.assert_called_with(Keys.KEY)

async def test_increment(self, memory):
await memory._increment(Keys.KEY, 2)
SimpleMemoryBackend._cache.__contains__.assert_called_with(Keys.KEY)
SimpleMemoryBackend._cache.__setitem__.assert_called_with(Keys.KEY, 2)
memory._cache.__contains__.assert_called_with(Keys.KEY)
memory._cache.__setitem__.assert_called_with(Keys.KEY, 2)

async def test_increment_missing(self, memory):
SimpleMemoryBackend._cache.__contains__.return_value = True
SimpleMemoryBackend._cache.__getitem__.return_value = 2
memory._cache.__contains__.return_value = True
memory._cache.__getitem__.return_value = 2
await memory._increment(Keys.KEY, 2)
SimpleMemoryBackend._cache.__getitem__.assert_called_with(Keys.KEY)
SimpleMemoryBackend._cache.__setitem__.assert_called_with(Keys.KEY, 4)
memory._cache.__getitem__.assert_called_with(Keys.KEY)
memory._cache.__setitem__.assert_called_with(Keys.KEY, 4)

async def test_increment_typerror(self, memory):
SimpleMemoryBackend._cache.__contains__.return_value = True
SimpleMemoryBackend._cache.__getitem__.return_value = "asd"
memory._cache.__contains__.return_value = True
memory._cache.__getitem__.return_value = "asd"
with pytest.raises(TypeError):
await memory._increment(Keys.KEY, 2)

async def test_expire_no_handle_no_ttl(self, memory):
SimpleMemoryBackend._cache.__contains__.return_value = True
memory._cache.__contains__.return_value = True
await memory._expire(Keys.KEY, 0)
assert memory._handlers.get(Keys.KEY) is None

async def test_expire_no_handle_ttl(self, memory):
SimpleMemoryBackend._cache.__contains__.return_value = True
memory._cache.__contains__.return_value = True
await memory._expire(Keys.KEY, 1)
assert isinstance(memory._handlers.get(Keys.KEY), asyncio.Handle)

async def test_expire_handle_ttl(self, memory):
fake = create_autospec(asyncio.TimerHandle, instance=True)
SimpleMemoryBackend._handlers[Keys.KEY] = fake
SimpleMemoryBackend._cache.__contains__.return_value = True
memory._handlers[Keys.KEY] = fake
memory._cache.__contains__.return_value = True
await memory._expire(Keys.KEY, 1)
assert fake.cancel.call_count == 1
assert isinstance(memory._handlers.get(Keys.KEY), asyncio.Handle)

async def test_expire_missing(self, memory):
SimpleMemoryBackend._cache.__contains__.return_value = False
memory._cache.__contains__.return_value = False
assert await memory._expire(Keys.KEY, 1) is False

async def test_delete(self, memory):
fake = create_autospec(asyncio.TimerHandle, instance=True)
SimpleMemoryBackend._handlers[Keys.KEY] = fake
memory._handlers[Keys.KEY] = fake
await memory._delete(Keys.KEY)
assert fake.cancel.call_count == 1
assert Keys.KEY not in SimpleMemoryBackend._handlers
SimpleMemoryBackend._cache.pop.assert_called_with(Keys.KEY, None)
assert Keys.KEY not in memory._handlers
memory._cache.pop.assert_called_with(Keys.KEY, None)

async def test_delete_missing(self, memory):
SimpleMemoryBackend._cache.pop.return_value = None
memory._cache.pop.return_value = None
await memory._delete(Keys.KEY)
SimpleMemoryBackend._cache.pop.assert_called_with(Keys.KEY, None)
memory._cache.pop.assert_called_with(Keys.KEY, None)

async def test_delete_non_truthy(self, memory):
non_truthy = MagicMock(spec_set=("__bool__",))
Expand All @@ -145,44 +144,44 @@ async def test_delete_non_truthy(self, memory):
with pytest.raises(ValueError):
bool(non_truthy)

SimpleMemoryBackend._cache.pop.return_value = non_truthy
memory._cache.pop.return_value = non_truthy
await memory._delete(Keys.KEY)

assert non_truthy.__bool__.call_count == 1
SimpleMemoryBackend._cache.pop.assert_called_with(Keys.KEY, None)
memory._cache.pop.assert_called_with(Keys.KEY, None)

async def test_clear_namespace(self, memory):
SimpleMemoryBackend._cache.__iter__.return_value = iter(["nma", "nmb", "no"])
memory._cache.__iter__.return_value = iter(["nma", "nmb", "no"])
await memory._clear("nm")
assert SimpleMemoryBackend._cache.pop.call_count == 2
SimpleMemoryBackend._cache.pop.assert_any_call("nma", None)
SimpleMemoryBackend._cache.pop.assert_any_call("nmb", None)
assert memory._cache.pop.call_count == 2
memory._cache.pop.assert_any_call("nma", None)
memory._cache.pop.assert_any_call("nmb", None)

async def test_clear_no_namespace(self, memory):
SimpleMemoryBackend._handlers = "asdad"
SimpleMemoryBackend._cache = "asdad"
memory._handlers = "asdad"
memory._cache = "asdad"
await memory._clear()
SimpleMemoryBackend._handlers = {}
SimpleMemoryBackend._cache = {}
memory._handlers = {}
memory._cache = {}

async def test_raw(self, memory):
await memory._raw("get", Keys.KEY)
SimpleMemoryBackend._cache.get.assert_called_with(Keys.KEY)
memory._cache.get.assert_called_with(Keys.KEY)

await memory._set(Keys.KEY, "value")
SimpleMemoryBackend._cache.__setitem__.assert_called_with(Keys.KEY, "value")
memory._cache.__setitem__.assert_called_with(Keys.KEY, "value")

async def test_redlock_release(self, memory):
SimpleMemoryBackend._cache.get.return_value = "lock"
memory._cache.get.return_value = "lock"
assert await memory._redlock_release(Keys.KEY, "lock") == 1
SimpleMemoryBackend._cache.get.assert_called_with(Keys.KEY)
SimpleMemoryBackend._cache.pop.assert_called_with(Keys.KEY)
memory._cache.get.assert_called_with(Keys.KEY)
memory._cache.pop.assert_called_with(Keys.KEY)

async def test_redlock_release_nokey(self, memory):
SimpleMemoryBackend._cache.get.return_value = None
memory._cache.get.return_value = None
assert await memory._redlock_release(Keys.KEY, "lock") == 0
SimpleMemoryBackend._cache.get.assert_called_with(Keys.KEY)
assert SimpleMemoryBackend._cache.pop.call_count == 0
memory._cache.get.assert_called_with(Keys.KEY)
assert memory._cache.pop.call_count == 0


class TestSimpleMemoryCache:
Expand Down