diff --git a/VERSION b/VERSION index 40d833ed..9830195b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.1.0 (2012-10-08 22:09:18) dev +Version 2.1.0 (2012-10-09 14:36:06) dev diff --git a/gluon/contrib/redis_session.py b/gluon/contrib/redis_session.py new file mode 100644 index 00000000..f675313e --- /dev/null +++ b/gluon/contrib/redis_session.py @@ -0,0 +1,198 @@ +""" +Developed by niphlod@gmail.com +""" + +import redis +from redis.exceptions import ConnectionError +from gluon import current +from gluon.storage import Storage +import cPickle as pickle +import time +import re +import logging +import thread + +logger = logging.getLogger("web2py.session.redis") + +locker = thread.allocate_lock() + +def RedisSession(*args, **vars): + """ + Usage example: put in models + + sessiondb = RedisSession('localhost:6379',db=0, debug=True) + session.connect(request, response, db = sessiondb) + + Simple slip-in storage for session + """ + + locker.acquire() + try: + if not hasattr(RedisSession, 'redis_instance'): + RedisSession.redis_instance = RedisClient(*args, **vars) + finally: + locker.release() + return RedisSession.redis_instance + + +class RedisClient(object): + + meta_storage = {} + MAX_RETRIES = 5 + RETRIES = 0 + + def __init__(self, server='localhost:6379', db=None, debug=False, session_expiry=False): + """session_expiry can be an integer, in seconds, to set the default expiration + of sessions. The corresponding record will be deleted from the redis instance, + and there's virtually no need to run sessions2trash.py + """ + self.server = server + self.db = db or 0 + host,port = (self.server.split(':')+['6379'])[:2] + port = int(port) + self.debug = debug + if current and current.request: + self.app = current.request.application + else: + self.app = '' + self.r_server = redis.Redis(host=host, port=port, db=self.db) + self.tablename = None + self.session_expiry = session_expiry + + def get(self, what, default): + return self.tablename + + def Field(self, fieldname, type='string', length=None, default=None, + required=False,requires=None): + return None + + def define_table(self,tablename,*fields,**args): + if not self.tablename: + self.tablename = MockTable(self, self.r_server, tablename, self.session_expiry) + return self.tablename + + def __getitem__(self, key): + return self.tablename + + def __call__(self, where=''): + q = self.tablename.query + return q + + def commit(self): + #this is only called by session2trash.py + pass + +class MockTable(object): + + def __init__(self, db, r_server, tablename, session_expiry): + self.db = db + self.r_server = r_server + self.tablename = tablename + #set the namespace for sessions of this app + self.keyprefix = 'w2p:sess:%s' % tablename.replace('web2py_session_', '') + #fast auto-increment id (needed for session handling) + self.serial = "%s:serial" % self.keyprefix + #index of all the session keys of this app + self.id_idx = "%s:id_idx" % self.keyprefix + #remember the session_expiry setting + self.session_expiry = session_expiry + + def getserial(self): + #return an auto-increment id + return "%s" % self.r_server.incr(self.serial, 1) + + def __getattr__(self, key): + if key == 'id': + #return a fake query. We need to query it just by id for normal operations + self.query = MockQuery(field='id', db=self.r_server, prefix=self.keyprefix, session_expiry=self.session_expiry) + return self.query + elif key == '_db': + #needed because of the calls in sessions2trash.py and globals.py + return self.db + + def insert(self, **kwargs): + #usually kwargs would be a Storage with several keys: + #'locked', 'client_ip','created_datetime','modified_datetime' + #'unique_key', 'session_data' + #retrieve a new key + newid = self.getserial() + key = "%s:%s" % (self.keyprefix, newid) + #add it to the index + self.r_server.sadd(self.id_idx, key) + #set a hash key with the Storage + self.r_server.hmset(key, kwargs) + if self.session_expiry: + self.r_server.expire(key, self.session_expiry) + return newid + +class MockQuery(object): + """a fake Query object that supports querying by id + and listing all keys. No other operation is supported + """ + def __init__(self, field=None, db=None, prefix=None, session_expiry=False): + self.field = field + self.value = None + self.db = db + self.keyprefix = prefix + self.op = None + self.session_expiry = session_expiry + + def __eq__(self, value, op='eq'): + self.value = value + self.op = op + + def __gt__(self, value, op='ge'): + self.value = value + self.op = op + + def select(self): + if self.op == 'eq' and self.field == 'id' and self.value: + #means that someone wants to retrieve the key self.value + rtn = self.db.hgetall("%s:%s" % (self.keyprefix, self.value)) + if rtn == dict(): + #return an empty resultset for non existing key + return [] + else: + return [Storage(rtn)] + elif self.op == 'ge' and self.field == 'id' and self.value == 0: + #means that someone wants the complete list + rtn = [] + id_idx = "%s:id_idx" % self.keyprefix + #find all session keys of this app + allkeys = self.db.smembers(id_idx) + for sess in allkeys: + val = self.db.hgetall(sess) + if val == dict(): + if self.session_expiry: + #clean up the idx, because the key expired + self.db.srem(id_idx, sess) + continue + else: + continue + val = Storage(val) + #add a delete_record method (necessary for sessions2trash.py) + val.delete_record = RecordDeleter(self.db, sess, self.keyprefix) + rtn.append(val) + return rtn + else: + raise Exception("Operation not supported") + + def update(self, **kwargs): + #means that the session has been found and needs an update + if self.op == 'eq' and self.field == 'id' and self.value: + return self.db.hmset("%s:%s" % (self.keyprefix, self.value), kwargs) + +class RecordDeleter(object): + """Dumb record deleter to support sessions2trash.py""" + + def __init__(self, db, key, keyprefix): + self.db, self.key, self.keyprefix = db, key, keyprefix + + def __call__(self): + id_idx = "%s:id_idx" % self.keyprefix + #remove from the index + self.db.srem(id_idx, self.key) + #remove the key itself + self.db.delete(self.key) + + \ No newline at end of file diff --git a/scripts/sessions2trash.py b/scripts/sessions2trash.py index f9155221..5357659f 100755 --- a/scripts/sessions2trash.py +++ b/scripts/sessions2trash.py @@ -94,11 +94,11 @@ class SessionSetDb(SessionSet): """Return list of SessionDb instances for existing sessions.""" sessions = [] tablename = 'web2py_session' - if request.application: - tablename = 'web2py_session_' + request.application - if tablename in db: - for row in db(db[tablename].id > 0).select(): - sessions.append(SessionDb(row)) + from gluon import current + (record_id_name, table, record_id, unique_key) = \ + current.response._dbtable_and_field + for row in table._db(table.id > 0).select(): + sessions.append(SessionDb(row)) return sessions @@ -121,8 +121,11 @@ class SessionDb(object): self.row = row def delete(self): + from gluon import current + (record_id_name, table, record_id, unique_key) = \ + current.response._dbtable_and_field self.row.delete_record() - db.commit() + table._db.commit() def get(self): session = Storage() @@ -130,7 +133,13 @@ class SessionDb(object): return session def last_visit_default(self): - return self.row.modified_datetime + if isinstance(self.row.modified_datetime, datetime.datetime): + return self.row.modified_datetime + else: + try: + return datetime.datetime.strptime(self.row.modified_datetime, '%Y-%m-%d %H:%M:%S.%f') + except: + print 'failed to retrieve last modified time (value: %s)' % self.row.modified_datetime def __str__(self): return self.row.unique_key