Commits

Chia-Huan Wu  committed c459e44

Imported twicecache revision 2 from http://code.google.com/p/twicecache.

  • Participants

Comments (0)

Files changed (9)

+Twice is a proxy server that uses caching and partial rendering to reduce load on application servers.
+
+Features at a Glance:
+
+    * In-memory cache
+    * Basic templating language to render dynmaic page elements
+    * Intelligent reverse proxy (Squid like functionality)
+    * HTTP header and cookie inspection 
+
+Like what you see? Keep reading...
+Detailed Overview:
+
+Twice is a caching reverse-proxy webserver with a templating language and plugable page element rendering engine. You can configure Twice on a per page basis by adding extra HTTP headers to your application server's responses.
+
+It is best to run Twice behind hardened web server (like Apache, Lighttpd, or Nginx). These programs are better suited to serve static content like images and css files. Other requests hit Twice, which serves cached responses when it can. If Twice can't serve a page, it will proxy the request through to the application server(s) and intelligently cache the response.
+Case Study:
+
+At Justin.tv we use Twice to serve the majority of dynamic page views (to the tune of 6-8 million pages per day). Here are some of the things it does:
+
+    * Temporarily caches pages that update frequently
+    * Renders cached pages in foreign languages by inspecting HTTP headers
+    * Renders usernames at the top of every page by inspecting browser cookies
+    * Renders page view counts into cached pages on demand by reading memcached keys 
+
+Twice cut our peak application server load by 78% and reduced our average page load time by 50% overnight.
+Bugs:
+	rewrite_host causes things to never cache (fixed?)
+		
+Todo:
+	Prevent request pileup
+	Properly refetch pages that time out
+	Increase default request timeout
+	Add reconnect code to memcache library
+	Cache dependencies for cached pages so they don't get scanned on every request
+	syntax for either purging or mark dirty
+"""
+
+    File: cache_internal.py
+    Description: 
+    
+        This module uses a Python dictionary to implement a simple in-memory Twice cache.
+
+    Author: Kyle Vogt
+    Copyright (c) 2008, Justin.tv, Inc.
+    
+"""
+
+from twisted.python import log
+from twisted.protocols.memcache import MemCacheProtocol
+from twisted.internet import protocol, reactor
+import random, time
+
+class TwiceCache:
+    """ Base class for implementing a Twice Cache"""
+    
+    def __init__(self, config):
+        self.config = config
+        
+    def ready(self):
+        "Call when the cache is online"
+        pass
+        
+    def set(self, dictionary, time = None):
+        "Store value(s) supplied as a python dict for a certain time"
+        pass
+        
+    def get(self, keylist):
+        "Retreive a list of values as a python dict"
+        return {}
+        
+    def flush(self):
+        "Delete all keys"
+        pass
+        
+class InternalCache(TwiceCache):
+    "Implements a Twice Cache using a Python dictionary"
+    
+    def __init__(self, config):
+        Cache.__init__(self, config)
+        self.cache = {}
+        self.ready()
+        
+    def ready(self):
+        limit = self.config.get('memory_limit')
+        if not limit:
+            log.msg('WARNING: memory_limit not specified, using 100MB as default')
+            self.config['memory_limit'] = 100000
+        log.msg("CACHE_BACKEND: Using %s MB in-memory cache" % limit)
+        
+    def set(self, dictionary, time = None):
+        for key, val in dict(dictionary.items()):
+            element = {
+                'expires_on' : time.time() + (time or 0),
+                'element' : val
+            }
+            dictionary[key] = element
+        self.cache.update(element)
+            
+    def get(self, keylist):
+        if not isinstance(keylist, list): keylist = [keylist]
+        output = {}
+        for key in keylist:
+            element = self.cache.get(key)
+            if element:
+                if time.time() > element['expires_on']:
+                    output[key] = None
+                else:
+                    output[key] = element['element']
+        return output
+        
+    def delete(self, keylist):
+        for key in keylist:
+            try:
+                del self.cache[key]
+            except:
+                pass
+        
+    def flush(self):
+        self.cache = {}
+        
+class MemcacheCache(TwiceCache):
+    "Implements a Twice Cache using a memcache server"
+
+    def __init__(self, config):
+        Cache.__init__(self, config)
+        server = config['cache_server']
+        connection_pool_size = int(config.get('cache_pool', 1))
+        log.msg('Creating memcache connection pool to server %s...' % server)
+        self.pool = []
+        # Import pickling library
+        try:
+            import cPickle as pickle
+        except ImportError:
+            log.msg('cPickle not available, using slower pickle library.')
+            import pickle
+        # Parse server string
+        try:
+            self.host, self.port = server.split(':')
+        except:
+            self.host = server
+            self.port = 11211
+        # Make connections
+        defers = []
+        for i in xrange(connection_pool_size):
+            d = protocol.ClientCreator(reactor, MemCacheProtocol).connectTCP(self.host, int(self.port))
+            d.addCallback(self.add_connection)
+            defers.append(d)
+        defer.DeferredList(defers).addCallback(self.ready)        
+        
+    def add_connection(self, result=None):
+        log.msg('CACHE_BACKEND: Connected to memcache server at %s:%s' % (self.host, self.port))
+        self.pool.append(result)
+
+    def ready(self, result=None):
+        log.msg('CACHE_BACKEND: Memcache pool complete')
+        
+    def cache_pool(self):
+        "Random load balancing across connection pool"
+        return random.choice(self.pool)
+
+    def set(self, dictionary, time = None):
+        pickled_dict = dict([(key, pickle.dumps(val)) for key, val in dictionary.items() if val is not None])
+        connection = self.cache_pool()
+        #log.msg('SET on cache %s' % cache)
+        if len(pickled_dict):
+            return connection.set_multi(pickled_dict, expireTime = time)
+        else:
+            return {}
+
+    def get(self, keylist):
+        if not isinstance(keylist, list): keylist = [keylist]
+        #log.msg('keylist: %s' % keylist)
+        connection = self.cache_pool()
+        #log.msg('GET on cache %s' % cache)
+        return connection.get_multi(keylist).addCallback(self._format, keylist)
+        
+    def delete(self, keylist):
+        for key in keylist:
+            self.cache_pool().delete(key)
+        
+    def _format(self, results, keylist):
+        "Return a dictionary containing all keys in keylist, with cache misses as None"
+        output = dict([(key, results[1].get(key, None) and pickle.loads(results[1][key])) for key in keylist])
+        #log.msg('Memcache results:\n%s' % repr(output))
+        return output
+        
+    def flush(self):
+        self.cache_pool().flushAll()
+        
+"""
+
+    File: engine.py
+    Description: 
+    
+        Rendering engine.
+
+    Author: Kyle Vogt
+    Copyright (c) 2008, Justin.tv, Inc.
+    
+"""
+
+from twisted.internet import reactor, protocol, defer
+from twisted.python import log
+import traceback, urllib, time
+import cache, http
+
+class DataStore:
+    
+    # Element types to fetch on every request
+    prefetch_types = ['session']
+    # Element types that depend on results of a prefetch element
+    prefetch_dependent_elements = []
+    
+    # Status codes
+    uncacheable_status = [500, 502, 503, 504, 307]
+    short_status = [404, 304]
+    
+    def __init__(self, config):
+        self.config = config   
+
+        # Mecache Backend
+        from twisted.protocols.memcache import MemCacheProtocol, DEFAULT_PORT
+        try:
+            server, port = config.get('backend_memcache').split(':')
+        except:
+            server = config.get('backend_memcache')
+            port = DEFAULT_PORT
+        d = protocol.ClientCreator(reactor, MemCacheProtocol).connectTCP(server, int(port))
+        d.addCallback(self.memcacheConnected)
+        
+        # Database Backend
+        from twisted.enterprise import adbapi
+        log.msg('Conneting to db...')
+        try:
+            self.db = adbapi.ConnectionPool("pyPgSQL.PgSQL", 
+                database=config.get('backend_db_name', ''), 
+                host=config.get('backend_db_host', '127.0.0.1'), 
+                user=config.get('backend_db_user', ''), 
+                password=config.get('backend_db_pass', ''),
+                cp_noisy=True,
+                cp_reconnect=True,
+                cp_min=config.get('backend_db_pool_min', 1),
+                cp_max=config.get('backend_db_pool_max', 1),
+            )
+            log.msg("Connected to db.")
+        except ImportError:
+            log.msg("Could not import database library")
+            traceback.print_exc()
+        except:
+            log.msg("Unable to connect to database")
+            traceback.print_exc()
+            
+         # HTTP Backend
+        try:
+            self.backend_host, self.backend_port = self.config['backend_appserver'].split(':')
+            self.backend_port = int(self.backend_port)
+        except:
+            self.backend_host = self.config['backend_appserver']
+            self.backend_port = 80
+            
+        # Cache Backend
+        log.msg('Initializing cache...')
+        cache_type = config['cache_type'].capitalize() + 'Cache'
+        self.cache = getattr(cache, cache_type)(config)     
+        
+        # Memorize variants of a uri
+        self.uri_lookup = {}
+        
+    # Init status
+
+    def memcacheConnected(self, proto):
+        log.msg('Memcache connection success.')
+        self.proto = proto
+
+    def dbConnected(self, db):
+        log.msg('Database connection success.')
+        self.db = db
+
+    def viewdbConnected(self, viewdb):
+        log.msg("Viewdb connection success.")
+        self.viewdb = viewdb
+
+    def get(self, keys, request):
+        "Get, cache, and return elements"
+        d = defer.maybeDeferred(self.cache.get, keys)
+        d.addCallback(self.handleMisses, request)
+        d.addErrback(self.getError)
+        return d
+        
+    def delete(self, keys):
+        "Delete elements from cache"
+        if not isinstance(keys, list): keys = [keys]
+        self.cache.delete(keys)
+        
+    def flush(self):
+        "Flush entire cache"
+        self.cache.flush()
+
+    def handleMisses(self, dictionary, request):
+        "Process hits, check for validity, and fetch misses or invalids"
+        missing_deferreds = []
+        missing_elements = []
+        for key, value in dictionary.items():
+            if value is None:
+                log.msg('MISS [%s]' % key)
+                d = getattr(self, 'fetch_' + self.elementType(key))(request, self.elementId(key))
+                missing_deferreds.append(d)
+                missing_elements.append(key)
+            elif not getattr(self, 'valid_' + self.elementType(key))(request, self.elementId(key), value):
+                log.msg('INVALID [%s]' % key)
+                d = getattr(self, 'fetch_' + self.elementType(key))(request, self.elementId(key))
+                missing_deferreds.append(d)
+                missing_elements.append(key)
+            else:
+                log.msg('HIT [%s]' % key)
+        # Wait for all items to be fetched
+        if missing_deferreds:
+            deferredList = defer.DeferredList(missing_deferreds)
+            deferredList.addCallback(self.returnElements, dictionary, missing_elements)
+            return deferredList
+        else:
+            return defer.succeed(dictionary)
+        
+    def returnElements(self, results, dictionary, missing_elements):
+        if not isinstance(results, list): results = [results]
+        uncached_elements = dict([(key, results.pop(0)[1]) for key in missing_elements])
+        dictionary.update(uncached_elements)
+        return dictionary
+
+    def getError(self, dictionary):
+        log.msg('uh oh! %s' % dictionary)
+        traceback.print_exc()
+        
+    # Hashing
+            
+    def elementHash(self, request, element_type, element_id = None):
+        "Hash function for elements"
+        return getattr(self, 'hash_' + element_type.lower())(request, element_id)
+        
+    def elementType(self, key):
+        return key.split('_')[0]
+        
+    def elementId(self, key):
+        return '_'.join(key.split('_')[1:])
+        
+    # Page
+    
+    def hash_page(self, request, id=None, cookies = []):
+        # Hash the request key
+        key = 'page_' + (request.getHeader('x-real-host') or request.getHeader('host')) + request.uri
+        # Internationalization salt
+        if self.config.get('hash_lang_header'):
+            header = request.getHeader('accept-language') or self.config.get('hash_lang_default', 'en-us')
+            if header:
+                try:
+                    lang = header.replace(' ', '').split(';')[0].split(',')[0].lower()
+                    key += '//' + lang
+                except:
+                    traceback.print_exc()
+        if cookies:
+            # Grab the cookies we care about from the request
+            found_cookies = []
+            for cookie in cookies:
+                val = request.getCookie(cookie)
+                if val:
+                    found_cookies.append('%s=%s' % (cookie, val))
+            # Update key based on cookies we care about
+            if found_cookies:
+                key += '//' + ','.join(found_cookies)
+        return key
+    
+    def fetch_page(self, request, id):
+        # Tell backend that we are Twice and strip cache-control headers
+        request.setHeader(self.config.get('twice_header'), 'true')
+        request.removeHeader('cache-control')
+        # Make the request
+        sender = http.HTTPRequestSender(request)
+        sender.noisy = False
+        reactor.connectTCP(self.backend_host, self.backend_port, sender)
+        return sender.deferred.addCallback(self.extract_page, request).addErrback(self.page_failed, request)
+        
+    def valid_page(self, request, id, value):
+        "Determine whether the page can be served stale"
+        now = time.time()
+        # Force refetch of very stale (3x cache_control value) pages
+        if now > value['expires_on'] + value['cache_control'] * 3:
+            log.msg('STALE-HARD [%s]' % id)
+            return False
+        # Sever semi-stale pages but refresh in the background
+        elif now > value['expires_on']:
+            log.msg('STALE-SOFT [%s]' % id)
+            self.fetch_page(request, id)
+            return True
+        # Valid page
+        else:
+            return True
+        
+    def page_failed(self, response, request):
+        log.msg('ERROR: Could not retrieve [%s]' % request.uri)
+        response.printBriefTraceback()
+        # TODO: Return something meaningful!
+        return ''
+        
+    def extract_page(self, response, request):
+
+        # Extract uniqueness info
+        cookies = sorted((response.getHeader(self.config.get('cookies_header')) or '').split(','))
+        key = self.hash_page(request, cookies = cookies)
+
+        # Store uri variant
+        if key not in self.uri_lookup.setdefault(request.uri, []):
+            log.msg('Added new varient for %s: %s' % (request.uri, key))
+            self.uri_lookup[request.uri].append(key)
+
+        # Override for non GET's
+        if request.method.upper() not in ['GET']:
+            log.msg('NO-CACHE (Method is %s) [%s]' % (request.method, key))
+            cache = False
+            cache_control = 0
+        else:    
+            # Cache logic
+            cache_control = response.getCacheControlHeader(self.config.get('cache_header')) or 0
+            if response.status in self.uncacheable_status:
+                log.msg('NO-CACHE (Status is %s) [%s]' % (response.status, key))
+                cache = False
+            elif response.status in self.short_status:
+                log.msg('SHORT-CACHE (Status is %s) [%s]' % (response.status, key))
+                cache = True
+                cache_control = 30
+            elif cache_control and cache_control > 0:
+                log.msg('CACHE [%s] (for %ss)' % (key, cache_control))
+                cache = True
+            else:
+                log.msg('NO-CACHE (No cache data) [%s]' % key)
+                cache = False
+
+        # Actual return value  
+        value =  {
+            'dependencies' : [],
+            'response' : response,
+            'expires_on' : time.time() + cache_control,
+            'cache_control' : cache_control
+        }
+        if cache:
+            response.cookies = []
+            self.cache.set({key : value}, cache_control + 86400) # Keep pages for up to 24 hours
+        return value
+        
+    # Memcache
+    
+    def hash_memcache(self, request, id):
+        return 'memcache_' + id
+    
+    def fetch_memcache(self, request, id):
+        log.msg('Looking up memcache %s' % id)
+        return self.proto.get(id).addCallback(self.extract_memcache, request, id)  
+        
+    def extract_memcache(self, result, request, id):
+        value = result and result[1]
+        self.cache.set({self.hash_memcache(request, id) : value}, 30) # 30 seconds
+        return value
+        
+    def valid_memcache(self, request, id, value):
+        return True
+                
+    def incr_memcache(self, key):
+        log.msg('Incrementing memcache %s' % key)
+        return self.proto.increment(key)
+        
+    def set_memcache(self, key, val):
+        log.msg('Setting memcache %s' % key)
+        return self.proto.set(key, val)
+                
+    # Session    
+    
+    def hash_session(self, request, id):
+        id = self._read_session(request)
+        if id:
+            return 'session_' + id
+        else:
+            return ''
+      
+    def fetch_session(self, request, id):
+        id = self._read_session(request)
+        return self.db.runInteraction(self._session, id).addCallback(self.extract_session, request, id)
+    
+    def extract_session(self, result, request, id):
+        if len(result[0]):
+            output = result[0][0]
+        else:
+            output = {}
+        self.cache.set({self.hash_session(request, id) : output}, 86400) # 24 hours
+        return output
+    
+    def valid_session(self, request, id, value):
+        return True
+    
+    def _read_session(self, request):
+        "Extract session id from the HTTP request"
+        return urllib.unquote(request.getCookie('session_cookie') or '')
+
+    def _session(self, txn, id):
+        log.msg('Looking up session %s' % id)
+        users_query = "select * from users where session_cookie = '%s'" % id
+        log.msg('Running query: %s' % users_query)
+        txn.execute(users_query)
+        return txn.fetchall()
+"""
+
+    File: handler.py
+    Description: 
+    
+        Main HTTP request handler.
+
+    Author: Kyle Vogt
+    Copyright (c) 2008, Justin.tv, Inc.
+    
+"""
+
+from twisted.internet import reactor, defer
+from twisted.python import log
+import sys, urllib, time, re, traceback
+import parser, engine, http, cache
+
+class RequestHandler(http.HTTPRequestDispatcher):
+    
+    def __init__(self, config):
+
+        # Caches and config
+        self.config = config
+        
+        # Template format
+        self.specialization_re = re.compile(self.config['template_regex'])
+        
+        # Data Store
+        log.msg('Initializing data store...')
+        self.store = storage.DataStore(config)
+                            
+    def objectReceived(self, connection, request):
+        "Main request handler"
+        # Handle mark dirty requests
+        if request.getHeader(self.config.get('purge_header')) is not None:
+            self.markDirty(connection, request)
+            return
+        # Handle time requests
+        if 'live/time' in request.uri:
+            connection.sendCode(200, str(time.time()))
+            return
+        # Check cache
+        else:
+            # Overwrite host field
+            real_host = self.config.get('rewrite_host', request.getHeader('x-real-host'))
+            if real_host:
+                request.setHeader('host', real_host)
+            
+            # Add in prefetch keys
+            keys = [self.store.elementHash(request, 'page')]
+            session_key = self.store.elementHash(request, 'session')
+            if session_key:
+                keys.append(session_key)
+
+            # Retrieve keys
+            log.msg('PREFETCH: %s' % keys)
+            self.store.get(keys, request).addCallback(self.checkPage, connection, request)
+                        
+# ---------- CACHE EXPIRATION -----------
+            
+    def markDirty(self, connection, request):
+        "Mark a uri as dirty"
+        uri = request.uri
+        try:
+            kind = request.getHeader(self.config.get('purge_header')).lower()
+        except:
+            log.msg('Could not read expiration type: %s' % repr(request.getHeader(self.config.get('purge_header'))))
+            return
+        log.msg("Expire type: %s, arg: %s" % (kind, uri))
+        # Parse request
+        if kind == '*':
+            self.store.flush()
+            log.msg('Cleared entire cache')
+        elif kind == 'url':
+            try:
+                keys = self.store.uri_lookup[uri]
+                self.store.delete(keys)
+                del self.store.uri_lookup[uri]
+                log.msg('Deleted all variants of %s' % uri)
+            except:
+                pass
+        elif kind == 'session':
+            try:
+                types = ['favorite', 'subscription', 'session']
+                keys = ['%s_%s' % (t, uri[1:]) for t in types]
+                self.store.delete(keys)
+                log.msg('Deleted session-related keys: %s' % keys)
+            except:
+                pass
+        else:
+            try:
+                key = kind + '_' + uri[1:]
+                self.store.delete(key)
+                log.msg('Deleted %s_%s' % (kind, uri[1:]))
+            except:
+                pass
+        # Write response
+        connection.sendCode(200, "Expired %s_%s" % (kind, uri))
+        return True       
+                
+# ---------- CLIENT RESPONSE -----------  
+
+    def checkPage(self, elements, connection, request, extra = {}):
+        "See if we have the correct version of the page"        
+        # Process cookies
+        response = [val for key, val in elements.items() if key.startswith('page_')][0]['response']
+        cookies = sorted((response.getHeader(self.config.get('cookies_header')) or '').split(','))
+        key = self.store.hash_page(request, cookies = cookies)
+            
+        # If the page we fetched doesn't have the right cookies, try again!
+        if key != self.store.hash_page(request):
+            self.store.get(key, request).addCallback(self.scanPage, connection, request, extra = elements)
+        else:
+            self.scanPage(elements, connection, request)
+
+    def scanPage(self, elements, connection, request, extra = {}):
+        "Scan for missing elements"
+        elements.update(extra)
+        logged_in = [True for key, value in elements.items() if key.startswith('session_') and value is not None]
+        data = [val for key, val in elements.items() if key.startswith('page_')][0]['response'].body
+        matches = self.specialization_re.findall(data)
+        missing_keys = []
+        for match in matches:
+            # Parse element
+            try:
+                parts = match.strip().split()
+                command = parts[0].lower()
+                element_type = parts[1].lower()
+                element_id = parts[2]
+            except:
+                traceback.print_exc()
+                continue
+            if element_type not in ['page', 'session']:
+                if element_type in ['memcache', 'viewdb'] or logged_in:
+                    key = self.store.elementHash(request, element_type, element_id)
+                    if key and key not in missing_keys:
+                        missing_keys.append(key)
+        if missing_keys:
+            d = self.store.get(missing_keys, request)
+            d.addCallback(self.renderPage, connection, request, elements)
+        else:
+            self.renderPage({}, connection, request, elements)
+
+    def renderPage(self, new_elements, connection, request, elements):
+        "Write the page out to the request's connection"
+        elements.update(new_elements)
+        for etype in ['page', 'session', 'favorite', 'subscription']:
+            eitems = [val for key, val in elements.items() if key.startswith(etype)]
+            if eitems:
+                eitems = eitems[0]
+            else:
+                eitems = {}
+            setattr(self, 'current_' + etype, eitems)
+            #log.msg('Current %s: %s' % (etype, eitems))
+
+        for etype in ['memcache', 'viewdb']:
+            #log.msg('elements: %s' % elements.items())
+            eitems = dict([(self.store.elementId(key), val) for key, val in elements.items() if key.startswith(etype)])
+            setattr(self, 'current_' + etype, eitems)
+            #log.msg('Current %s: %s' % (etype, eitems))
+
+        response = self.current_page['response']
+        # Do Templating
+        data = self.specialization_re.sub(self.specialize, response.body)
+        # Remove current stuff
+        for etype in ['session', 'favorite', 'subscription']:
+            setattr(self, 'current_' + etype, {})
+        # Overwrite headers
+        response.setHeader('connection', 'close')
+        response.setHeader('content-length', len(data))
+        response.setHeader('via', 'Twice 0.1')
+        # Delete twice/cache headers
+        response.removeHeader(self.config.get('cache_header'))
+        response.removeHeader(self.config.get('twice_header'))
+        response.removeHeader(self.config.get('cookies_header'))
+        # Write response
+        connection.transport.write(response.writeResponse(body = data))
+        connection.shutdown()
+        log.msg('RENDER [%s] (%.3fs after request received)' % (request.uri, (time.time() - request.received_on)))
+
+# ---------- TEMPLATING -----------
+
+    def specialize(self, expression):
+        "Parse an expression and return the result"
+        try:
+            expression = expression.groups()[0].strip()
+            parts = expression.split()
+            # Syntax is: command target arg1 arg2 argn
+            #   command - one of 'get', 'if', 'unless', 'incr', 'decr'
+            #   target - one of 'memcache', 'session'
+            #   arg[n] - usually the name of a key
+            command, target, args = parts[0].lower(), parts[1], parts[2:]
+            #log.msg('command: %s target: %s args: %s' % (command, target, repr(args)))
+        except:
+            log.msg('Could not parse expression: [%s]' % expression)
+            return expression
+        # Grab dictionary
+        try:
+            dictionary = getattr(self, 'current_' + target)
+        except:
+            dictionary = {}
+        #log.msg('dictionary: %s' % dictionary)
+        # Handle commands
+        if command == 'get' and len(args) >= 1:
+            if len(args) >= 2:
+                default = args[1]
+            else:
+                default = ''
+            val = dictionary.get(args[0])
+            if not val:
+                val = default
+            #log.msg('arg: %s val: %s (default %s)' % (args[0], val, default))
+            return str(val)
+        elif command == 'if' and len(args) >= 2:
+            if dictionary.get(args[0]):
+                return str(args[1])
+            elif len(args) >= 3:
+                return str(args[2])
+            else:
+                return ''
+        elif command == 'unless' and len(args) >= 2:
+            if not dictionary.get(args[0]):
+                return str(args[1])
+            elif len(args) >= 3:
+                return str(args[2])
+            else:
+                return ''
+        elif (command == 'incr' or command == 'decr') and len(args) >= 1:
+            try:
+                func = getattr(self.store, command + '_' + target)
+                set_func = getattr(self.store, 'set_' + target)
+            except:
+                log.msg('Data store is missing %s_%s or set_%s' % (command, target, target))
+                return ''
+            val = dictionary.get(args[0])
+            if val:
+                try:
+                    func(args[0])
+                    if command == 'incr':
+                        dictionary[args[0]] = int(val) + 1
+                    else:
+                        dictionary[args[0]] = int(val) - 1
+                except:
+                    pass
+            elif len(args) >= 2:
+                set_func(args[0], args[1])
+                dictionary[args[0]] = args[1]
+            return ''
+        else:
+            log.msg('Invalid command: %s' % command)
+            return expression    
+"""
+
+    File: http.py
+    Description: 
+    
+        Twisted HTTP 1.0 library.
+
+    Author: Kyle Vogt
+    Copyright (c) 2008, Justin.tv, Inc.
+    
+"""
+
+from twisted.python import log
+from twisted.protocols import basic
+from twisted.internet import protocol, defer
+import traceback, urllib, time
+
+messages = {
+    200 : 'OK',
+    400 : 'Bad Request',
+    500 : 'Internal Server Error',
+    501 : 'Not Implemented',
+    502 : 'Bad Gateway',
+    503 : 'Service Unavailable',
+    504 : 'Gateway Timeout',
+    505 : 'HTTP Version Not Supported',
+}
+
+class HTTPObject:    
+    
+    def __init__(self, id=None):
+        self.id = id
+        self.headers = {}
+        self.cookies = []
+        self.body = ''
+        self.method = 'GET'
+        self.mode = 'status'
+        self.uri = ''
+        self.protocol = 'HTTP/1.0'
+        self.status = 200
+        self.message = None
+        self.element_cache = []
+        self.response = None
+        self.cacheable = False
+        self.dependencies = []
+        self.elements = {}
+        self.received_on = None
+        
+    def setHeader(self, key, value=''):
+        self.removeHeader(key)
+        self.headers[key.lower()] = value
+        
+    def getHeader(self, key):
+        for hkey, hval in self.headers.items():
+            if key.lower() == hkey.lower():
+                return hval
+        return None
+        
+    def getCacheControlHeader(self, header='x-twice-control'):
+        "Parse headers looking like 'x-twice-control: max-age=23423'"
+        header = self.getHeader(header)
+        if header:
+            for element in header.split('; '):
+                if '=' in element:
+                    key, val = element.split('=')[0:2]
+                    if key == 'max-age':
+                        return int(val)
+        return None
+        
+    def removeHeader(self, key):
+        for k in list(self.headers.keys()):
+            if key.lower() == k.lower():
+                del self.headers[k]
+        
+    def addCookie(self, key, value, path='/'):
+        self.cookies.append((key.lower(), value, path))
+    
+    def removeCookie(self, key):
+        for cookie in list(self.cookies):
+            k, v, path = cookie
+            if key.lower() == k:
+                cookies.remove(cookie)
+                
+    def getCookie(self, key):
+        for cookie in self.cookies:
+            parts = cookie.split('; ')[0].split('=')
+            ckey, cval = parts[0], parts[1:]
+            if ckey.lower() == key.lower():
+                return '='.join(cval)
+        return None
+                
+    def writeStatus(self):
+        status_data = '%s %s %s\r\n' % (self.protocol, self.status, self.message or messages.get(self.status, 'ERROR'))
+        return status_data
+        
+    def writeCommand(self):
+        command_data = '%s %s %s\r\n' % (self.method, self.uri, self.protocol)
+        return command_data
+
+    def writeHeaders(self):
+        header_data = ''.join(['%s: %s\r\n' % (k,v) for k,v in self.headers.items()])
+        return header_data
+
+    def writeCookies(self, key='set-cookie'):
+        if not self.cookies: return ''
+        if key.lower() == 'set-cookie':
+            cookie_data = ''.join(['set-cookie: %s\r\n' % cookie for cookie in self.cookies])
+        elif key.lower() == 'cookie':
+            cookie_data = 'cookie: %s\r\n' % ('; '.join(self.cookies))
+        return cookie_data
+        
+    def writeBody(self, body = None):
+        self.setHeader('content-length', len(body or self.body))
+
+    def writeResponse(self, body = None):
+        self.writeBody(body or self.body)
+        return ''.join([self.writeStatus(), self.writeHeaders(), self.writeCookies('set-cookie'), '\r\n', body or self.body])
+
+    def writeRequest(self, body = None):
+        self.writeBody()
+        return ''.join([self.writeCommand(), self.writeHeaders(), self.writeCookies('cookie'), '\r\n', body or self.body])
+
+        
+class HTTPHandler(basic.LineReceiver):
+
+    def __init__(self):
+        self.max_headers = 100
+        self.object_count = 0
+        self.object = None 
+        self.received_on = None
+        
+    def connectionMade(self):
+        self.received_on = time.time()       
+        
+    def lineReceived(self, line):
+        if not self.object:
+            self.object = HTTPObject(self.object_count)
+            self.object.received_on = self.received_on
+            self.object_count += 1      
+        if self.object.mode == 'status':
+            try:
+                parts = line.split()
+                if parts[0].upper() in ['GET', 'PUT', 'POST', 'DELETE', 'HEAD']:
+                    self.object.method, self.object.uri, self.object.protocol = parts
+                    self.object.uri = self.object.uri
+                else:
+                    self.object.protocol = parts[0]
+                    self.object.status = int(parts[1])
+                    self.object.message = ' '.join(parts[2:])
+                self.object.mode = 'headers'
+                return
+            except:
+                log.msg("Bad line was: %s" % line)
+                traceback.print_exc()
+                try:
+                    self.sendCode(400)
+                except:
+                    pass
+                self.shutdown()
+                return          
+        elif self.object.mode == 'headers':
+            if line != '':
+                try:
+                    key, value = line.split(': ')
+                    if key.lower() == 'cookie':
+                        new_cookies = value.split('; ')
+                        self.object.cookies.extend(new_cookies)
+                    elif key.lower() == 'set-cookie':
+                        new_cookie = value
+                        self.object.cookies.append(new_cookie)
+                    else:
+                        self.object.setHeader(key, value)
+                except:
+                    self.sendCode(400)
+                    self.shutdown()
+                    return
+            else:
+                length = self.object.getHeader('content-length')
+                if length and int(length) > 0:
+                    self.mode = 'body'
+                    self.setRawMode()
+                else:
+                    self.factory.objectReceived(self, self.object)
+                    self.object = None
+
+    def rawDataReceived(self, data):
+        "Process HTTP body data"
+        self.object.body += data
+        if len(self.object.body) == int(self.object.getHeader('content-length')):
+            self.factory.objectReceived(self, self.object)
+            self.object = None
+            self.setLineMode()
+            
+    def shutdown(self):
+        self.transport.loseConnection()
+        
+class HTTPServer(HTTPHandler):
+    
+    def __init__(self):
+        HTTPHandler.__init__(self)
+        
+    def sendCode(self, code, body = ''):
+        response = HTTPObject()
+        response.status = int(code)
+        response.body = body
+        self.transport.write(response.writeResponse())
+        self.shutdown()
+        
+class HTTPClient(HTTPHandler):
+    
+    def __init__(self):
+        HTTPHandler.__init__(self)
+    
+    def connectionMade(self):
+        data = self.factory.request.writeRequest()
+        self.transport.write(data)
+        
+class HTTPRequestDispatcher(protocol.ServerFactory):
+    
+    protocol = HTTPServer
+        
+    def objectReceived(self, connection, request):
+        "Override me"
+        
+class HTTPRequestSender(protocol.ClientFactory):
+    
+    protocol = HTTPClient
+    
+    def __init__(self, request):
+        self.request = request
+        self.deferred = defer.Deferred()
+        
+    def __repr__(self):
+        return '<HTTPRequestSender (%s)>' % self.request.uri
+    
+    def objectReceived(self, connection, response):
+        "Send the page!"
+        self.deferred.callback(response)
+"""
+
+    File: parser.py
+    Description: 
+    
+        Config file parser.
+
+    Author: Kyle Vogt
+    Copyright (c) 2008, Justin.tv, Inc.
+    
+"""
+
+from twisted.python import usage, log
+import sys, traceback
+
+def parse():
+    "Parse conf file into a dict"
+    try:
+        options = Options()
+        options.parseOptions()
+    except usage.UsageError, errortext:
+        print '%s: %s' % (sys.argv[0], errortext)
+        print '%s: Try --help for usage details.' % (sys.argv[0])
+        sys.stdout.flush()
+        sys.exit(1)
+    try:
+        # Read config file
+        settings = {}
+        data = file(options['config']).readlines()
+        for line in data:
+            args = line.split()
+            if len(args) >= 2 and not line.startswith('#'):
+                if args[0] not in settings:
+                    val = args[1].strip()
+                else:
+                    val = settings[args[0]] + ',' + args[1].strip()
+                if val.lower() in ['yes', 'true', 'on']: val = True
+                elif val.lower() in ['no', 'false', 'off']: val = False
+                settings[args[0]] = val
+        for option, val in options.items():
+            if val:
+                settings[option] = val
+        log.msg(settings)
+        return settings
+    except:
+        print 'Unable to parse config file %s:' % options['config']
+        traceback.print_exc()
+        sys.stdout.flush()
+        sys.exit(1)
+        
+class Options(usage.Options):
+    optFlags = [
+        ['verbose', 'v', 'Verbose mode'],
+        ['daemon', 'd', 'Daemonize'],
+    ]
+    optParameters = [
+        ['config', 'c', 'twice.conf', 'Config file'],
+        ['log', 'l', '', 'Log file'],
+        ['port', 'p', '', 'Port to listen on'],
+        ['interface', 'i', '', 'Interface to bind to']
+    ]
+# General:
+#
+#   Specify server port, memory limit, and template tag format.  In order
+# to protect a server from memory leaks or overload conditions, Twice will 
+# terminate if it uses more than memory_limit MB's of RAM.
+
+port                3333                
+memory_limit        100
+template_regex      <&(.*?)&>
+
+# Headers:
+#
+#   Twice uses HTTP headers to communicate with application servers.  The 
+# names of each of these headers can be specified below.  
+#
+# purge_header      - delete cache keys
+# twice_header      - tell app server to render for Twice
+# cache_header      - tells twice how to cache a response
+# cookies_header    - tells twice which cookies affect caching
+
+purge_header        x-mark-dirty
+twice_header        x-twice
+cache_header        x-twice-control
+cookies_header      x-twice-cookies
+
+# Backend Servers:
+# 
+#   Twice can talk to almost any type of backend server or storage device (with
+# the proper plugin of course).  This is the place to specify addresses, ports, 
+# and login credentials for these resources.
+
+backend_appserver   127.0.0.1:8080
+backend_memcache    127.0.0.1:11211
+backend_memcachedb  127.0.0.1:21201
+backend_db_host     127.0.0.1:5432
+backend_db_name     db_name
+backend_db_user     db_user
+backend_db_pass     db_pass
+backend_db_pool_min 1
+backend_db_pool_max 5
+
+# Cache Type:
+#
+#   For smaller sites, use the internal cache for the lowest possible latency.
+# If you are running serveral Twice processes or have a very large number of 
+# pages to cache, use the memcache cache.  If the cache type is internal, 
+# cache_server and cache_pool are meaningless.
+
+#cache_type          internal
+cache_type          memcache
+cache_server        127.0.0.1
+cache_pool          10
+
+# Internationalization:
+#
+#   If your appliation renders different versions of the same url based on the 
+# preferred language of the browser, you will want to enable hash_langauge.  
+# This appends the browser's preferred language string to the uri hash.
+
+hash_lang_header    yes
+hash_lang_default   en-us
+
+# Misc:
+
+#   If you need to do something special with virtual hosts, you can use 
+# rewrite_host to modify the Host header of requests that Twice proxies
+# to the application servers.
+
+#rewrite_host        www.mydomain.com
+
+
+
+import sys, os, signal, traceback, resource
+
+__author__    = "Kyle Vogt <kyleavogt@gmail.com> and Emmett Shear <emmett.shear@gmail.com>"
+__version__   = "0.2"
+__copyright__ = "Copyright (c) 2008, Justin.tv, Inc."
+__license__   = "MIT"        
+
+def check_memory(limit):
+    try:
+        cpu, mem = [i.replace('\n', '') for i in os.popen('ps -p %s -o pcpu,rss' % os.getpid()).readlines()[1].split(' ') if i]
+        real = int(mem) / 1000.0
+        if real > limit:
+            log.msg('Using too much memory (%.2fMB out of %.2fMB)' % (real, limit))
+            os.kill(os.getpid(), signal.SIGTERM)
+    except:
+        log.msg('Unable to read memory or cpu usage!')
+        traceback.print_exc()
+    reactor.callLater(15.0, check_memory, limit)
+    
+if __name__ == '__main__':
+
+    # Read config
+    import parser
+    config = parser.parse()
+    
+    # Log
+    from twisted.python import log
+    f = config['log']
+    if f != 'stdout':
+        log.startLogging(open(f, 'w'))
+    else:
+        log.startLogging(sys.stdout)
+
+    # Set up reactor
+    try:
+        from twisted.internet import epollreactor
+        epollreactor.install()
+        log.msg('Using epoll')
+    except:
+        pass
+    from twisted.internet import reactor  
+    
+    # Step up to maximum file descriptor limit
+    try:
+        soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
+        for i in xrange(16):
+            test = 2 ** i
+            if test > hard: break
+            try:
+                resource.setrlimit(resource.RLIMIT_NOFILE, (test, hard))        
+                val = test
+            except ValueError:
+                soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
+                val = soft
+                break
+        log.msg('%s file descriptors available (system max is %s)' % (val, hard))
+    except:
+        log.msg('Error setting fd limit!')
+        traceback.print_exc()
+        
+    # Check memory usage
+    check_memory(int(config.get('memory_limit', 100)))
+        
+    # Start request handler event loop
+    import handler
+    factory = handler.RequestHandler(config)
+    reactor.listenTCP(int(config['port']), factory)
+    
+    #from twisted.manhole import telnet
+    #shell = telnet.ShellFactory()
+    #shell.username = 'admin'
+    #shell.password = 'changeme'
+    #try:
+    #    reactor.listenTCP(4040, shell)
+    #    log.msg('Telnet server running on port 4040.')
+    #except:
+    #    log.msg('Telnet server not running.')
+        
+    reactor.run()