Skip to content

Commit cd34861

Browse files
committed
Refactored simple cache and redis
1 parent 63ec390 commit cd34861

File tree

8 files changed

+111
-58
lines changed

8 files changed

+111
-58
lines changed

ellar/cache/backends/aio_cache.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _cache_client(self) -> Client:
8383

8484
@make_key_decorator
8585
async def get_async(self, key: str, version: str = None) -> t.Optional[t.Any]:
86-
value = await self._cache_client.get(key=key.encode("utf-8"))
86+
value = await self._cache_client.get(key.encode("utf-8"))
8787
if value:
8888
return self._deserializer(value)
8989
return None # pragma: no cover
@@ -104,12 +104,12 @@ async def set_async(
104104

105105
@make_key_decorator
106106
async def delete_async(self, key: str, version: str = None) -> bool:
107-
return await self._cache_client.delete(key=key.encode("utf-8"))
107+
return await self._cache_client.delete(key.encode("utf-8"))
108108

109109
@make_key_decorator
110110
async def touch_async(
111111
self, key: str, timeout: t.Union[float, int] = None, version: str = None
112112
) -> bool:
113113
return await self._cache_client.touch(
114-
key=key.encode("utf-8"), exptime=self.get_backend_timeout(timeout)
114+
key.encode("utf-8"), exptime=self.get_backend_timeout(timeout)
115115
)

ellar/cache/backends/base.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ async def executor(self, func: t.Callable, *args: t.Any, **kwargs: t.Any) -> t.A
8686
return await run_in_threadpool(func, *args, **kwargs)
8787

8888
async def get_async(self, key: str, version: str = None) -> t.Any:
89-
return await self.executor(self.get, key)
89+
return await self.executor(self.get, key, version=version)
9090

9191
async def set_async(
9292
self,
@@ -95,17 +95,19 @@ async def set_async(
9595
timeout: t.Union[float, int] = None,
9696
version: str = None,
9797
) -> bool:
98-
result = await self.executor(self.set, key, value, timeout)
98+
result = await self.executor(
99+
self.set, key, value, timeout=timeout, version=version
100+
)
99101
return bool(result)
100102

101103
async def delete_async(self, key: str, version: str = None) -> bool:
102-
result = await self.executor(self.delete, key, version)
104+
result = await self.executor(self.delete, key, version=version)
103105
return bool(result)
104106

105107
async def touch_async(
106108
self, key: str, timeout: t.Union[float, int] = None, version: str = None
107109
) -> bool:
108-
result = await self.executor(self.touch, key, timeout)
110+
result = await self.executor(self.touch, key, timeout=timeout, version=version)
109111
return bool(result)
110112

111113
async def close_async(self, **kwargs: t.Any) -> None:

ellar/cache/backends/simple_cache.py renamed to ellar/cache/backends/local_cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ..model import BaseCacheBackend
1414

1515

16-
class SimpleCacheBackendSync(IBaseCacheBackendAsync, ABC):
16+
class LocalMemCacheBackendSync(IBaseCacheBackendAsync, ABC):
1717
def _async_executor(self, func: t.Awaitable) -> t.Any:
1818
return get_or_create_eventloop().run_until_complete(func)
1919

@@ -45,7 +45,7 @@ def touch(
4545
return bool(res)
4646

4747

48-
class SimpleCacheBackend(SimpleCacheBackendSync, BaseCacheBackend):
48+
class LocalMemCacheBackend(LocalMemCacheBackendSync, BaseCacheBackend):
4949
pickle_protocol = pickle.HIGHEST_PROTOCOL
5050

5151
def __init__(self, **kwargs: t.Any) -> None:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .backend import RedisCacheBackend
2+
from .serializer import IRedisSerializer
3+
4+
__all__ = ["IRedisSerializer", "RedisCacheBackend"]
Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
import asyncio
2-
import pickle
1+
import random
32
import typing as t
43
from abc import ABC
54

5+
from ellar.helper.event_loop import get_or_create_eventloop
6+
67
try:
78
from redis.asyncio import Redis # type: ignore
89
from redis.asyncio.connection import ConnectionPool # type: ignore
910
except ImportError as e: # pragma: no cover
1011
raise RuntimeError(
1112
"To use `RedisCacheBackend`, you have to install 'redis' package e.g. `pip install redis`"
1213
) from e
13-
from ..interface import IBaseCacheBackendAsync
14-
from ..make_key_decorator import make_key_decorator
15-
from ..model import BaseCacheBackend
14+
from ...interface import IBaseCacheBackendAsync
15+
from ...make_key_decorator import make_key_decorator
16+
from ...model import BaseCacheBackend
17+
from .serializer import IRedisSerializer, RedisSerializer
1618

1719

1820
class RedisCacheBackendSync(IBaseCacheBackendAsync, ABC):
1921
def _async_executor(self, func: t.Awaitable) -> t.Any:
20-
return asyncio.get_event_loop().run_until_complete(func)
22+
return get_or_create_eventloop().run_until_complete(func)
2123

2224
def get(self, key: str, version: str = None) -> t.Any:
2325
return self._async_executor(self.get_async(key, version=version))
@@ -48,36 +50,58 @@ def touch(
4850

4951

5052
class RedisCacheBackend(RedisCacheBackendSync, BaseCacheBackend):
51-
"""Redis-based cache backend."""
53+
"""Redis-based cache backend.
54+
55+
Redis Server Construct example::
56+
backend = RedisCacheBackend(servers=['redis://[[username]:[password]]@localhost:6379/0'])
57+
OR
58+
backend = RedisCacheBackend(servers=['redis://[[username]:[password]]@localhost:6379/0'])
59+
OR
60+
backend = RedisCacheBackend(servers=['rediss://[[username]:[password]]@localhost:6379/0'])
61+
OR
62+
backend = RedisCacheBackend(servers=['unix://[username@]/path/to/socket.sock?db=0[&password=password]'])
5263
53-
pickle_protocol: t.Any = pickle.HIGHEST_PROTOCOL
64+
"""
5465

5566
def __init__(
5667
self,
57-
url: str = "localhost",
58-
db: int = None,
59-
port: int = None,
60-
username: str = None,
61-
password: str = None,
68+
servers: t.List[str],
6269
options: t.Dict = None,
63-
serializer: t.Callable = pickle.dumps,
64-
deserializer: t.Callable = pickle.loads,
70+
serializer: IRedisSerializer = None,
6571
**kwargs: t.Any
6672
) -> None:
6773
super().__init__(**kwargs)
6874

69-
self._cache_client_init: Redis = None
75+
self._pools: t.Dict[int, ConnectionPool] = {}
76+
self._servers = servers
7077
_default_options = options or {}
7178
self._options = {
72-
"url": url,
73-
"db": db,
74-
"port": port,
75-
"username": username,
76-
"password": password,
7779
**_default_options,
7880
}
79-
self._serializer = serializer
80-
self._deserializer = deserializer
81+
self._serializer = serializer or RedisSerializer()
82+
83+
def _get_connection_pool_index(self, write: bool) -> int:
84+
# Write to the first server. Read from other servers if there are more,
85+
# otherwise read from the first server.
86+
if write or len(self._servers) == 1:
87+
return 0
88+
return random.randint(1, len(self._servers) - 1)
89+
90+
def _get_connection_pool(self, write: bool) -> ConnectionPool:
91+
index = self._get_connection_pool_index(write)
92+
if index not in self._pools:
93+
self._pools[index] = ConnectionPool.from_url(
94+
self._servers[index],
95+
**self._options,
96+
)
97+
return self._pools[index]
98+
99+
def _get_client(self, *, write: bool = False) -> Redis:
100+
# key is used so that the method signature remains the same and custom
101+
# cache client can be implemented which might require the key to select
102+
# the server, e.g. sharding.
103+
pool = self._get_connection_pool(write)
104+
return Redis(connection_pool=pool)
81105

82106
def get_backend_timeout(
83107
self, timeout: t.Union[float, int] = None
@@ -88,21 +112,12 @@ def get_backend_timeout(
88112
# Non-positive values will cause the key to be deleted.
89113
return None if timeout is None else max(0, int(timeout))
90114

91-
@property
92-
def _cache_client(self) -> Redis:
93-
"""
94-
Implement transparent thread-safe access to a memcached client.
95-
"""
96-
if self._cache_client_init is None:
97-
pool = ConnectionPool.from_url(**self._options)
98-
self._redis_int = Redis(connection_pool=pool)
99-
return self._cache_client_init
100-
101115
@make_key_decorator
102116
async def get_async(self, key: str, version: str = None) -> t.Any:
103-
value = await self._cache_client.get(key)
117+
client = self._get_client()
118+
value = await client.get(key)
104119
if value:
105-
return self._deserializer(value)
120+
return self._serializer.load(value)
106121
return None
107122

108123
@make_key_decorator
@@ -113,27 +128,26 @@ async def set_async(
113128
timeout: t.Union[float, int] = None,
114129
version: str = None,
115130
) -> bool:
116-
value = self._serializer(value, self.pickle_protocol)
131+
client = self._get_client()
132+
value = self._serializer.dumps(value)
117133
if timeout == 0:
118-
await self._cache_client.delete(key)
134+
await client.delete(key)
119135

120-
return bool(
121-
await self._cache_client.set(
122-
key, value, ex=self.get_backend_timeout(timeout)
123-
)
124-
)
136+
return bool(await client.set(key, value, ex=self.get_backend_timeout(timeout)))
125137

126138
@make_key_decorator
127139
async def delete_async(self, key: str, version: str = None) -> bool:
128-
result = await self._cache_client.delete(key)
140+
client = self._get_client()
141+
result = await client.delete(key)
129142
return bool(result)
130143

131144
@make_key_decorator
132145
async def touch_async(
133146
self, key: str, timeout: t.Union[float, int] = None, version: str = None
134147
) -> bool:
148+
client = self._get_client()
135149
if timeout is None:
136-
res = await self._cache_client.persist(key)
150+
res = await client.persist(key)
137151
return bool(res)
138-
res = await self._cache_client.expire(key, self.get_backend_timeout(timeout))
152+
res = await client.expire(key, self.get_backend_timeout(timeout))
139153
return bool(res)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import pickle
2+
import typing as t
3+
from abc import ABC, abstractmethod
4+
5+
6+
class IRedisSerializer(ABC):
7+
default_protocol: int = pickle.HIGHEST_PROTOCOL
8+
9+
@abstractmethod
10+
def load(self, data: t.Any) -> t.Any:
11+
...
12+
13+
@abstractmethod
14+
def dumps(self, data: t.Any) -> t.Any:
15+
...
16+
17+
18+
class RedisSerializer(IRedisSerializer):
19+
def __init__(self, protocol: int = None) -> None:
20+
self._protocol = protocol or self.default_protocol
21+
22+
def load(self, data: t.Any) -> t.Any:
23+
try:
24+
return int(data)
25+
except ValueError:
26+
return pickle.loads(data)
27+
28+
def dumps(self, data: t.Any) -> t.Any:
29+
# Only skip pickling for integers, a int subclasses as bool should be
30+
# pickled.
31+
if type(data) is int:
32+
return data
33+
return pickle.dumps(data, self._protocol)

ellar/cache/model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ async def has_key_async(self, key: str, version: str = None) -> bool:
2727
"""
2828
Return True if the key is in the cache and has not expired.
2929
"""
30-
return await self.get_async(key, version) is not None
30+
return await self.get_async(key, version=version) is not None
3131

3232
def has_key(self, key: str, version: str = None) -> bool:
3333
"""
3434
Return True if the key is in the cache and has not expired.
3535
"""
36-
return self.get(key, version) is not None
36+
return self.get(key, version=version) is not None
3737

3838
def validate_key(self, key: str) -> None:
3939
if len(key) > self.MEMCACHE_MAX_KEY_LENGTH:

ellar/cache/service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from ellar.di import injectable
44

5-
from .backends.simple_cache import SimpleCacheBackend
5+
from .backends.local_cache import LocalMemCacheBackend
66
from .interface import ICacheService, ICacheServiceSync
77
from .model import BaseCacheBackend
88

@@ -52,7 +52,7 @@ def __init__(self, backends: t.Dict[str, BaseCacheBackend] = None) -> None:
5252
"default"
5353
), "CACHES configuration must have a 'default' key."
5454
self._backends = backends or {
55-
"default": SimpleCacheBackend(key_prefix="ellar", version=1, timeout=300)
55+
"default": LocalMemCacheBackend(key_prefix="ellar", version=1, timeout=300)
5656
}
5757

5858
def _get_backend(self, backend: str = None) -> BaseCacheBackend:

0 commit comments

Comments
 (0)