Most mid-sized Django websites thrive by relying on memcached. Though what happens when basic memcached is not enough? And how can one identify when the caching architecture is becoming a bottleneck? We'll cover the problems we've encountered and solutions we've put in place.
4. (Some) Technologies We Use
• Chef • Backbone.js
• Jenkins • Jinja2
• AWS • Celery
• Sphinx • DebugToolbar
• Memcached • jQuery / jQueryUI
• South • Compass / Sass
• git • Mongo & MySQL
5. Released CSky Creations
• djenesis – Easy Django Bootstrapping
• breakdown - Lightweight jinja2 template
prototyping server
• django-cachemodel – Automatic caching
for django models
6. CSky Teams
10 Django Team
STAFF
50+
5 Design Team
9 PM Team
3 System Admins
7. Cached Joke
Two hard things in computer science:
1. cache invalidation
2. naming things,
3. and off-by-one errors
8. Journey of Optimization
• Like Life
• Focus on the journey, not the destination
• No Premature Optimization
9. Continual Process of Optimization
• In a response to a changing environment
• (hopefully huge traffic)
• (hopefully not DDOS)
• Subject to constraints of time
• With infinite time, you’d build it perfect initially
• Ideally you can do small tricks up front to prepare
10. Cycle of Optimization
1. Measure - Traffic, latency, bottlenecks, profile components
2. Plan
3. Implement
4. Measure – So, did it work?
11. Journey of Caching
"There is no one right way to do it...the best
approach is very application dependent; one-size
does not fit all."
Yann Malet, Lincoln Loop
12. Web Caching Defined
"Storing expensive data for more
immediate, future retrieval"
Modified from Noah Silas, C.R.E.A.M. Talk PyCon 2012
13. Goals for Caching
• Faster response times
• Scalability
• Cache unreliable external services
• Assume all are unreliable...
14. Beware, Caching Can Lead to
• More points of failure. Especially if relying on
cache.
• Complexity & Invalidation hell
• Thundering herd & Warming Solutions
• Elegant, Scalable, Performant Applications
16. Types of Caching Strategies
• What are the data to cache?
• Where will it be stored?
• Memoize in python (e.g. via decorator)
• Backend server (e.g. memcached or redis)
• Where will it be rendered?
• In-app (query, view, template)
• Outside app: client-side, edge-side (a.k.a. upstream)
• Do updates happen inside request-repsonse cycle or
outside? Or both?
17. Madlib of Cache
I want to cache _____ data so that it stores it _____
and when needed returns it into the [template/view/
query] via a request from [in-app/AJAX-call].
Updates to this occur [in/outside/both] the request-
response cycle and when they occur it [triggers a
update-job/invalidates it and related].
18. How Important to Cache
• If not in a cache, how problematic
• That is, would you be reliant on caching
19. Properties of Cached Data
1. Sensitivity to it returning old data
2. Cost of (re)creating cache element (e.g. time, RAM)
3. Expense/Size of storing cache result
4. How often (likely to be) used?
5. How often (likely to) change?
6. Additional complexity caching adds
20. Cached Data – Lists/Arrays
7. N-dimensional array of data (e.g. list of objects)
• Constant order/filter (e.g. client-side use)
• Changing order/filter, beware of caching
• Denormalize via external tool or data structure. E.g. in-
database, sphinx, datacube
21. Cached Data – Relationality
8. Relationality of these data with others
• The more dependence on data, the more complexity
related to invalidation
• Graph of dependencies (specifically a DAG)
• Computed result and what input data it depends on
• Joins are one example
22. Caching, the simple solution
• Who is using Memcache?
• Django’s basic per-site caching?
• Noah Silas: “Cache Rules Everything Around Me?”
32. Thundering Herd
• A cache miss means many requests will trigger
database reads until the cache can be filled.
33. Thundering Herd
Solutions?
• Do not let values expire from the cache
• When an object is stale, refresh the value in the
background and continue to provide the cached
value to the rest of the herd until the value can be
updated
34. Large data sets don't cache well
Problems?
• Requests for rarely used data
• Address searching on a map
• Mail clients
35. Gotchas when deploying caching
• Versioning your caches.
• Separating your caches based on use.
• Using consistent hashing algorithms.
• Deploying memcached with Elasticache.
36. Version your cache
CACHES = {
'default': {
'BACKEND':
‘django.core.cache.backends.memcached.Mecachedcache’,
'LOCATION': '127.0.0.1:11211',
‘VERSION’: 1,
},
}
• You need to push a code change that alters the
way a cache value is generated.
• You need to preserve two different copies of the
cache based on the version
• Consider using the git sha1 hash.
$ git rev-parse HEAD | cut -b 1-7
37. Sessions in the cache
• Django documentation urges using the
memcached session backend.
• But if you need to bump your cache, you will log
out all your users.
• So Django 1.3 added multiple named caches...
39. Named caches are great, but...
• Django’s memcached session backend does not
have a way to choose a particular named cache...
• So, you’d have to write your own SessionBackend.
• OR, specify a different cache for everything else.
40. Consistent Hashing
server_idx = hash(key) % serverlist.length;
server = serverlist[server_idx]
• If serverlist.length changes, all keys get expired
• That means that if a node goes down or you need to add
a new node, you will invalidate most of the keys in your
cache.
41. The Ketama Algorithm
http://www.last.fm/user/RJ/journal/2007/04/10/rz_libketama_-_a_consistent_hashing_algo_for_memcache_clients
• Take your list of servers (eg: 1.2.3.4:11211, 5.6.7.8:11211, 9.8.7.6:11211)
• Hash each server string to several (100-200) unsigned ints
• Conceptually, these numbers are placed on a circle called the
continuum. (imagine a clock face that goes from 0 to 2^32)
• Each number links to the server it was hashed from, so servers appear
at several points on the continuum, by each of the numbers they
hashed to.
• To map a key->server, hash your key to a single unsigned int, and find
the next biggest number on the continuum. The server linked to that
number is the correct server for that key.
TL;DR
42. But who cares...
• Just use pylibmc and django-pylibmc
https://github.com/jbalogh/django-pylibmc
# pip install django-pylibmc
CACHES = {
'default': {
'BACKEND': 'django_pylibmc.memcached.PyLibMCCache',
'LOCATION': 'localhost:11211',
‘TIMEOUT’: 0,
'OPTIONS': {
'ketama': True
}
}
}
• Note that timeout=0 now caches forever
43. Elasticache
• Elasticache is memcache as an AWS service.
• It just works.
• It does cost more than doing it yourself...
• but deployment is hard, so think about it.
46. Elasticache Security Groups
• EC2 Security Groups define incoming firewall rules.
• You should create a EC2 security group for each project.
• Add a EC2 security group rule to allow access to the
memcache port
• Then create an Elasticache Security Group for the
project.
• Authorize the EC2 Security Group on the Elasticache
Security Group
48. How do I know what I should cache?
• Use debug-toolbar to reduce query counts and
monitor cache hits/misses.
https://github.com/django-debug-toolbar/django-debug-toolbar
• Use django-memcached-monitor to monitor
memcache stats in the admin.
https://github.com/bartTC/django-memcache-status
• NewRelic is a great tool for profiling and
monitoring.
http://newrelic.com
56. Caching Frameworks
• Auto Cache
• https://github.com/noah256/django-autocache
MyModel.cache.get() vs. MyModel.objects.get()
57. The Publish Model
• See “Cache Rules Everything Around Me”
--Noah Silas
http://pyvideo.org/video/679
• Also see “Django Doesnt Scale”
--Jacob Kaplan Moss
https://speakerdeck.com/u/jacobian/p/django-doesnt-scale
58. C.R.E.A.M. get the money.
• Cache everything “forever”.
One month is basically forever right?
• If you hit the cache, and the data is stale,
immediately return it to the user anyway, and
update it in the background.
• Waiting for results appears broken.
• Stale results gives you perceived performance.
59. Views should never block on the db...
Shamelessly ripped from “Django Doesnt Scale” by Jacob Kaplan-Moss
60. KK. But How?!
• Build a cache key file.
• Associate cache keys with functions that publish to
the cache.
• Use celery tasks to trigger cache warming.
• Pre-warm caches.
62. That’s right, another slide of code!
croupon/models.py
import croupon.cachekeys
class Croupon(models.Model):
...
def save(self, *args, **kwargs):
super(Croupon, self).save(*args, **kwargs)
# something has changed
# so update the cache in the background
publish_to_cache.apply_async(croupon.cachekeys,
'PopularInCity', city=self.city)
croupon/views.py
context['popular_croupons'] =
consume_from_cache(croupon.cachekeys,
‘PopularInCity’, city=city)
...
63. More code? Really?!
publish_model/utils.py
def publish_to_cache(keydict, key_name, **kwargs):
key_fmt, data_fun = keydict[key_name]
key = key_fmt % kwargs
data = data_fun(**kwargs)
cache.set(key, data, FOREVER_TIMEOUT)
return data
def consume_from_cache(keydict, key_name, **kwargs):
key_fmt, data_fun = keydict[key_name]
key = key_fmt % kwargs
data = cache.get(key)
if data is None:
# there was a cache miss,
# so fall back to thundering herd
data = publish_to_cache.apply(keydict,
key_name, **kwargs)
return data
64. Where Django Caching
Busts at the Seams
DjangoCon US 2012
concentricsky.com
@concentricsky // wiggins / rimkus / biglan
Editor's Notes
try to focus on middle size applications. strategies, tools, implementation details, warnings. and ways of thinking about the problem space\n\n- you've written a mid-sized django app\n- maybe you already have some caching.\n\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
apologies, promised\nstale cache from previous talks\n
\n
\n
\n
\n
\n
\n
\n
\n
planning and implementing is what we focus on\n
\n
\n
\n
\n
\n
\n
\n
\n
* what -- more on this\n\n* where rendered, could be multiple spots\n\n* Updates outside: Asynchronous: e.g. cron, sphinx. \nAsynchronous may be triggered by request-response cycle\n\n** can mix and match. beware dependencies. even of same type. two-phased template rendering\n- two phase template renderings\n\n
* what -- more on this\n\n* where rendered, could be multiple spots\n\n* Updates outside: Asynchronous: e.g. cron, sphinx. \nAsynchronous may be triggered by request-response cycle\n\n** can mix and match. beware dependencies. even of same type. two-phased template rendering\n- two phase template renderings\n\n
* what -- more on this\n\n* where rendered, could be multiple spots\n\n* Updates outside: Asynchronous: e.g. cron, sphinx. \nAsynchronous may be triggered by request-response cycle\n\n** can mix and match. beware dependencies. even of same type. two-phased template rendering\n- two phase template renderings\n\n
* what -- more on this\n\n* where rendered, could be multiple spots\n\n* Updates outside: Asynchronous: e.g. cron, sphinx. \nAsynchronous may be triggered by request-response cycle\n\n** can mix and match. beware dependencies. even of same type. two-phased template rendering\n- two phase template renderings\n\n
next up strategies for this by wiggins/kyle\n
\n
twitter sidebar asynch update every 5 minutes. fine.\nuser does x. x replaces y. user asks for x and gets y. problem.\n\nIf changes constantly, caching no value\n
twitter sidebar asynch update every 5 minutes. fine.\nuser does x. x replaces y. user asks for x and gets y. problem.\n\nIf changes constantly, caching no value\n
twitter sidebar asynch update every 5 minutes. fine.\nuser does x. x replaces y. user asks for x and gets y. problem.\n\nIf changes constantly, caching no value\n
twitter sidebar asynch update every 5 minutes. fine.\nuser does x. x replaces y. user asks for x and gets y. problem.\n\nIf changes constantly, caching no value\n
twitter sidebar asynch update every 5 minutes. fine.\nuser does x. x replaces y. user asks for x and gets y. problem.\n\nIf changes constantly, caching no value\n
twitter sidebar asynch update every 5 minutes. fine.\nuser does x. x replaces y. user asks for x and gets y. problem.\n\nIf changes constantly, caching no value\n
\n
!!! “string theory” - helpful to imagine reaching into a db (or persistent layer) with strings attached\n\n!!! reach into database with a join. cache in query. \n\nhigher up you cache, likely the more \n\nstrategy next\n
- Who in the crowd is currently using memcache in their Django applications?\n- What about Django's basic per-site caching?\n- Has anyone seen Noah Silas talk about Cache Rules Everything Around Me?\n- in that talk, he briefly mentions the source of his title, but doesn’t want to talk about it\n\n>> We ARE going to talk about Wu Tang Clan\n\n
- but only for this slide\n- they had the hit Cash Rules Everything Around Me\n\n
- If you want to discuss more, see me after the talk\n\n
- Back to Django\n\n- So does caching in Django just mean using memcache?\n\n- Well, it is commonly used and easy to set up\n- A lot of cache frameworks have documentation along the lines of "... developed specifically with the use of memcached in mind. "\n\nNext:\n- We'd like to talk about caching in the context of an imaginary website. \n- We will walk through the growth of this site and what issues we'd hit along the way\n\n\n
- As many of you may have experienced, we have a great idea, but we don't have the time or money to build a huge, scaling site from the very start\n- We shouldn't completely ignore scalability, but with limited resources, a site that launches is better than one that is prematurely optimized.\n- Also, marketing or management might have constraints on the project outside of development's control\n- We aren't here to wag our finger at you and tell you how you should have done things all along, we're here to walk through some real issues and present solutions\n
- So we have our site live\n- Although there isn't much data on it\n- And not many users\n- Perhaps a significant amount of the site traffic is from the development team testing the site.\n- it happens\n- Perhaps all of the actual business is from family and friends of the team.\n- But now that the site is live, the dev team has time to focus on the next phase of features.\n- Let's talk about ways that we can improve the site, hopefully to help prepare it for growth\n
\n- Of course, some of the primary concerns of the Django developers on the team are the responsiveness of our application, and the load it creates on the web server.\n\n- Lucky for us developers that like to fly by the seat of our pants, Django provides some extremely quick ways of adding basic per-site caching to every page. This basically means that if a page has been requested within the cache timeout, it will be served to any other users from the cache instead of the database\n\n- This is close to upstream caching like Varnish, but it’s still aware of things like the request user’s authentication status.\n\n- Hopefully everyone here is already past this level, but we will talk more about deploying memcache later.\n
\n- Of course, some of the primary concerns of the Django developers on the team are the responsiveness of our application, and the load it creates on the web server.\n\n- Lucky for us developers that like to fly by the seat of our pants, Django provides some extremely quick ways of adding basic per-site caching to every page. This basically means that if a page has been requested within the cache timeout, it will be served to any other users from the cache instead of the database\n\n- This is close to upstream caching like Varnish, but it’s still aware of things like the request user’s authentication status.\n\n- Hopefully everyone here is already past this level, but we will talk more about deploying memcache later.\n
\n- So then it happens. Your management lands a deal with a rival company and suddenly you have way more data than you anticipated.\n\n- The per-site caching helps with a lot of the site, but there are still some expensive queries happening, and probably on the index page\n
So what is a solution?\n- perhaps there are only a few expensive queries to make.\n- Just wrap a cache read/write around them, and they will be cached for subsequent\n\n- don’t name your cache key “my_cached_items”\n\n- this will result in keys generated as-needed, sprinkled througout the code\n\n
So what is a solution?\n- perhaps there are only a few expensive queries to make.\n- Just wrap a cache read/write around them, and they will be cached for subsequent\n\n- don’t name your cache key “my_cached_items”\n\n- this will result in keys generated as-needed, sprinkled througout the code\n\n
So what is a solution?\n- perhaps there are only a few expensive queries to make.\n- Just wrap a cache read/write around them, and they will be cached for subsequent\n\n- don’t name your cache key “my_cached_items”\n\n- this will result in keys generated as-needed, sprinkled througout the code\n\n
- Pros: quick\n- Cons: dirty\n
- One of the first issues this site is going to run into is what happens when there is a cache miss?\n\n- Pretending that our new expanded dataset is actually enticing customers, In the case of heavy traffic, a cache miss means many requests will trigger database reads until the cache can be filled. This stampede of requests is commonly referred to as a ‘Thundering Herd’.\n\n- Even though a small percentage of requests trigger a read, they are occurring at the same time. \n
\n- A solution to Thundering Herd is to not drop values completely from the cache, but to refresh the value when the first miss occurs, or when an object is deemed to be ‘stale’, and continue to provide the cached value to the rest of the herd until the value can be updated. We refer to this as the Publish Model, and we will talk more about it later.\n\n- One issue is that the rest of the herd will be getting the stale value, but it probably is fine for the short time.\n\n- The new value will probably take seconds, if that, to calculate and refresh the cache, but only one read will be sent to the database.\n\n
So even with \n\n- Often, requests are spread out evenly across a large dataset\n\n- Sometimes, users are not requesting anything seen in a long time. The data might be unique to that user\n\n- This doesn’t apply to our croupon site, but it would with Address lookups on a map, mail clients, etc\n\n- If you are running into these sorts of issues, then your site is moving along that spectrum of what is an app and what is a content site. Remember the slide from Adrian’s keynote yesterday on the sweet spot that Django aims to serve. It doesn’t reach all the way to the ‘app’ end of the spectrum\n\n- Luckily Croupon is solidly toward the content end, and is pretty well suited for a lot of the solutions we are talking about\n\nPASS TO WIGGINS to talk about deploying caching\n
Versioning your caches.\nSeparating your caches based on use.\nUsing consistent hashing algorithms.\nDeploying memcached with Elasticache.\n
- What happens when you need to incrementally push a code change that alters the way a cache value is generated?\n- You need a way to have the new code write to a different key so that the old values are preserved\n - staging and production\n- Versioning according to git commit hash is nice, but that means settings needs to know the hash before it is made, fabric is a good solution for this.\n
- What happens when you need to incrementally push a code change that alters the way a cache value is generated?\n- You need a way to have the new code write to a different key so that the old values are preserved\n - staging and production\n- Versioning according to git commit hash is nice, but that means settings needs to know the hash before it is made, fabric is a good solution for this.\n
- What happens when you need to incrementally push a code change that alters the way a cache value is generated?\n- You need a way to have the new code write to a different key so that the old values are preserved\n - staging and production\n- Versioning according to git commit hash is nice, but that means settings needs to know the hash before it is made, fabric is a good solution for this.\n
\nuse a separate cache for sessions\n
\nuse a separate cache for sessions\n
\nuse a separate cache for sessions\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
MOVING ON TO: elasticache\n
PASS TO KYLE to talk about optimizing cache\n
PASS TO KYLE to talk about optimizing cache\n
PASS TO KYLE to talk about optimizing cache\n
PASS TO KYLE to talk about optimizing cache\n
\n
\n
NOW ONTO PROFILING\n
NOW ONTO PROFILING\n
NOW ONTO PROFILING\n
NOW ONTO PROFILING\n
NOW ONTO PROFILING\n
\n
- I’m going to talk about three topics related to optimizing your cache\n- This is going to be very high-level\n
- I’m going to talk about three topics related to optimizing your cache\n- This is going to be very high-level\n
- I’m going to talk about three topics related to optimizing your cache\n- This is going to be very high-level\n
- Everyone should be using debug toolbar\n
- Everyone should be using debug toolbar\n
- Everyone should be using debug toolbar\n
\n- Croupon marketing decides that we need to have croupon counts peppered around the site to let customers know just how many croupons there are available in their area for each category. \n- Unfortunately, calculating these counts is very expensive. \n- The site may be lightly used, but each index page would generate a lot of joins and filters.\n-very expensive database lookups\n\n- > This is what I see in my head\n\n- The default behavior of cache is to let the process of the first read populate the cache\n-That’s for a static page that takes two seconds to load, but a cached version gets it down to 100 milliseconds\n- But that won’t work in this instance, because you don’t want any users to have to wait for these expensive lookups\n- Especially because these reads are from data that doesn’t change often\n
- so what is cache warming? It is filling the cache in the background, asynchronously with web requests\n\n- The solution is to warm the cache manually, not passively. Give the user what they requested, THEN do the work. Don’t make a user suffer to make your job easier.\n\n- Before the cache expires, refresh the data and reset the expiration date.\n\n- If the data in the cache is a list of items, it is easy to iterate over each item in a queryset.\n\n- For more complicated queries, write a custom manage.py command\n\n- All of this allows the synchronous web requests to remain fast for users\n
\n- manage.py command is simplest\n\n- Run the command just slightly more often than your cache timeout is set for (16 -> 15)\n\n- some caching frameworks want you to register with a warming task\n\n- That can be a quick solution, but it is better to be explicit than implicit. \n- If a different developer down the road wants to see what is being warmed, it is hard to trace back if you aren’t calling a function that explicitly lists what is being done\n
Here are some of the more popular frameworks\n\nHere’s a shameless plug for our own CacheModel\n\nYou can find the entire grid on django packages\n
- You use Cache Machine by inheriting from its CachingMixin and setting its CachingManager as your model’s default manager\n\n- It uses “flush lists” to keep track of the cached queries an object belongs to. Then it will iterate over that list and flush each one when an object is saved or deleted.\n- It also follows foreign keys and flushes them as well, so that’s nice\n\n- Cache Machine also includes a Jinja2 extension to cache template fragments based on the querysets used inside that fragment. That tag will get added to the flush lists, so it will get invalidated just like normal querysets.\n\n\n
- This is a popular framework and is updated fairly often. \n- So, of course, it works with Django 1.4\n\n- "Johnny provides a number of backends, all of which are subclassed versions of django builtins that cache “forever” when passed a 0 timeout."\n\n- Wiggins will talk about how this relates to the publish model\n- The important thing is that the framework gives you the flexibility to use various backends with the same code, much like the Django ORM does with various databases.\n\n\n
- Drop-in caching\n- Your model can just inherit from CacheModel and you get caching on your default manager\n\n
- Written by Noah Silas, who we’ve mentioned\n\n- It provides a controller that you can set on a model, then call that instead of calling your default manager\n\n- myModel.cache.get() instead of myModel.objects.get()\n\n\nPASS TO WIGGINS to talk about the Publish Model\n
The Publish Model - 10min (wiggins)\n - what is the publish model\n - how do you implement it?\n - cachemodel\n\n
\n
\n
- well defined keys in one DRY place\n- define functions that celery can use to publish to the cache\n- you need to pre-warm since the cache will be empty initially.\n
- well defined keys in one DRY place\n- define functions that celery can use to publish to the cache\n- you need to pre-warm since the cache will be empty initially.\n
- well defined keys in one DRY place\n- define functions that celery can use to publish to the cache\n- you need to pre-warm since the cache will be empty initially.\n
- well defined keys in one DRY place\n- define functions that celery can use to publish to the cache\n- you need to pre-warm since the cache will be empty initially.\n
- this is an example of how we would implement the publish model on croupon\n\n- cachekeys is a dictionary of tuples, (key_format_string, publish_function)\n\n- publish functions are decorated as celery tasks so they can be run async or inline.\n
- this is an example of how we would implement the publish model on croupon\n\n- cachekeys is a dictionary of tuples, (key_format_string, publish_function)\n\n- publish functions are decorated as celery tasks so they can be run async or inline.\n
- this is an example of how we would implement the publish model on croupon\n\n- cachekeys is a dictionary of tuples, (key_format_string, publish_function)\n\n- publish functions are decorated as celery tasks so they can be run async or inline.\n
- before we get into the weeds of the functions, here is how they are used.\n
- before we get into the weeds of the functions, here is how they are used.\n
- before we get into the weeds of the functions, here is how they are used.\n
- before we get into the weeds of the functions, here is how they are used.\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n
- key_fmt % kwargs is naive and you will need to do better cleaning of hash keys based on arguments\n\n- FOREVER_TIMEOUT can be 0 if you are using pylibmc, otherwise set it to one year...\n\n- we fall back to thundering herd if there is a cache miss using .apply()\n