# Implementation of lru_cache for daily “top categories” query

In my website, I want to show top categories by products' view / sell counts.

Since this is not something changes every hour, I want to fetch real results only once a day from database (may be once a week). Here is what I have done:

def global_context(request):
## ... some other queries

today = datetime.datetime.now().day
top_cats = top_categories(today)  # hit once a day
return {'top_categories': top_cats}

@lru_cache(maxsize=2)
def top_categories(day):
# fetch first 50 top-selling products
top_products = Product.objects.all().order_by('-sell_count')[:50]

# get first 10 unique categories
categories = list(set([product.categori for product in top_products]))[:10]

return categories


This code is working. (I have tested with per minute version). But I wonder,

1. Is this correct way to achieve my goal?

2. When currsize reached to maxsize nothing happened. Code kept working. Why?

3. Should I manually reset / clear cache with cache_clear?

• There is one concern I have. Is not the lru_cache the "in-process" cache in this case - every single process of your django app will eventually hold a copy of the cache? I mean, you should probably use django built-in caching mechanisms with an external storage (database or a, say, memcached or redis key-value store) Thanks. – alecxe Apr 3 '17 at 15:26
• @alecxe I'm not very good at Django, but I just made two views that call the same wrapped function, the wrapper just prints some text to check it's been called once, and the function sleeps for 10 seconds and return a string to display. The wrapper was only called once. Do you have any examples where the wrapper would be called more than once? – Peilonrayz Apr 3 '17 at 16:20
• @Peilonrayz I guess it is okay to have lru_cache for development because if you run this on, for example, a django development server, it'll be single-process - the results will be consistent and there will be only a single copy of the cache. But, if you have, say, apache with multiple processes serving your app, you'll have each process holding it's own cache +, as Gareth mentioned, there could be inconsistencies in what is inside this cache as well. Thanks. – alecxe Apr 3 '17 at 16:39

1. You state your goal as wanting to show:

top categories by products' view / sell counts

but what you actually show is:

ten categories chosen at random from the categories that contain a product in the top 50 products by sell count

which is not the same at all.

Assuming your description in the post is what you really want, then you need a query like this:

# Top 10 categories by product sell count plus view count.
return (Category.objects
.annotate(score=Sum('product__sell_count') + Sum('product__view_count'))
.order_by('-score')[:10])


I'm assuming here that you have a Category model with a many-to-many relation named product with the Product model, and that the Product model has a view_count field, and that adding the sell count and the view count is the appropriate scoring function. If your actual models or requirements are different then you'll have to update the query appropriately.

2. If functools.lru_cache can't find the requested item in its cache, then it calls the function as usual and puts the result in the cache, throwing out the oldest cache entry if necessary to make room. So even if the cache is full, the function still works and returns the correct results. You could set maxsize=1 here since you only need to remember the most recent result.

3. The only reasons to manually clear the cache would be (i) it's using too much memory; or (ii) the cached results may be out of date and need to be recomputed using up-to-date information.

Further comments on the design:

1. Each Django process ends up having a cache of the "top categories" query result. But this might differ according to exactly when the query was issued, leading to different processes having different top categories. If this is a problem for your application, you probably want to use a single application cache, as noted by alexce in comments.

2. You probably want to incorporate some measure of time in your "top categories" query, for example aggregating over recent sales and views rather than all sales and views. (But maybe this is already done and recorded in the sell_count field?)

• Comments below my question, your and @alecxe 's answers.. Today, I learned a lot from only one question. I think i should visit this website more than SO. Thank you. – alix Apr 3 '17 at 16:52

From what I understand, the lru_cache is not the right tool when it comes to caching things in a production web application. Depending on how the web application is deployed, you would usually end up having multiple web server processes serving your application. Which means, that the LRU cache will be created in the memory space of every process. It's like if you would use the local-memory caching:

Note that each process will have its own private cache instance, which means no cross-process caching is possible. This obviously also means the local memory cache isn’t particularly memory-efficient, so it’s probably not a good choice for production environments. It’s nice for development.

In other words, you should not be caching with Python, cache with Django built-in caching mechanisms in an external storage - a database, or a memcached or redis instance. For instance, in a simple case to avoid external dependencies, you can just use a cache table in your database:

CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_cache_table',
}
}


Then, you can cache the result of top_categories in this table, letting Django handle the cache record expiration.

• As i mentioned in my above comment, thank you for detailed explanation. I wish i could accept more than one answers. – alix Apr 3 '17 at 16:54