jamtoday.org

Jul 23

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