Skip to content

Instantly share code, notes, and snippets.

@dound
Created May 7, 2010 07:51
Show Gist options
  • Save dound/393168 to your computer and use it in GitHub Desktop.
Save dound/393168 to your computer and use it in GitHub Desktop.
application: yourappid
version: testcache
runtime: python
api_version: 1
handlers:
- url: /_ah/queue/deferred
script: $PYTHON_LIB/google/appengine/ext/deferred/handler.py
login: admin
- url: /.*
script: demo_main.py
"""Implements a request handler which caches the response to GET requests.
Big Picture:
* memcache is used to cache generated content.
* Task queue is used to regenerate the content when it expires.
* When a page expires, the old page is continued to be served until the new
page is ready (to minimize downtime).
* A new page will be generated only if the old page has expired AND someone is
requesting the page.
How to Use:
* For request handlers whose get() method you would like to cache, override
CachingRequestHandler instead of webapp.RequestHandler.
* At a minimum, override get_fresh(), get_path(), and get_temporary_page().
* Other methods can be overridden to customize cache timeout, headers, etc.
* Details are in the comments for each method.
"""
import os
import time
from google.appengine.api import memcache
from google.appengine.api.capabilities import CapabilitySet
from google.appengine.api.labs import taskqueue
from google.appengine.ext import deferred
from google.appengine.ext import webapp
DEFAULT_PAGE_CACHE_TIME_IN_SECONDS = 300
ON_LOCALHOST = ('Development' == os.environ['SERVER_SOFTWARE'][:11])
class CachingRequestHandler(webapp.RequestHandler):
def get(self):
# try to get the page from memcache first
cn = self.__class__.__name__
key = 'page:' + cn
page = memcache.get(key)
if not page:
# page isn't in the cache, so try to regenerate it
# but first make sure memcache is available
memcache_service = CapabilitySet('memcache', methods=['set','get'])
if not memcache_service.is_enabled():
# memcache is down: we have no choice but to generate the page
page = self.get_fresh(memcache_down=True)
else:
# the new page will be generated on the task queue soon: try to
# get the previous version of the page for now
key_stale = 'stalepage:' + cn
page = memcache.get(key_stale)
# use a lock so that if multiple users request the page at once,
# only one triggers the (potentially expensive) page generation.
lock_key = 'pagelock:' + cn
if memcache.add(lock_key, None, 30):
# we got the lock: asynchronously regenerate the page
interval = self.get_cache_ttl()
interval_num = int(time.time() / interval)
task_name = '%s-%d-%d' % (cn, interval, interval_num)
try:
if not page or ON_LOCALHOST:
# If we don't even have a stale page to present
# then just call it - the user has no choice but to
# wait for it. Also avoid using the task queue when
# on localhost as it requires manual triggering.
page = _regenerate_page(self.get_fresh, key, key_stale, lock_key, interval)
else:
deferred.defer(_regenerate_page_path, self.get_path(), key, key_stale, lock_key, interval, _name=task_name)
except (taskqueue.TaskAlreadyExistsError, taskqueue.TombstonedTaskError):
pass
elif not page:
# Someone else is already generating the page, but there is
# no stale page for us to return: generate something else.
page = self.get_temporary_page()
# got the page: send it to the user
self.write_headers_for_get_request()
self.response.out.write(page)
@staticmethod
def get_cache_ttl():
"""Returns the number of seconds to cache the generated page."""
return DEFAULT_PAGE_CACHE_TIME_IN_SECONDS
@staticmethod
def get_fresh(memcache_down=False):
"""Override this method to return the contents of the page.
memcache_down will be True if memcache is down. You might use this as
a cue to generate a light version of the page with an error message
since there will be no caching (e.g., this will be called for every
request until memcache is back).
"""
return ''
@staticmethod
def get_path():
"""This method returns a string to the class on which this method is
defined. This is required so that the deferred library (task queue
helper) can find your class and execute its get_fresh() method to create
a new page.
"""
return 'CachingRequestHandler.CachingRequestHandler'
def get_temporary_page(self, refresh_delay=2):
"""Returns a page to use if the actual page is not available. This
means the page is in the process of being generated. If it is not too
expensive, consider returning self.get_fresh() (i.e., just build the
page an extra time). Otherwise, you probably want to override this to
provide a nicer looking page for the user to look at while they wait for
the actual page to be ready.
"""
return '<html><head><meta http-equiv="refresh" content="%d;url=%s"></head><body><p>One moment please ...</p></body></html>' % (refresh_delay, self.request.path)
def write_headers_for_get_request(self):
"""Writes the headers for the GET response."""
self.response.headers['Content-Type'] = 'text/html'
def _istring(import_name):
"""Imports an object based on a string.
@param import_name the dotted name for the object to import.
@return imported object
"""
module, obj = import_name.rsplit('.', 1)
# __import__ can't handle unicode strings in fromlist if module is a package
if isinstance(obj, unicode):
obj = obj.encode('utf-8')
return getattr(__import__(module, None, None, [obj]), obj)
def _regenerate_page(get_fresh, key, key_stale, lock_key, ttl):
"""Regenerates a page for the specified CachingRequestHandler class."""
page = get_fresh()
memcache.set(key, page, ttl)
memcache.set(key_stale, page, ttl+30) # leave time to regen the page
memcache.delete(lock_key)
return page
def _regenerate_page_path(path_to_crh, key, key_stale, lock_key, ttl):
"""Wrapper which takes a path a CachingRequestHandler subclass."""
cls = _istring(path_to_crh)
return _regenerate_page(cls.get_fresh, key, key_stale, lock_key, ttl)
import time
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from CachingRequestHandler import CachingRequestHandler
class Cacher(CachingRequestHandler):
VER = 0
@staticmethod
def get_path():
return 'main.Cacher'
@staticmethod
def get_fresh(memcache_down=False):
time.sleep(2.5) # pretend the page is slow to generate
Cacher.VER += 1
return 'slow page generated #' + str(Cacher.VER)
@staticmethod
def get_cache_ttl():
"""Returns the number of seconds to cache the generated page. Much
lower than a typical value for demo purposes."""
return 5
application = webapp.WSGIApplication([('/', Cacher)], debug=True)
def main():
run_wsgi_app(application)
if __name__ == '__main__': main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment