Skip to content

Commit 255a9cd

Browse files
authored
Merge pull request #65 from eadwinCode/cache_feat
Cache
2 parents 7bfb304 + d837e6c commit 255a9cd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2756
-28
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[flake8]
22
max-line-length = 88
3-
ignore = E203, E241, E501, W503, F811
3+
ignore = E203, E241, E501, W503, F811, W601
44
exclude =
55
.git,
66
__pycache__

docs/caching.md

Lines changed: 462 additions & 0 deletions
Large diffs are not rendered by default.

ellar/cache/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .interface import ICacheService
2+
from .model import BaseCacheBackend
3+
from .service import CacheService
4+
5+
__all__ = ["CacheService", "ICacheService", "BaseCacheBackend"]

ellar/cache/backends/__init__.py

Whitespace-only changes.

ellar/cache/backends/aio_cache.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import pickle
2+
import typing as t
3+
from abc import ABC
4+
5+
from ellar.helper.event_loop import get_or_create_eventloop
6+
7+
try:
8+
from aiomcache import Client
9+
except ImportError as e: # pragma: no cover
10+
raise RuntimeError(
11+
"To use `AioMemCacheBackend`, you have to install 'aiomcache' package e.g. `pip install aiomcache`"
12+
) from e
13+
14+
15+
from ..interface import IBaseCacheBackendAsync
16+
from ..make_key_decorator import make_key_decorator, make_key_decorator_and_validate
17+
from ..model import BaseCacheBackend
18+
19+
20+
class _AioMemCacheBackendSync(IBaseCacheBackendAsync, ABC):
21+
def _async_executor(self, func: t.Awaitable) -> t.Any:
22+
return get_or_create_eventloop().run_until_complete(func)
23+
24+
def get(self, key: str, version: str = None) -> t.Any:
25+
return self._async_executor(self.get_async(key, version=version))
26+
27+
def delete(self, key: str, version: str = None) -> bool:
28+
res = self._async_executor(self.delete_async(key, version=version))
29+
return bool(res)
30+
31+
def set(
32+
self,
33+
key: str,
34+
value: t.Any,
35+
timeout: t.Union[float, int] = None,
36+
version: str = None,
37+
) -> bool:
38+
res = self._async_executor(
39+
self.set_async(key, value, version=version, timeout=timeout)
40+
)
41+
return bool(res)
42+
43+
def touch(
44+
self, key: str, timeout: t.Union[float, int] = None, version: str = None
45+
) -> bool:
46+
res = self._async_executor(
47+
self.touch_async(key, version=version, timeout=timeout)
48+
)
49+
return bool(res)
50+
51+
52+
class AioMemCacheBackend(_AioMemCacheBackendSync, BaseCacheBackend):
53+
"""Memcached-based cache backend."""
54+
55+
pickle_protocol = pickle.HIGHEST_PROTOCOL
56+
MEMCACHE_CLIENT: t.Type = Client
57+
58+
def __init__(
59+
self,
60+
host: str,
61+
port: int = 11211,
62+
pool_size: int = 2,
63+
pool_minsize: int = None,
64+
serializer: t.Callable = pickle.dumps,
65+
deserializer: t.Callable = pickle.loads,
66+
**kwargs: t.Any
67+
) -> None:
68+
super().__init__(**kwargs)
69+
self._client: Client = None # type: ignore[assignment]
70+
self._client_options = dict(
71+
host=host, port=port, pool_size=pool_size, pool_minsize=pool_minsize
72+
)
73+
self._serializer = serializer
74+
self._deserializer = deserializer
75+
76+
def get_backend_timeout(self, timeout: t.Union[float, int] = None) -> int:
77+
return int(super().get_backend_timeout(timeout))
78+
79+
@property
80+
def _cache_client(self) -> Client:
81+
if self._client is None:
82+
self._client = self.MEMCACHE_CLIENT(**self._client_options)
83+
return self._client
84+
85+
@make_key_decorator
86+
async def get_async(self, key: str, version: str = None) -> t.Optional[t.Any]:
87+
value = await self._cache_client.get(key.encode("utf-8"))
88+
if value:
89+
return self._deserializer(value)
90+
return None # pragma: no cover
91+
92+
@make_key_decorator_and_validate
93+
async def set_async(
94+
self,
95+
key: str,
96+
value: t.Any,
97+
timeout: t.Union[float, int] = None,
98+
version: str = None,
99+
) -> bool:
100+
return await self._cache_client.set(
101+
key.encode("utf-8"),
102+
self._serializer(value, self.pickle_protocol),
103+
exptime=self.get_backend_timeout(timeout),
104+
)
105+
106+
@make_key_decorator
107+
async def delete_async(self, key: str, version: str = None) -> bool:
108+
return await self._cache_client.delete(key.encode("utf-8"))
109+
110+
@make_key_decorator
111+
async def touch_async(
112+
self, key: str, timeout: t.Union[float, int] = None, version: str = None
113+
) -> bool:
114+
return await self._cache_client.touch(
115+
key.encode("utf-8"), exptime=self.get_backend_timeout(timeout)
116+
)

ellar/cache/backends/base.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import typing as t
2+
from abc import ABC
3+
4+
from starlette.concurrency import run_in_threadpool
5+
6+
from ..make_key_decorator import make_key_decorator, make_key_decorator_and_validate
7+
from ..model import BaseCacheBackend
8+
9+
10+
class _BasePylibMemcachedCacheSync(BaseCacheBackend, ABC):
11+
_cache_client: t.Any
12+
13+
@make_key_decorator
14+
def get(self, key: str, version: str = None) -> t.Any:
15+
return self._cache_client.get(key)
16+
17+
@make_key_decorator_and_validate
18+
def set(
19+
self,
20+
key: str,
21+
value: t.Any,
22+
timeout: t.Union[float, int] = None,
23+
version: str = None,
24+
) -> bool:
25+
result = self._cache_client.set(
26+
key, value, int(self.get_backend_timeout(timeout))
27+
)
28+
if not result:
29+
# Make sure the key doesn't keep its old value in case of failure
30+
# to set (memcached's 1MB limit).
31+
self._cache_client.delete(key)
32+
return False
33+
return bool(result)
34+
35+
@make_key_decorator
36+
def delete(self, key: str, version: str = None) -> bool:
37+
result = self._cache_client.delete(key)
38+
return bool(result)
39+
40+
@make_key_decorator
41+
def touch(
42+
self, key: str, timeout: t.Union[float, int] = None, version: str = None
43+
) -> bool:
44+
result = self._cache_client.touch(key, self.get_backend_timeout(timeout))
45+
return bool(result)
46+
47+
def close(self, **kwargs: t.Any) -> None:
48+
# Many clients don't clean up connections properly.
49+
self._cache_client.disconnect_all()
50+
51+
def clear(self) -> None:
52+
self._cache_client.flush_all()
53+
54+
55+
class BasePylibMemcachedCache(_BasePylibMemcachedCacheSync):
56+
MEMCACHE_CLIENT: t.Type
57+
58+
def __init__(self, servers: t.List[str], options: t.Dict = None, **kwargs: t.Any):
59+
super().__init__(**kwargs)
60+
self._servers = servers
61+
62+
self._cache_client_init: t.Any = None
63+
self._options = options or {}
64+
65+
@property
66+
def _cache_client(self) -> t.Any:
67+
"""
68+
Implement transparent thread-safe access to a memcached client.
69+
"""
70+
if self._cache_client_init is None:
71+
self._cache_client_init = self.MEMCACHE_CLIENT(
72+
self._servers, **self._options
73+
)
74+
return self._cache_client_init
75+
76+
async def executor(self, func: t.Callable, *args: t.Any, **kwargs: t.Any) -> t.Any:
77+
return await run_in_threadpool(func, *args, **kwargs)
78+
79+
async def get_async(self, key: str, version: str = None) -> t.Any:
80+
return await self.executor(self.get, key, version=version)
81+
82+
async def set_async(
83+
self,
84+
key: str,
85+
value: t.Any,
86+
timeout: t.Union[float, int] = None,
87+
version: str = None,
88+
) -> bool:
89+
result = await self.executor(
90+
self.set, key, value, timeout=timeout, version=version
91+
)
92+
return bool(result)
93+
94+
async def delete_async(self, key: str, version: str = None) -> bool:
95+
result = await self.executor(self.delete, key, version=version)
96+
return bool(result)
97+
98+
async def touch_async(
99+
self, key: str, timeout: t.Union[float, int] = None, version: str = None
100+
) -> bool:
101+
result = await self.executor(self.touch, key, timeout=timeout, version=version)
102+
return bool(result)
103+
104+
async def close_async(self, **kwargs: t.Any) -> None:
105+
# Many clients don't clean up connections properly.
106+
await self.executor(self._cache_client.disconnect_all)
107+
108+
async def clear_async(self) -> None:
109+
await self.executor(self._cache_client.flush_all)
110+
111+
def validate_key(self, key: str) -> None:
112+
super().validate_key(key)
113+
self._memcache_key_warnings(key)

ellar/cache/backends/local_cache.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import pickle
2+
import time
3+
import typing as t
4+
from abc import ABC
5+
from collections import OrderedDict
6+
7+
from anyio import Lock
8+
9+
from ellar.helper.event_loop import get_or_create_eventloop
10+
11+
from ..interface import IBaseCacheBackendAsync
12+
from ..make_key_decorator import make_key_decorator, make_key_decorator_and_validate
13+
from ..model import BaseCacheBackend
14+
15+
16+
class _LocalMemCacheBackendSync(IBaseCacheBackendAsync, ABC):
17+
def _async_executor(self, func: t.Awaitable) -> t.Any:
18+
return get_or_create_eventloop().run_until_complete(func)
19+
20+
def get(self, key: str, version: str = None) -> t.Any:
21+
return self._async_executor(self.get_async(key, version=version))
22+
23+
def delete(self, key: str, version: str = None) -> bool:
24+
res = self._async_executor(self.delete_async(key, version=version))
25+
return bool(res)
26+
27+
def set(
28+
self,
29+
key: str,
30+
value: t.Any,
31+
timeout: t.Union[float, int] = None,
32+
version: str = None,
33+
) -> bool:
34+
res = self._async_executor(
35+
self.set_async(key, value, timeout=timeout, version=version)
36+
)
37+
return bool(res)
38+
39+
def touch(
40+
self, key: str, timeout: t.Union[float, int] = None, version: str = None
41+
) -> bool:
42+
res = self._async_executor(
43+
self.touch_async(key, timeout=timeout, version=version)
44+
)
45+
return bool(res)
46+
47+
48+
class LocalMemCacheBackend(_LocalMemCacheBackendSync, BaseCacheBackend):
49+
pickle_protocol = pickle.HIGHEST_PROTOCOL
50+
51+
def __init__(self, **kwargs: t.Any) -> None:
52+
super().__init__(**kwargs)
53+
self._cache: t.Dict[str, bytes] = OrderedDict()
54+
self._expire_track: t.Dict[str, float] = {}
55+
self._lock = Lock()
56+
57+
@make_key_decorator
58+
async def get_async(self, key: str, version: str = None) -> t.Any:
59+
async with self._lock:
60+
if self._has_expired(key):
61+
await self._delete(key)
62+
return None
63+
64+
pickled = self._cache[key]
65+
return pickle.loads(pickled)
66+
67+
async def _delete(self, key: str) -> bool:
68+
try:
69+
self._cache.pop(key)
70+
self._expire_track.pop(key)
71+
except KeyError:
72+
return False
73+
return True
74+
75+
@make_key_decorator
76+
async def delete_async(self, key: str, version: str = None) -> bool:
77+
async with self._lock:
78+
return await self._delete(key)
79+
80+
@make_key_decorator_and_validate
81+
async def set_async(
82+
self,
83+
key: str,
84+
value: t.Any,
85+
timeout: t.Union[float, int] = None,
86+
version: str = None,
87+
) -> bool:
88+
async with self._lock:
89+
self._cache[key] = pickle.dumps(value, self.pickle_protocol)
90+
self._expire_track[key] = self.get_backend_timeout(timeout)
91+
return True
92+
93+
def _has_expired(self, key: str) -> bool:
94+
exp = self._expire_track.get(key, -1)
95+
return exp is not None and exp <= time.time()
96+
97+
@make_key_decorator
98+
async def has_key_async(self, key: str, version: str = None) -> bool:
99+
async with self._lock:
100+
if self._has_expired(key):
101+
await self._delete(key)
102+
return False
103+
return True
104+
105+
@make_key_decorator
106+
async def touch_async(
107+
self, key: str, timeout: t.Union[float, int] = None, version: str = None
108+
) -> bool:
109+
async with self._lock:
110+
if self._has_expired(key):
111+
return False
112+
113+
self._expire_track[key] = self.get_backend_timeout(timeout)
114+
return True
115+
116+
def has_key(self, key: str, version: str = None) -> bool:
117+
res = self._async_executor(self.has_key_async(key, version=version))
118+
return bool(res)

ellar/cache/backends/pylib_cache.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
PyLibMCCacheBackend inspired by Django PyLibMCCache
3+
"""
4+
import typing as t
5+
6+
try:
7+
from pylibmc import Client
8+
except ImportError as e: # pragma: no cover
9+
raise RuntimeError(
10+
"To use `PyLibMCCacheBackend`, you have to install 'pylibmc' package e.g. `pip install pylibmc`"
11+
) from e
12+
13+
from .base import BasePylibMemcachedCache
14+
15+
16+
class PyLibMCCacheBackend(BasePylibMemcachedCache):
17+
"""An implementation of a cache binding using pylibmc"""
18+
19+
MEMCACHE_CLIENT = Client
20+
21+
def __init__(self, servers: t.List[str], options: t.Dict = None, **kwargs: t.Any):
22+
super().__init__(servers, options=options, **kwargs)
23+
24+
async def close_async(self, **kwargs: t.Any) -> None:
25+
# libmemcached manages its own connections. Don't call disconnect_all()
26+
# as it resets the failover state and creates unnecessary reconnects.
27+
return None
28+
29+
def close(self, **kwargs: t.Any) -> None:
30+
return None

0 commit comments

Comments
 (0)