Skip to content

Instantly share code, notes, and snippets.

@jvanasco
Last active September 15, 2016 21:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jvanasco/a83be3b3b536bc964477c1dbc830674c to your computer and use it in GitHub Desktop.
Save jvanasco/a83be3b3b536bc964477c1dbc830674c to your computer and use it in GitHub Desktop.
"""
After a bit of thought, I rewrote our response caching.
I stripped it into this gist in case anyone needs a starting point in the future. it's still rather specialized and hacky... but a bit cleaner than the earlier approach
The general overiew is this:
* We needed to cache responses AND perform some light operations. 99% of a rendered page is cachable as-is.
* We wanted to consolidate where the configuration is, perferalbly with the route_name declaration - but in this case it was better off with the caching code.
* The cached output should be rendered, which mades the `decorators` egress perfect... however some setup code happens in the __init__ phase of view-based classes and the 'decorators' ingress happens too soon.
Solution achieved via:
* This is implemented as a `decorators` argument to view_config.
* A local dict is used to map routes to page keys, and whether or not the matchdict or pagination should be used.
* Exceptions are cached in a modified format, unless the request fails a check for `request.is_cacheable`
* If a cached value is found, a request attribute `is_initialize_only` is set to True. this allows the __init__ phase of class based views to execute and raise `HandlerInitializeOnly`.
The `initialize_only` bit is the dirtiest part of this -- but it allows me to grab the decorator's ingress and still run some code for the views via a class based view's.
Perhaps someone will come up with a better method.
request properties
:is_in_generator: Boolean. If True, marks that we are in a cache generator. this will disable some renderings
:is_initialize_only: Boolean. If True, view based classes will only do their __init__, then raise this as "okay"
functions:
`deserialize_onpage_ids`
`serialize_onpage_ids`
"""
from pyramid.httpexceptions import HTTPException
from pyramid.response import Response
MAX_PAGINATED_CACHE = 5
_cached_mapping = {
# route_name: (page_key, use_matchdict, maybe_paginated ),
"main": ("Main", False, False, ),
}
class Uncacheable(Exception):
"""raise if you shouldn't cache"""
pass
class HandlerInitializeOnly(Exception):
"""raise if you shouldn't cache"""
pass
def cacheable_route_decorator(wrapped):
"""
This is preferable to ``@pregenerated_cache``
"""
def wrapper(context, request):
def generate_response():
"""consolidated response generation"""
response = wrapped(context, request)
return response
try:
# register generator status; this effects the interpolation
request.is_in_generator = True
_route_name = request.matched_route.name
if _route_name not in _cached_mapping:
# easy out!
raise Uncacheable()
# grab our cache mapping
(page_key, use_matchdict, maybe_paginated) = _cached_mapping[_route_name]
# generate a local key!
key = page_key
if use_matchdict:
key += '|' + ','.join([v for k, v in sorted(request.matchdict.items())])
if maybe_paginated:
# default to page1
_page = request.matchdict.get('page', 1)
if _page > MAX_PAGINATED_CACHE:
# easy out!
raise Uncacheable()
# only update the key if we're not using the matchdict already
if not use_matchdict:
key += '|p.%s' % _page
_cached = cache_utils.CACHE_REGIONS['pages'].get(key)
if _cached == cache_utils.NO_VALUE:
try:
response = generate_response()
response_text = response.body
response_status = response.status_code
except HTTPException, e:
response_text = "EXCEPTION"
if "Location" in e.headers:
response_text += "\nLocation||%s" % e.headers['Location']
response_status = e.status_code
response = e
except Exception as e:
log.debug("Exception in cacheable_route_decorator")
raise
if request.is_cacheable:
onpage_ids = serialize_onpage_ids(request, page_key)
_cached = (response_text, response_status, onpage_ids)
cache_utils.CACHE_REGIONS['pages'].set(key, _cached)
else:
(response_text, response_status, onpage_ids) = _cached
try:
# we have a _cached, but we need to know more about the object...
request.is_initialize_only = True
generate_response()
except HandlerInitializeOnly, e:
"""
this should `raise` on the `__init__`
which signifies that we are fully permissioned via the Handler
"""
pass
except HTTPException, e:
raise e
except Exception as e:
raise e
# analyze the response
if response_text[:9] == 'EXCEPTION':
response = HTTPException()
response.status_code = response_status
response_text = response_text.split("\n")[1:]
for h in response_text:
(k, v) = h.split("||")
response.headers[k] = v
raise response
else:
response = Response(response_text)
deserialize_onpage_ids(request, page_key, onpage_ids)
except Uncacheable, e:
response = generate_response()
return response
finally:
# deregister generator status
request.is_in_generator = False
if isinstance(response, HTTPException):
raise response
return response
return wrapper
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment