· 7 years ago · Jan 14, 2018, 10:18 AM
1import hashlib
2import redis
3from django.db import DEFAULT_DB_ALIAS
4from django.db.models import Q, signals as db_signals
5from piston.models import Consumer, Token
6from piston.store import DataStore as PistonDataStore
7from mirrio.api._redis import redis
8
9
10redis = redis.Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB)
11
12md5 = lambda v: hashlib.md5(v).hexdigest()
13
14def redis_model_cache(model_class):
15 r"""
16 Returns a callable which will cache the model passed in. See the docstring for the returned callable.
17
18 :arg model_class: a django.model.Model class
19 :rtype: a callable which can be used to access and cache rows from the model class
20 """
21 def _cacher(q, expiry=60*60*24, using=None):
22 r"""
23 This is a cache of %(model)s. You can retrieve single objects using a django.models.Q object.
24 The method will cache which exact row is identified by this query and cache a it's primary key,
25 in addition to the row itself.
26
27 :arg q: A django `Q` object (to use on `model_class.objects.get()`)
28 :arg model_class: A django model
29 :arg expiry: When this key should expire from the cache
30 :rtype: An instance of model_class
31 """ % dict(model=model_class)
32
33 # build an instance of model_class from dict:d
34 def _builder(d):
35 inst = model_class(**d)
36 inst._state.adding = False # so django doesn't try to overwrite
37 inst._state.db = using or DEFAULT_DB_ALIAS
38 return inst
39
40 # save an instance of model_class
41 def _cache_model(key, obj):
42 d = {}
43 for field in obj._meta.fields:
44 if field.get_internal_type() == 'FileField':
45 continue
46 d[field.attname] = getattr(obj, field.attname)
47 # save the object
48 redis.hmset(key, d)
49 redis.expire(key, expiry)
50 return d
51
52 # we save a hash of the query and save it to the pk it actually represents
53 # this way we can make cache arbitrary queries that lookup the same object
54 pk_key = 'q' + md5(model_class.__name__ + str(q))
55 # see if this query has been performed before
56 pk = redis.get(pk_key)
57 if pk is not None:
58 # HEY WE FOUND A PK FOR THIS QUERY LETS TRY TO GET IT FROM REDIS
59 key = 'pk' + model_class.__name__ + pk
60 try:
61 #print 'cache hit key', key
62 return _builder(redis.hgetall(key))
63 except:
64 redis.delete(key)
65 redis.delete(pk_key)
66 else:
67 # one caveat to doing it this way is that if we have a cache miss on retrieving
68 # the query=>pk, we have to fetch the pk of the matching object, and might as well
69 # get the entire thing
70 obj = model_class._default_manager.using(using).get(q)
71 # save the query => pk cache
72 redis.set(pk_key, str(obj.pk))
73 redis.expire(pk_key, expiry)
74
75 # now we do normal row caching
76 key = 'pk' + model_class.__name__ + str(obj.pk)
77 #print 'cache miss key', key
78 if not redis.exists(key): # but don't re-cache if it's not necessary
79 #print 'caching key', key
80 _cache_model(key, obj)
81
82 return obj
83
84 if not hasattr(model_class, 'redis_cache_cached'): # only connect these things once
85 def _clear(sender, instance, *args, **kwargs):
86 key = 'pk' + model_class.__name__ + str(instance.pk)
87 #print 'expiring key', key
88 redis.delete(key)
89
90 db_signals.post_save.connect(_clear, sender=model_class, weak=False)
91 db_signals.post_delete.connect(_clear, sender=model_class, weak=False)
92
93 setattr(model_class, 'redis_cache_cached', True)
94
95 return _cacher
96
97consumer_cache = redis_model_cache(Consumer)
98token_cache = redis_model_cache(Token)
99
100
101class RedisDataStore(PistonDataStore):
102 def lookup_consumer(self, key):
103 self.consumer = consumer_cache(Q(key=key))
104 self.consumer.key = self.consumer.key.encode('ascii')
105 self.consumer.secret = self.consumer.secret.encode('ascii')
106 return self.consumer
107
108 def lookup_token(self, token_type, token):
109 if token_type == 'request':
110 token_type = Token.REQUEST
111 elif token_type == 'access':
112 token_type = Token.ACCESS
113 try:
114 self.request_token = token_cache(Q(token_type=token_type, key=token))
115 return self.request_token
116 except Token.DoesNotExist:
117 return None
118
119 def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
120 key = 'nonce' + md5(oauth_consumer.key + oauth_consumer.secret
121 + getattr(oauth_token, 'key', '')
122 + getattr(oauth_token, 'secret', '') + nonce)
123 r = redis.get(key)
124 if not r:
125 redis.set(key, 'v')
126 redis.expire(key, 60*60*24)
127 return None
128 return r