Created
May 7, 2010 07:51
-
-
Save dound/393168 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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