Why Ring?¶
Caching is a popular concept widely spread on the broad range of computer science. But cache interface is not well-developed yet. Ring is one of the solutions for humans. Its approach is close integration with a programming language.
Common problems of cache¶
note: | Skip this section if you are familiar with cache and decorator patterns for cache in Python world. |
---|
The straightforward approach to storage¶
Traditionally we considered cache as a storage. In that sense, calling useful
actual_function()
with an argument for the cached result of
cached_function()
looks like next series of works:
# psuedo code for rough flow
key = create_key()
if storage.has(key):
result = storage.get(key)
else:
result = cached_function()
storage.set(key, result)
actual_function(result)
What’s the problem? We are interested in cached_function()
and
actual_function()
instead of storage
. But the code is full of storage
operations.
Decorated cache function¶
Lots of cache libraries working with immutable functions share the similar
solution. Here is a functools.lru_cache()
example:
from functools import lru_cache
@lru_cache(maxsize=32)
def cached_function():
...
result = cached_function()
actual_function(result)
This code is a lot more readable. Now the last 2 lines of code show what the
code does. Note that this code even includes the definition of
cached_function()
which was not included in the prior code.
If the programming world was built on immutable functions, this is perfect;
But actually not. I really love functools.lru_cache()
but couldn’t use
it for most of the cases.
In the real world, lots of functions are not pure function - but still, need to be cached. Since this also is one of the common problems, there are solutions too. Let’s see Django’s view cache which helps to reuse web page for specific seconds.
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def cached_page(request):
...
It means the view is cached and the cached data is valid for 15 minutes. In
this case, actual_function()
is inside of the Django. The actual function
will generate HTTP response based on the cached_page
. It is good enough
when cache expiration is not a real-time requirement.
Manual expiration¶
Unfortunately, websites are often real-time. Suppose it was a list of customer service articles. New articles must be shown up in short time. This is how Django handle it with The per-view cache.
request = Request(...) # fake request to create key
key = get_cache_key(request)
cache.delete(key) # expire
get_cache_key
and
cache
are global names from Django framework
to control cache. We started from a neat per-view cache decorator - but now it
turns into a storage approach which we demonstrated at first section.
You can control them at a consistent level with Ring.
see: | Ring controls cache life-cycle with sub-functions section for details. |
---|---|
see: | ring.django.cache_page() which exactly solved the problem. |
Methods and descriptors support¶
This kind of convenient decorators commonly works for plain functions. Then how about class components like methods, classmethod and property? Ring supports them in expected convention with a unique form.
see: | Methods and descriptors support section for details. |
---|
Fixed strategy¶
Sometimes we need more complicated strategy than normal. Suppose we have very heavy and critical layer. We don’t want to lose cache. It must be updated in every 60 seconds, but without losing the cached version even if it wasn’t updated - or even during it is being updated. With common solutions, we needed to drop the provided feature but to implement a new one.
You can replace semantics of Ring commands and storage behaviors.
see: | Ring comes with configurable commands and storage actions section for details. |
---|
Data encoding¶
How to save non-binary data? Python supports pickle
as a standard
library to convert python objects to binary. Some of the storage libraries
like python-memcached implicitly run pickle
to support saving and
loading Python objects.
class A(object):
"""Custom object with lots of features and data"""
...
client = memcache.Client(...)
original_data = A()
client.set(key, original_data)
loaded_data = client.get(key)
assert isinstance(loaded_data, A) # True
assert original_data == loaded_data # mostly True
Unfortunately, pickle
is not compatible out of the Python world and
even some complex Python classes also generate massive pickle
data for
small information. For example, when you have non-Python code which accesses
to the same data, pickle
doesn’t fit.
In Ring, data encoder is not a fixed value. Choose a preferred way to encode data by each ring rope.
note: | To be fair, you can pass pickler parameter to
memcached.Client in python-memcached to change the behavior.
In Ring, you can reuse the same memcached.Client to use
multiple coders. |
---|---|
see: | Ring provides a configurable data-coding layer section for details. |
Ring controls cache life-cycle with sub-functions¶
The basic usage is similar to functools.lru_cache()
or Django per-view
cache.
import ring
@ring.lru()
def cached_function():
...
result = cached_function()
actual_function(result)
Extra operations are supported as below:
cached_function.update() # force update
cached_function.delete() # expire
cached_function.execute() # this will not generate cache
cached_function.get() # get value only when cache exists
Ring provides a common auto-cache approach by default but not only that. Extra controllers provide full functions for cache policy and storages.
see: | Attributes of Ring object for details. |
---|
Function parameters are also supported in an expected manner:
@ring.lru()
def cached_function(a, b, c):
...
cached_function(10, 20, 30) # normal call
cached_function.delete(10, 20, 30) # delete call
Ring approaches backend transparent way¶
High-level interface providers like Ring cannot expose full features of
the backends. Various storages have their own features by their design.
Ring covers common features but does not cover others.
ring.func.base.Ring
objects serve data extractors instead.
client = memcache.Client(...)
@ring.memcache(client)
def f(a):
...
cache_key = f.key(10) # cache key for 10
assert f.storage.backend is client
encoded_data = f.storage.backend.get(cache_key) # get from memcache client
actual_data = f.decode(encoded_data) # decode
see: | Attributes of Ring object for details. |
---|
Ring provides a configurable data-coding layer¶
python-memcached supports pickle
by default but pylibmc doesn’t.
By adding coder='pickle'
, next code will be cached through pickle even
with pylibmc. Of course for other backends too.
client = pylibmc.Client(...)
@ring.memcache(client, coder='pickle')
def f(a):
...
note: | Does it look verbose? functools.partial() is your friend. Try
my_cache = functools.partial(ring.memcache, client, coder='pickle') . |
---|
When you need a special coder for a function, overriding encode/decode for a specific function also is possible. For example, the next code works the same as the above.
client = pylibmc.Client(...)
@ring.memcache(client)
def f(a):
...
@f.encode
def f_encode(value):
return pickle.dumps(value)
@f.decode
def f_decode(data):
return pickle.loads(data)
see: | Save and load rich data for more information about coders. |
---|
Methods and descriptors support¶
Ring supports methods and descriptors including classmethod()
,
staticmethod()
and property()
in orthogonal interface. Any custom
descriptors written in (weak) common convention also work.
class A(object):
v = None
def __ring_key__(self):
'''convert self value typed 'A' to ring key component'''
return v
@ring.lru()
def method(self):
'''method support'''
...
@ring.lru()
@classmethod
def cmethod(self):
'''classmethod support'''
...
@ring.lru()
@staticmethod
def smethod(self):
'''staticmethod support'''
...
@ring.lru()
@property
def property(self):
'''property support'''
...
Ring comes with configurable commands and storage actions¶
see: | ring.func.base.BaseStorage |
---|---|
see: | ring.func.sync.CacheUserInterface |
see: | ring.func.asyncio.CacheUserInterface |