11
\$\begingroup\$

In my rumblings developing flask applications I found the caching decorator. The decorator is a short and well-written piece of code, but I feel like it misses the few points bellow:

  • It only caches on the server side and doesn’t leverage the client-side cache - meaning that clients still have to go and hit the server every single time they require a resource
  • It doesn’t respect the no-cache requirement from the client (although I understand why one may not want this, as it would mean busting cache every single time someone asks for it, thus creating the possibility of reducing the caching effort and benefits to zero)

The way I try to improve it is by having a decorator that easily allows me to define a caching-control policy per view, that involes both the client-side and server-side, by following the flow-chart bellow, taken from the HTTP Caching article by Google Guru Ilya Grigorik

enter image description here

The code for the decorator is:

import binascii
import datetime
from flask import Response, make_response
def cached(cacheable = False, must_revalidate = True, client_only = True, client_timeout = 0, server_timeout = 5 * 60, key='view/%s'):
 """
 @see https://jakearchibald.com/2016/caching-best-practices/
 https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching
 """
 def decorator(f):
 @wraps(f)
 def decorated_function(*args, **kwargs):
 cache_key = key % request.full_path # include querystring
 cache_policy = ''
 etag = ''
 if not cacheable:
 cache_policy += ', no-store' # tells the browser not to cache at all
 else:
 if must_revalidate: # this looks contradicting if you haven't read the article.
 # no-cache doesn't mean "don't cache", it means it must check
 # (or "revalidate" as it calls it) with the server before
 # using the cached resource
 cache_policy += ', no-cache'
 else:
 # Also must-revalidate doesn't mean "must revalidate", it
 # means the local resource can be used if it's younger than
 # the provided max-age, otherwise it must revalidate
 cache_policy += ', must-revalidate'
 if client_only:
 cache_policy += ', private'
 else:
 cache_policy += ', public'
 cache_policy += ', max-age=%d' % (client_timeout)
 headers = {}
 cache_policy = cache_policy.strip(',')
 headers['Cache-Control'] = cache_policy
 now = datetime.datetime.utcnow()
 client_etag = request.headers.get('If-None-Match')
 response = cache.get(cache_key)
 # respect the hard-refresh
 if response is not None and request.headers.get('Cache-Control', '') != 'no-cache':
 headers['X-Cache'] = 'HIT from Server'
 cached_etag = response.headers.get('ETag')
 if client_etag and cached_etag and client_etag == cached_etag:
 headers['X-Cache'] = 'HIT from Client'
 headers['X-Last-Modified'] = response.headers.get('X-LastModified')
 response = make_response('', 304)
 else:
 response = make_response(f(*args, **kwargs))
 if response.status_code == 200 and request.method in ['GET', 'HEAD']:
 headers['X-Cache'] = 'MISS'
 # - Added the headers to the response object instead of the
 # headers dict so they get cached too
 # - If you can find any faster random algorithm go for it.
 response.headers.add('ETag', binascii.hexlify(os.urandom(4)))
 response.headers.add('X-Last-Modified', str(now))
 cache.set(cache_key, response, timeout=server_timeout)
 response.headers.extend(headers)
 return response
 return decorated_function
 return decorator

The decorator can then by used like

@pages.route('/')
@cached(True, must_revalidate=True, client_only=False, client_timeout=120, server_timeout=5*60)
def index():
 """Serve client-side application shell."""
 return render_template('shell.html', model = get_default_model())

Note that the cache on the decorator is provided by Werkzeug and in this particular case its in-memory, but it could be redis, memcached and etc. since Werkzeug cache provides a common high-level API for them, while keeping implementation details on the low-level away from the developers eyes or worries.

I'd like to have your feedback on this, on whether it is a valuable improvement in comparison with the "standard" decorator, where it could be improved and simplified.

200_success
146k22 gold badges190 silver badges478 bronze badges
asked Nov 14, 2016 at 22:54
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

It looks like cache_policy only depends on the arguments to the cached decorator. So, the calculation of cache_policy can be moved out of decorated_function() and into cached() or decorator(). That way, it's not recalculated on every call to the function.

Use something longer than f for the function. It's easy to miss where the function gets called in the body of decorated_function.

answered Oct 4, 2023 at 8:14
\$\endgroup\$
0
\$\begingroup\$

feedback on this, on whether it is a valuable improvement

Yes. This is terrific.

I share your concern about whether it could be simplified. But alas, web caching rules are a bit complex.

answered Jan 7, 2023 at 2:53
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.