Caching¶
Caching refers to the process of storing frequently accessed data in a temporary storage area called a cache
, in order to speed up access to that data in the future.
In computing, caching is used to optimize the performance of applications and systems by reducing the time it takes to retrieve data from slower or more distant storage. By caching data in a faster, more local storage location, the system can quickly retrieve the data without needing to go all the way to the original source of the data.
In Ellar, we provided several cache backends interface that interacts with different cache types to assist in cache endpoint responses or other relevant data.
Setting up the cache¶
It's very simple to set up cache in Ellar but the crucial part is picking the cache type that is suitable for your application because some cache type behave differently and perform better and faster than others.
To set up cache, we need to use CacheModule
. CacheModule
provides two methods, CacheModule.register_setup
and CacheModule.setup
, for setting up cache
in ellar applications.
This setup method requires you to defined CACHES
variable containing key value pairs of cache backends in config.py
file.
for example:
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.local_cache import LocalMemCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': LocalMemCacheBackend(ttl=300, key_prefix='local', version=1)
}
After that you register CacheModule
to application modules
# project_name/root_module.py
from ellar.cache import CacheModule
from ellar.common import Module
@Module(modules=[CacheModule.register_setup()])
class ApplicationModule:
pass
register_setup
will read CACHES
from application config and setup the CacheService
appropriately. The setup method requires direct definition of cache backend on the CacheModule
setup function.
for example:
# project_name/root_module.py
from ellar.cache import CacheModule
from ellar.cache.backends.local_cache import LocalMemCacheBackend
from ellar.common import Module
@Module(modules=[
CacheModule.setup(
default=LocalMemCacheBackend(ttl=300, key_prefix='default', version=1),
local=LocalMemCacheBackend(key_prefix='local'),
others=LocalMemCacheBackend(key_prefix='others'),
)
])
class ApplicationModule:
pass
setup
, the default
parameter must be provided and other cache backends will be defined as keyword-arguments just like local
and others
incase you want to set up more than one cache backend. Memcached¶
Memcached is an entirely memory-based cache server, originally developed to handle high loads at LiveJournal.com and subsequently open-sourced by Danga Interactive.
Memcached runs as a daemon and is allotted a specified amount of RAM. All it does is provide a fast interface for adding, retrieving and deleting data in the cache. All data is stored directly in memory.
After installing Memcached itself, you’ll need to install a Memcached binding. There are several Python Memcached bindings available;
Ellar supports are pylibmc and pymemcache
For an example, lets assume you have a Memcached is running on localhost (127.0.0.1) port 11211, using the pymemcache
or pylibmc
binding:
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.pymem_cache import PyMemcacheCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': PyMemcacheCacheBackend(servers=['127.0.0.1:11211'])
}
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.pylib_cache import PyLibMCCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': PyLibMCCacheBackend(servers=['127.0.0.1:11211'])
}
If Memcached is available through a local Unix socket file /tmp/memcached.sock using the pymemcache
or pylibmc
binding:
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.pymem_cache import PyMemcacheCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': PyMemcacheCacheBackend(servers=['/tmp/memcached.sock'])
}
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.pylib_cache import PyLibMCCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': PyLibMCCacheBackend(servers=['/tmp/memcached.sock'])
}
if your Memcached is its ability to share a cache over multiple servers, then you can config that too Lets assume the cache is shared over Memcached instances running on IP address 172.19.26.240 and 172.19.26.242, both on port 11211 or different ports
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.pymem_cache import PyMemcacheCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': PyMemcacheCacheBackend(servers=[
'172.19.26.240:11211',
'172.19.26.242:11212',
'172.19.26.244:11213',
])
}
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.pylib_cache import PyLibMCCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': PyLibMCCacheBackend(servers=[
'172.19.26.240:11211',
'172.19.26.242:11212',
'172.19.26.244:11213',
])
}
For pymemcache, we provided some default configuration during initialization shown below:
import pymemcache
_options = {
'allow_unicode_keys': True,
'default_noreply': False,
'serde': pymemcache.serde.pickle_serde,
}
These can be changed by setting the desired value in options
parameter during initialization. For example:
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.pymem_cache import PyMemcacheCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': PyMemcacheCacheBackend(servers=[
'172.19.26.240:11211',
'172.19.26.242:11212',
'172.19.26.244:11213',
], options={'default_noreply': True})
}
Redis¶
Redis is a high-performance, in-memory database that is commonly used for caching data. To get started with Redis, you will need to have a Redis server running on either your local machine or a remote server.
Once you have set up the Redis server, you will need to install the Redis Python client library to be able to communicate with Redis from your Python code.
To use redis in Ellar, you need to import RedisCacheBackend from ellar.cache.backend.redis
.
Let's assume after setting up your redis server and it's running on localhost (127.0.0.1) port 6379:
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.redis import RedisCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': RedisCacheBackend(servers=['redis://127.0.0.1:6379'])
}
Often Redis servers are protected with authentication. In order to supply a username and password as follows:
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.redis import RedisCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': RedisCacheBackend(servers=['redis://username:password@127.0.0.1:6379'])
}
If you have multiple Redis servers set up in the replication mode, you can specify the servers either as a semicolon or comma delimited string, or as a list. While using multiple servers, write operations are performed on the first server (leader). Read operations are performed on the other servers (replicas) chosen at random:
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.redis import RedisCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': RedisCacheBackend(servers=[
'redis://127.0.0.1:6379', # leader
'redis://127.0.0.1:6378', # read-replica 1
'redis://127.0.0.1:6377', # read-replica 2
])
}
Local-memory caching¶
The local memory cache is the default caching mechanism used by Ellar, and it is automatically used if you do not specify a different caching backend in your config.py file. This cache stores cached data in memory, which provides fast access to cached data, and is ideal if you don't have the resources or capabilities to set up a separate caching server like Memcached. Its also thread-safe.
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.local_cache import LocalMemCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': LocalMemCacheBackend()
}
Custom Cache Backend¶
You can create you own version of the cache backend. All you need is to inherit for ellar.
for example:
# project_name/cache_backend.py
from ellar.cache.model import BaseCacheBackend
class CustomCacheBackend(BaseCacheBackend):
pass
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from .cache_backend import CustomCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': CustomCacheBackend()
}
Cache Arguments¶
You can customize the behavior of each caching backend in Django by passing additional arguments when you configure the cache. The valid arguments that can be passed to each backend are as follows:
- TIMEOUT: The default timeout, in seconds, to use for the cache. This argument defaults to 300 seconds (5 minutes). You can set TIMEOUT to None so that, by default, cache keys never expire. A value of 0 causes keys to immediately expire
- OPTIONS: Any options that should be passed to the cache backend. The list of valid options will vary with each backend.
- KEY_PREFIX: A string that will be automatically prepended to all cache keys.
- VERSION: The default version number for cache keys.
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.pymem_cache import PyMemcacheCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': PyMemcacheCacheBackend(
servers=['127.0.0.1:11211'],
options={'default_noreply': True},
ttl=300,
version=1,
key_prefix='project_name'
)
}
Setting up More than One Cache Backend¶
To set up multiple cache backends in Django, you can add additional entries to the CACHES
variable in your config.py
file. The default
cache backend is typically defined first, followed by any additional cache backends you want to configure.
Here's an example CACHES
setting that defines two cache backends:
# project_name/config.py
from ellar.core import ConfigDefaultTypesMixin
from ellar.cache.backends.redis import RedisCacheBackend
from ellar.cache.backends.local_cache import LocalMemCacheBackend
class DevelopmentConfig(ConfigDefaultTypesMixin):
CACHES = {
'default': RedisCacheBackend(servers=['redis://127.0.0.1:6379'], key_prefix='project_name'),
'secondary': LocalMemCacheBackend(ttl=300, key_prefix='project_name', version=1)
}
CacheService (ICacheService)¶
Ellar does not provide cache backends directly, but instead offers a caching service that manages all the configured cache backends in your application. The CacheService
class serves as a wrapper for these cache backends and provides a uniform interface for interacting with them.
The CacheService
class can be injected into your application's code as a dependency, allowing you to use it throughout your application without the need for direct instantiation. This approach promotes a more modular and extensible design, as well as better testability of your code.
The CacheService class provides methods like:
class CacheService(ICacheService):
- get(key: str, version: str = None, backend: str = None): gets
key
value from a specified cache backend. - get_async(key: str, version: str = None, backend: str = None): asynchronous version of
get
action - set(key: str, value: t.Any, ttl: t.Union[float, int] = None, version: str = None,backend: str = None): sets value to a key to a specified cache backend.
- set_async(key: str, value: t.Any, ttl: t.Union[float, int] = None, version: str = None,backend: str = None): asynchronous version of
set
action - delete(key: str, version: str = None, backend: str = None): deletes a key from a specified cache backend.
- delete_async(key: str, version: str = None, backend: str = None): asynchronous version of
delete
action - has_key(key: str, version: str = None, backend: str = None): checks if a key exist in a specified backend
- has_key_async(key: str, version: str = None, backend: str = None): asynchronous version of
has_key
action - touch(key: str, ttl: t.Union[float, int] = None, version: str = None, backend: str = None): sets a new expiration for a key
- touch_async(key: str, ttl: t.Union[float, int] = None, version: str = None, backend: str = None): asynchronous version of
touch
action - incr(key: str, delta: int = 1, version: str = None, backend: str = None): increments a value for a key by delta
- incr_async(key: str, delta: int = 1, version: str = None, backend: str = None): asynchronous version of
incr
action - decr(key: str, delta: int = 1, version: str = None, backend: str = None): decrement a value for a key by delta
- decr_async(key: str, delta: int = 1, version: str = None, backend: str = None): asynchronous version of
decr
action
Note
If backend=None
, default
backend configuration is used.
These methods are available for each of the configured cache backends and can be used interchangeably with any backend.
Injecting CacheService¶
CacheService
is a core service registered in EllarInjector
and can be injected as every other service.
For example, lets make CacheService
available in our route function.
from ellar.common import get, Inject
from ellar.cache import ICacheService
...
@get('/cache-test')
def my_route_function(self, cache_service: Inject[ICacheService]):
cached_value = cache_service.get("my-key")
if cached_value:
return cached_value
processed_value = 'some-value'
cache_service.set('my-key', processed_value, ttl=300) # for 5mins
return processed_value
from ellar.common import get, Inject
from ellar.cache import ICacheService
...
@get('/cache-test')
async def my_route_function(self, cache_service: Inject[ICacheService]):
cached_value = await cache_service.get_async("my-key")
if cached_value:
return cached_value
processed_value = 'some-value'
await cache_service.set_async('my-key', processed_value, ttl=300) # for 5mins
return processed_value
Using Cache Decorator¶
Ellar provides a cache decorator that can be used to cache the responses of route functions. The cache decorator can be applied to a route function to automatically cache its response data for a specified amount of time.
The cache decorator takes the following arguments:
ttl
(time to live): the amount of time (in seconds) for which the response data should be cached.key_prefix
(optional): a string that is used to prefix the cache key, allowing for easy differentiation between different cache items.version
(optional): a string that is used to version the cache key, allowing for cache invalidation when the data schema changes.backend
(optional): the name of the cache backend to use for storing the cached data. By default, thedefault
cache backend is used.make_key_callback
(optional): a callback function that can be used to generate a custom cache key. This function takes anIExecutionContext
instance (which contains information about the request context) and key prefix, and should return the custom cache key to use.
Info
Cache
Decorator can also be applied to any controller class. When this is done, all the routes response of that controller will be cached
We can rewrite the above example using cache
decorator:
from ellar.common import get
from ellar.cache import Cache
...
@get('/cache-test')
@Cache(ttl=300, version='v1', key_prefix='project_name')
def my_route_function(self):
processed_value = 'some-value'
return processed_value
from ellar.common import get
from ellar.cache import Cache
...
@get('/cache-test')
@Cache(ttl=300, version='v1', key_prefix='project_name')
async def my_route_function(self):
processed_value = 'some-value'
return processed_value
Adding Custom key gen function for cache Decorator¶
By default, the cache
decorator combines the route function's URL and the specified key_prefix
value to generate the cache key used to store the response data. However, you can customize this behavior by providing a make_key_callback
function to the cache decorator.
The make_key_callback
function takes an ExecutionContext
instance (which contains information about the request context) and the key_prefix
value as input, and should return the custom cache key to use.
Here's an example of how to use a custom make_key_callback
function with the cache decorator:
from ellar.common import get
from ellar.cache import Cache
from ellar.core import ExecutionContext
from ellar.utils import get_name
def make_key_function(ctx: ExecutionContext, key_prefix: str) -> str:
function_name = get_name(ctx.get_handler())
return "%s:%s:%s" % (function_name, key_prefix, ctx.switch_to_http_connection().get_client().url)
...
@get("/my_endpoint")
@Cache(ttl=60, make_key_callback=make_key_function)
def my_endpoint(self):
# Code to generate response data here
processed_value = 'some-value'
return processed_value
...
from ellar.common import get
from ellar.cache import Cache
from ellar.core import ExecutionContext
from ellar.utils import get_name
def _make_key_function(ctx: ExecutionContext, key_prefix: str) -> str:
function_name = get_name(ctx.get_handler())
return "%s:%s:%s" % (function_name, key_prefix, ctx.switch_to_http_connection().get_client().url)
...
@get("/my_endpoint")
@Cache(ttl=60, make_key_callback=_make_key_function)
async def my_endpoint(self):
# Code to generate response data here
processed_value = 'some-value'
return processed_value
...
In this example, the cache
decorator is applied to the my_endpoint
route function, with a custom make_key_callback
function specified.
The make_key_callback
function uses the get_name
helper function to extract the name of the route function, and combines it with the key_prefix
value and the request URL to generate the cache key.