Package: chorde
Dependencies:numpy(for optimizations in FilesCacheClient)python-memcached(for MemcachedClient)pyzmq(for coherence support)dnspython(for dynamic dns-based load-balancing of MemcachedClient)cython(for optimizations in InprocCacheClient)Optional featuresWhen declaring dependencies in your project, you can reference the following optional features:mq(everything under chorde.mq, mostly pulls pyzmq)shmem(shared memory utils, needed for optimized files cache clients)memcacheelasticacheBasic Usage:The simplest way to use is to create one of the supported cache clients,
and use it directly, likefromchorde.clients.inprocimportInprocCacheClientfromchordeimportCacheMissErrorc=InprocCacheClient(200)c.put(3,10,300)# put value 10 on key 3, TTL 5minassert10==c.get(3)try:c.get(5)exceptCacheMissError:print"miss"This creates an In-process LRU cache. The in-process part indicates that it
is process-private, and not shared with other processes.There are two implementations of the LRU, with different performance
characteristics. TheInprocCacheClientcan take alternative store
implementations as an argument, see the module for details.The default LRUCache, accessible aschorde.clients.inproc.Cache,
is a regular LRU implemented with
a priority queue and a hash table in tandem, so it hasO(log n)writes
andO(1)reads, but by default all reads entail a write (to update the
LRU). That can be disabled by specifying custom options, see the module’s
documentation for more details.There’s an alternative approximate LRU, accessible inchorde.clients.inproc.CuckooCache, that implements a lazy version of
a cuckoo hash table, and hasO(1)reads and amortizedO(1)writes.
It’s also quite more space-efficient than the regular LRU, so it’s better
suited for very large caches, but its eviction strategy will be approximate,
and thus not guaranteed to always evict the actual least-recently-used item.Shared CachesThe most straightforward way to get a shared cache, is to use a memcache:fromchorde.clients.memcachedimportMemcachedClientfromchordeimportCacheMissErrorc=MemcachedClient(["localhost:11211"],checksum_key="testing")c.put(3,10,300)assert10==c.get(3)try:c.get(5)exceptCacheMissError:print"miss"The MemcachedClient is used just like any other client, only it talks to, in this
example, a local memcached listening on localhost port 11211. Multiple clients
can be given, and a consistent hash on the key will be used to spread the load
among them.If a hostname is given, and the hostname points to multiple IP addresses, the
same effect will be obtained, and the distribution will be dynamically updated
according to the TTL specified on the DNS entry. This, for example, makes the
client work seamlessly with Amazon ElastiCache’s “configuration endpoint”,
which is a DNS entry that points to one of the cache nodes. But it only works
like that with single-node clusters. For multi-node clusters, usechorde.clients.elasticache.ElastiCacheClient, which goes a step further
and queries this configuration endpoint for all the other nodes.Do beware that the key used against the memcached isn’t the key given, since
the client supports any hashable object as key, whereas memcached only supports
a subset of string keys. MemcachedClient has none of memcached’s limitations
with regards to key format and/or length and value size. It works around
memcaches limitations, by constructing a derived key that is suitable for
memcached. However, there is overhead in providing huge keys or values, so it’s generally
good advise to avoid them anyway.Values in the MemcachedClient are plickled, compressed and signed with the
checksum key, so it is relatively safe from both malicious code injection through
pickle, and transmission errors causing cPickle to dump core (which does happen when
it is fed unchecked data).In case the compression becomes a bottleneck, which shouldn’t be a problem unless
it is a high-traffic cache with rarely compressible values, one can disable it.
Check MemcachedClient’s documentation for more details.To reduce roundtrips, all clients (in particular MemcachedClients) support
getMulti and getTtlMulti, to fetch multiple keys at once:fromchorde.clients.memcachedimportMemcachedClientfromchordeimportCacheMissErrorc=MemcachedClient(["localhost:11211"],checksum_key="testing")c.put(3,10,300)c.put(4,20,300)assert{3:10,4:20,5:None}==dict(c.getMulti([3,4,5],None))See the documentation on clients.base for more details.Multilevel cachesA common approach when dealing with remote caches, like the above example using
memcached, is to have at least two levels: the memcached itself, and an in-process
small cache to avoid having to talk to the memcached all the time.This can be done straightforwardly with the tiered clients:fromchorde.clients.memcachedimportMemcachedClientfromchorde.clients.inprocimportInprocCacheClientfromchorde.clients.tieredimportTieredInclusiveClientfromchordeimportCacheMissErrorl1=InprocCacheClient(10)l2=MemcachedClient(["localhost:11211"],checksum_key="test")c=TieredInclusiveClient(l1,l2)c.put(3,10,300)assert10==c.get(3)try:c.get(5)exceptCacheMissError:print"miss"Here we build aninclusivetiered client, in which elements on higher levels are
promoted into the lower levels by copying, rather than swapping. This means there
is duplication among them, but this is usually best in cases like these, where the
upper levels are shared among processes.An exclusive client isn’t provided at this moment, since there is seldom any use
for the exclusive pattern on these types of caches.DecoratorsA more natural way to think about caching, is in that it’s a decorator of plain functions.Rather than explicitly putting and getting from caches, one can simply consider
caching as an optimization on an otherwise expensive function.Decorators in chorde.decorators provide a huge amount of functionality and flexibility,
these examples cover only the most basic usage:Assumingcis the client we want to use for caching,fromchorde.decoratorsimportcachedimportrandom@cached(c,ttl=300,async_ttl=-60)defexpensive_func(x):returnx*random.random()printexpensive_func(1)printexpensive_func(1)# Should return the sameprintexpensive_func.bg()(1)# will refresh asynchronously every minuteprintexpensive_func.future()(1).result()# same as before, but using the futures interfaceprintexpensive_func.peek(1)# just check the cacheprintexpensive_func.put(1,_cache_put=5)# write an explicit valueprintexpensive_func.bg().lazy(1)# don't wait, raise CacheMissError if not available, compute in backgroundprintexpensive_func.future().lazy(1).result()# same as before, but using the futures interfaceThere, the async_ttl means the minimum TTL that triggers
an asynchronous recomputation (you can use it to avoid ever having to wait on a recomputation).
The negative value makes it relative to the total TTL, so -60 always means recompute
every minute (60 seconds). The plain ttl is an absolute limit, no result older than
that will ever be returned.The documentation on chorde.decorators.cached will have more to say about the ways of
invoking cached functions.In general, the terms are:lazy: don’t wait for computation, return a cached result or raise CacheMissError.
When combined with bg, it will compute in the background.peek: don’t compute. Similar to lazy, but it will never trigger a computationbg: expensive things (computation) happen on a background threadpool.future: return futures rather than results, use the future to get notified of
results when they’re available. Actual cache access happens on a threadpool.
A non-blocking way of calling.refresh: immediately recompute the value.Integration with other librariesThe decorators’ future() interface is especially suited for integration with other libraries that can talk to
futures. Chorde’s futures, however, are not directly compatible with other libraries’, but they can easily be
wrapped like so:importtornado.webimporttornado.genfromchorde.clients.asyncacheimportmakeFutureWrapperWF=makeFutureWrapper(tornado.web.Future)...@tornado.gen.coroutinedefget(self):some_result=yieldWF(some_func.future()(some_args))There is a better way to integrate with tornado >= 4.0fromchorde.external_integrationimportmonkey_patch_tornadomonkey_patch_tornado()importtornado.webimporttornado.gen...@tornado.gen.coroutinedefget(self):some_result=yieldsome_func.future()(some_args)Additional documentationIn Depth Look into Caching (Part 1)(English)In Depth Look into Caching (Part 2)(English)Caching para hordas y estampidas por Claudio Freire - PyCon 2013(Spanish)