""" Developed by 616d41631bff906704951934ffe4015e Released under web2py license because includes gluon/cache.py source code """ import redis from redis.exceptions import ConnectionError from gluon import current from gluon.cache import CacheAbstract import cPickle as pickle import time import re import logging import thread logger = logging.getLogger("web2py.cache.redis") locker = thread.allocate_lock() def RedisCache(*args, **vars): """ Usage example: put in models from gluon.contrib.redis_cache import RedisCache cache.redis = RedisCache('localhost:6379',db=None, debug=True) cache.redis.stats() return a dictionary with statistics of Redis server with one additional key ('w2p_keys') showing all keys currently set from web2py with their TTL if debug=True additional tracking is activate and another key is added ('w2p_stats') showing total_hits and misses """ locker.acquire() try: if not hasattr(RedisCache, 'redis_instance'): RedisCache.redis_instance = RedisClient(*args, **vars) finally: locker.release() return RedisCache.redis_instance class RedisClient(object): meta_storage = {} MAX_RETRIES = 5 RETRIES = 0 def __init__(self, server='localhost:6379', db=None, debug=False): self.server = server self.db = db or 0 host,port = (self.server.split(':')+['6379'])[:2] port = int(port) self.request = current.request self.debug = debug if self.request: app = self.request.application else: app = '' if not app in self.meta_storage: self.storage = self.meta_storage[app] = { CacheAbstract.cache_stats_name: { 'hit_total': 0, 'misses': 0, }} else: self.storage = self.meta_storage[app] self.r_server = redis.Redis(host=host, port=port, db=self.db) def __call__(self, key, f, time_expire=300): try: if time_expire == None: time_expire = 24*60*60 newKey = self.__keyFormat__(key) value = None obj = self.r_server.get(newKey) ttl = self.r_server.ttl(newKey) or 0 if ttl > time_expire: obj = None if obj: if self.debug: self.r_server.incr('web2py_cache_statistics:hit_total') value = pickle.loads(obj) elif f is None: self.r_server.delete(newKey) else: if self.debug: self.r_server.incr('web2py_cache_statistics:misses') value = f() if time_expire == 0: time_expire = 1 self.r_server.setex(newKey, pickle.dumps(value), time_expire) return value except ConnectionError: return self.retry_call(key, f, time_expire) def retry_call(self, key, f, time_expire): self.RETRIES += 1 if self.RETRIES <= self.MAX_RETRIES: logger.error("sleeping %s seconds before reconnecting" % (2 * self.RETRIES)) time.sleep(2 * self.RETRIES) self.__init__(self.server, self.db, self.debug) return self.__call__(key, f, time_expire) else: self.RETRIES = 0 raise ConnectionError , 'Redis instance is unavailable at %s' % (self.server) def increment(self, key, value=1, time_expire=300): try: newKey = self.__keyFormat__(key) obj = self.r_server.get(newKey) if obj: return self.r_server.incr(newKey, value) else: self.r_server.setex(newKey, value, time_expire) return value except ConnectionError: return self.retry_increment(key, value, time_expire) def retry_increment(self, key, value, time_expire): self.RETRIES += 1 if self.RETRIES <= self.MAX_RETRIES: logger.error("sleeping some seconds before reconnecting") time.sleep(2 * self.RETRIES) self.__init__(self.server, self.db, self.debug) return self.increment(key, value, time_expire) else: self.RETRIES = 0 raise ConnectionError , 'Redis instance is unavailable at %s' % (self.server) def clear(self, regex): """ Auxiliary function called by `clear` to search and clear cache entries """ r = re.compile(regex) prefix = "w2p:%s:" % (self.request.application) pipe = self.r_server.pipeline() for a in self.r_server.keys("%s*" % \ (prefix)): if r.match(str(a).replace(prefix, '', 1)): pipe.delete(a) pipe.execute() def stats(self): statscollector = self.r_server.info() if self.debug: statscollector['w2p_stats'] = dict( hit_total = self.r_server.get( 'web2py_cache_statistics:hit_total'), misses=self.r_server.get('web2py_cache_statistics:misses') ) statscollector['w2p_keys'] = dict() for a in self.r_server.keys("w2p:%s:*" % ( self.request.application)): statscollector['w2p_keys']["%s_expire_in_sec" % (a)] = \ self.r_server.ttl(a) return statscollector def __keyFormat__(self, key): return 'w2p:%s:%s' % (self.request.application, key.replace(' ', '_'))