source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/contrib/redis_session.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 9.8 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3"""
4Developed by niphlod@gmail.com
5License MIT/BSD/GPL
6
7Redis-backed sessions
8"""
9
10import logging
11from threading import Lock
12from gluon import current
13from gluon.storage import Storage
14from gluon.contrib.redis_utils import acquire_lock, release_lock
15from gluon.contrib.redis_utils import register_release_lock
16from gluon._compat import to_native
17from datetime import datetime
18
19logger = logging.getLogger("web2py.session.redis")
20
21locker = Lock()
22
23
24def RedisSession(redis_conn, session_expiry=False, with_lock=False, db=None):
25    """
26    Usage example: put in models::
27
28        from gluon.contrib.redis_utils import RConn
29        rconn = RConn()
30        from gluon.contrib.redis_session import RedisSession
31        sessiondb = RedisSession(redis_conn=rconn, with_lock=True, session_expiry=False)
32        session.connect(request, response, db = sessiondb)
33
34    Args:
35        redis_conn: a redis-like connection object
36        with_lock: prevent concurrent modifications to the same session
37        session_expiry: delete automatically sessions after n seconds
38                        (still need to run sessions2trash.py every 1M sessions
39                        or so)
40
41    Simple slip-in storage for session
42    """
43
44    locker.acquire()
45    try:
46        instance_name = 'redis_instance_' + current.request.application
47        if not hasattr(RedisSession, instance_name):
48            setattr(RedisSession, instance_name,
49                    RedisClient(redis_conn, session_expiry=session_expiry, with_lock=with_lock))
50        return getattr(RedisSession, instance_name)
51    finally:
52        locker.release()
53
54
55class RedisClient(object):
56
57    def __init__(self, redis_conn, session_expiry=False, with_lock=False):
58        self.r_server = redis_conn
59        self._release_script = register_release_lock(self.r_server)
60        self.tablename = None
61        self.session_expiry = session_expiry
62        self.with_lock = with_lock
63
64    def get(self, what, default):
65        return self.tablename
66
67    def Field(self, fieldname, type='string', length=None, default=None,
68              required=False, requires=None):
69        return fieldname, type
70
71    def define_table(self, tablename, *fields, **args):
72        if not self.tablename:
73            self.tablename = MockTable(
74                self, self.r_server, tablename, self.session_expiry,
75                with_lock=self.with_lock, fields=fields)
76        return self.tablename
77
78    def __getitem__(self, key):
79        return self.tablename
80
81    def __call__(self, where=''):
82        q = self.tablename.query
83        return q
84
85    def commit(self):
86        # this is only called by session2trash.py
87        pass
88
89    def convert_dict_string(self, dict_string):
90        fields = self.tablename.fields
91        typed_dict = dict()
92        converters = {
93            'boolean': lambda x: 1 if x.decode() == '1' else 0,
94            'blob': lambda x: x,
95        }
96        for field, ftype in fields:
97            if field not in dict_string:
98                continue
99            if ftype in converters:
100                typed_dict[field] = converters[ftype](dict_string[field])
101            else:
102                typed_dict[field] = dict_string[field].decode()
103        return typed_dict
104
105
106class MockTable(object):
107
108    def __init__(self, db, r_server, tablename, session_expiry, with_lock=False, fields=None):
109        # here self.db is the RedisClient instance
110        self.db = db
111        self.tablename = tablename
112        # set the namespace for sessions of this app
113        self.keyprefix = 'w2p:sess:%s' % tablename.replace('web2py_session_', '')
114        # fast auto-increment id (needed for session handling)
115        self.serial = "%s:serial" % self.keyprefix
116        # index of all the session keys of this app
117        self.id_idx = "%s:id_idx" % self.keyprefix
118        # remember the session_expiry setting
119        self.session_expiry = session_expiry
120        self.with_lock = with_lock
121        self.fields = fields if fields is not None else []
122
123    def __call__(self, record_id, unique_key=None):
124        # Support DAL shortcut query: table(record_id)
125
126        # This will call the __getattr__ below
127        # returning a MockQuery
128        q = self.id
129
130        # Instructs MockQuery, to behave as db(table.id == record_id)
131        q.op = 'eq'
132        q.value = record_id
133        q.unique_key = unique_key
134
135        row = q.select()
136        return row[0] if row else Storage()
137
138    def __getattr__(self, key):
139        if key == 'id':
140            # return a fake query. We need to query it just by id for normal operations
141            self.query = MockQuery(
142                field='id', db=self.db,
143                prefix=self.keyprefix, session_expiry=self.session_expiry,
144                with_lock=self.with_lock, unique_key=self.unique_key
145            )
146            return self.query
147        elif key == '_db':
148            # needed because of the calls in sessions2trash.py and globals.py
149            return self.db
150
151    def insert(self, **kwargs):
152        # usually kwargs would be a Storage with several keys:
153        # 'locked', 'client_ip','created_datetime','modified_datetime'
154        # 'unique_key', 'session_data'
155        # retrieve a new key
156        newid = str(self.db.r_server.incr(self.serial))
157        key = self.keyprefix + ':' + newid
158        if self.with_lock:
159            key_lock = key + ':lock'
160            acquire_lock(self.db.r_server, key_lock, newid)
161        with self.db.r_server.pipeline() as pipe:
162            # add it to the index
163            pipe.sadd(self.id_idx, key)
164            # set a hash key with the Storage
165            pipe.hmset(key, kwargs)
166            if self.session_expiry:
167                pipe.expire(key, self.session_expiry)
168            pipe.execute()
169        if self.with_lock:
170            release_lock(self.db, key_lock, newid)
171        return newid
172
173
174class MockQuery(object):
175    """a fake Query object that supports querying by id
176       and listing all keys. No other operation is supported
177    """
178    def __init__(self, field=None, db=None, prefix=None, session_expiry=False,
179                 with_lock=False, unique_key=None):
180        self.field = field
181        self.value = None
182        self.db = db
183        self.keyprefix = prefix
184        self.op = None
185        self.session_expiry = session_expiry
186        self.with_lock = with_lock
187        self.unique_key = unique_key
188
189    def __eq__(self, value, op='eq'):
190        self.value = value
191        self.op = op
192
193    def __ge__(self, value, op='ge'):
194        self.value = value
195        self.op = op
196
197    def __gt__(self, value, op='gt'):
198        self.value = value
199        self.op = op
200
201    def select(self):
202        if self.op == 'eq' and self.field == 'id' and self.value:
203            # means that someone wants to retrieve the key self.value
204            key = self.keyprefix + ':' + str(self.value)
205            if self.with_lock:
206                acquire_lock(self.db.r_server, key + ':lock', self.value, 2)
207            rtn = {to_native(k): v for k, v in self.db.r_server.hgetall(key).items()}
208            if rtn:
209                if self.unique_key:
210                    # make sure the id and unique_key are correct
211                    if rtn['unique_key'] == to_native(self.unique_key):
212                        rtn['update_record'] = self.update  # update record support
213                    else:
214                        rtn = None
215            return [Storage(self.db.convert_dict_string(rtn))] if rtn else []
216        elif self.op in ('ge', 'gt') and self.field == 'id' and self.value == 0:
217            # means that someone wants the complete list
218            rtn = []
219            id_idx = "%s:id_idx" % self.keyprefix
220            # find all session keys of this app
221            allkeys = self.db.r_server.smembers(id_idx)
222            for sess in allkeys:
223                val = self.db.r_server.hgetall(sess)
224                if not val:
225                    if self.session_expiry:
226                        # clean up the idx, because the key expired
227                        self.db.r_server.srem(id_idx, sess)
228                    continue
229                val = Storage(self.db.convert_dict_string(val))
230                # add a delete_record method (necessary for sessions2trash.py)
231                val.delete_record = RecordDeleter(
232                    self.db, sess, self.keyprefix)
233                rtn.append(val)
234            return rtn
235        else:
236            raise Exception("Operation not supported")
237
238    def update(self, **kwargs):
239        # means that the session has been found and needs an update
240        if self.op == 'eq' and self.field == 'id' and self.value:
241            key = self.keyprefix + ':' + str(self.value)
242            if not self.db.r_server.exists(key):
243                return None
244            with self.db.r_server.pipeline() as pipe:
245                pipe.hmset(key, kwargs)
246                if self.session_expiry:
247                    pipe.expire(key, self.session_expiry)
248                rtn = pipe.execute()[0]
249            if self.with_lock:
250                release_lock(self.db, key + ':lock', self.value)
251            return rtn
252
253    def delete(self, **kwargs):
254        # means that we want this session to be deleted
255        if self.op == 'eq' and self.field == 'id' and self.value:
256            id_idx = "%s:id_idx" % self.keyprefix
257            key = self.keyprefix + ':' + str(self.value)
258            with self.db.r_server.pipeline() as pipe:
259                pipe.delete(key)
260                pipe.srem(id_idx, key)
261                rtn = pipe.execute()
262            return rtn[1]
263
264
265class RecordDeleter(object):
266    """Dumb record deleter to support sessions2trash.py"""
267
268    def __init__(self, db, key, keyprefix):
269        self.db, self.key, self.keyprefix = db, key, keyprefix
270
271    def __call__(self):
272        id_idx = "%s:id_idx" % self.keyprefix
273        # remove from the index
274        self.db.r_server.srem(id_idx, self.key)
275        # remove the key itself
276        self.db.r_server.delete(self.key)
Note: See TracBrowser for help on using the repository browser.