261 lines
8.5 KiB
Python
261 lines
8.5 KiB
Python
"""
|
|
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
|
|
from gluon.contrib.redis_session import RedisSession
|
|
sessiondb = RedisSession('localhost:6379',db=0, session_expiry=False)
|
|
session.connect(request, response, db = sessiondb)
|
|
|
|
Simple slip-in storage for session
|
|
"""
|
|
|
|
locker.acquire()
|
|
try:
|
|
instance_name = 'redis_instance_' + current.request.application
|
|
if not hasattr(RedisSession, instance_name):
|
|
setattr(RedisSession, instance_name, RedisClient(*args, **vars))
|
|
return getattr(RedisSession, instance_name)
|
|
finally:
|
|
locker.release()
|
|
|
|
|
|
class RedisClient(object):
|
|
|
|
meta_storage = {}
|
|
MAX_RETRIES = 5
|
|
RETRIES = 0
|
|
_release_script = None
|
|
|
|
def __init__(self, server='localhost:6379', db=None, debug=False,
|
|
session_expiry=False, with_lock=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)
|
|
if with_lock:
|
|
RedisClient._release_script = \
|
|
self.r_server.register_script(_LUA_RELEASE_LOCK)
|
|
self.tablename = None
|
|
self.session_expiry = session_expiry
|
|
self.with_lock = with_lock
|
|
|
|
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,
|
|
self.with_lock)
|
|
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, with_lock=False):
|
|
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
|
|
self.with_lock = with_lock
|
|
|
|
def __call__(self, record_id):
|
|
# Support DAL shortcut query: table(record_id)
|
|
|
|
q = self.id # This will call the __getattr__ below
|
|
# returning a MockQuery
|
|
|
|
# Instructs MockQuery, to behave as db(table.id == record_id)
|
|
q.op = 'eq'
|
|
q.value = record_id
|
|
|
|
row = q.select()
|
|
return row[0] if row else Storage()
|
|
|
|
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,
|
|
with_lock=self.with_lock)
|
|
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 = str(self.r_server.incr(self.serial))
|
|
key = self.keyprefix + ':' + newid
|
|
if self.with_lock:
|
|
key_lock = key + ':lock'
|
|
acquire_lock(self.r_server, key_lock, newid)
|
|
with self.r_server.pipeline() as pipe:
|
|
#add it to the index
|
|
pipe.sadd(self.id_idx, key)
|
|
#set a hash key with the Storage
|
|
pipe.hmset(key, kwargs)
|
|
if self.session_expiry:
|
|
pipe.expire(key, self.session_expiry)
|
|
pipe.execute()
|
|
if self.with_lock:
|
|
release_lock(self.r_server, key_lock, newid)
|
|
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,
|
|
with_lock=False):
|
|
self.field = field
|
|
self.value = None
|
|
self.db = db
|
|
self.keyprefix = prefix
|
|
self.op = None
|
|
self.session_expiry = session_expiry
|
|
self.with_lock = with_lock
|
|
|
|
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
|
|
key = self.keyprefix + ':' + str(self.value)
|
|
if self.with_lock:
|
|
acquire_lock(self.db, key + ':lock', self.value)
|
|
rtn = self.db.hgetall(key)
|
|
if rtn:
|
|
rtn['update_record'] = self.update # update record support
|
|
return [Storage(rtn)] if rtn else []
|
|
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 not val:
|
|
if self.session_expiry:
|
|
#clean up the idx, because the key expired
|
|
self.db.srem(id_idx, sess)
|
|
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:
|
|
key = self.keyprefix + ':' + str(self.value)
|
|
with self.db.pipeline() as pipe:
|
|
pipe.hmset(key, kwargs)
|
|
if self.session_expiry:
|
|
pipe.expire(key, self.session_expiry)
|
|
rtn = pipe.execute()[0]
|
|
if self.with_lock:
|
|
release_lock(self.db, key + ':lock', self.value)
|
|
return rtn
|
|
|
|
|
|
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)
|
|
|
|
|
|
def acquire_lock(conn, lockname, identifier, ltime=10):
|
|
while True:
|
|
if conn.set(lockname, identifier, ex=ltime, nx=True):
|
|
return identifier
|
|
time.sleep(.01)
|
|
|
|
|
|
_LUA_RELEASE_LOCK = """
|
|
if redis.call("get", KEYS[1]) == ARGV[1]
|
|
then
|
|
return redis.call("del", KEYS[1])
|
|
else
|
|
return 0
|
|
end
|
|
"""
|
|
|
|
|
|
def release_lock(conn, lockname, identifier):
|
|
return RedisClient._release_script(keys=[lockname], args=[identifier],
|
|
client=conn)
|