Yet Another GAE Memoize Decorator
When building a highly scalable website, one of the most important tools available is caching. While there are certainly times where you don’t want to cache a method, I’ve found that the I’m even starting to carve through my alloted free App Engine memcache quota.
I’ve used a variety of memcache utilities, and have never quite been pleased with any of them. I figured that I just needed to write my own, both for the enjoyment of meta-programming, and just so that I’d be intimately familiar with the decisions behind it.
One of the biggest potential gotchas lays with the need to convert arguments for the method into a string. For instance, while you might be tempted to simply use a Profile entity as an argument for a memoize handler, it would look like this after being converted to a string:
<model.user.Profile object at 0x92fbfec>
If you later call the same method with the exact same entity, your memcache key would now look like this:
<model.user.Profile object at 0x92f9aac>
And then your memcache.get() call wouldn’t find a match for the new key.
Of course, this is elementary to anyone who has dived into Python, but the memcache utilities I’ve used have not done a good job of being strict about just not working if you give it something that you shouldn’t. They try to make a guess as to what you’d like to do, or worse, just silently do what they’re told without throwing an exception.
I like my utilities to be more of the prescriptive whistle-blowing variety, so today I wrote a memoize utility that does indeed barf in your face if you give it an argument whose type it does not strictly approve. It also allows for some kwargs like ‘force_run’ that override the caching behavior.
Licensed under Apache 2.0. Repo at http://github.com/jamslevy/gae_memoize/tree/master
import logging
import os
from google.appengine.api import memcache
ACTIVE_ON_DEV_SERVER = False
def memoize(time=1000000, force_cache=False):
"""Decorator to memoize functions using memcache.
Optional Args:
time - duration before cache is refreshed
force_cache - forces caching on dev_server (useful for APIs, etc.)
force_run - forces fxn to run and cache to refresh
Usage:
@memoize(86400)
def updateAllEntities(key_name, params, force_run=False):
entity = Model.get_by_key_name(key_name)
for param in params.items():
setattr(entity, param.key(), param.value())
db.put(entity)
"""
def decorator(fxn):
def wrapper(*args, **kwargs):
approved_args=['Link', 'Key', 'str', 'unicode', 'int','float', 'bool']
arg_string = ""
for arg in args:
if type(arg).__name__ in approved_args:
arg_string += str( arg )
else: raise UnsupportedArgumentError(arg)
for kwarg in kwargs.items():
if type(kwarg[1]).__name__ in approved_args:
arg_string += str( kwarg[1] )
else: raise UnsupportedArgumentError(arg)
key = fxn.__name__ + arg_string
logging.debug('caching key: %s' % key)
data = memcache.get(key)
if Debug(): # on dev server
if not ACTIVE_ON_DEV_SERVER:
return fxn(*args, **kwargs)
if kwargs.get('force_run'):
logging.info("forced execution of %s" % fxn.__name__)
elif data:
if data.__class__ == NoneVal:
data = None
return data
data = fxn(*args, **kwargs)
if data is None: data = NoneVal()
memcache.set(key, data, time)
return data
return wrapper
return decorator
""" Util Methods """
class UnsupportedArgumentError(Exception):
''' An unsupported argument has been passed to Memoize fxn '''
def __init__(self, value):
self.arg = value
def __str__(self):
return repr(type(self.arg).__name__ + " is not a supported arg type")
def Debug():
'''return True if script is running in the development envionment'''
return 'Development' in os.environ['SERVER_SOFTWARE']
""" Singleton Classes """
class NoneVal():
''' A replacement for None, so a memoized fxn can return a None val
without making the Memoize fxn assume that the "None" means there
isn't a cached value '''
pass



