소스 검색

Added a memcache based database cache.

Michael Hope 3 년 전
부모
커밋
81ce9b2203
3개의 변경된 파일69개의 추가작업 그리고 19개의 파일을 삭제
  1. 5 1
      Makefile
  2. 63 18
      niche.py
  3. 1 0
      strings.py

+ 5 - 1
Makefile 파일 보기

@@ -1,6 +1,10 @@
1
-version.py: Makefile
1
+all: version.py
2
+
3
+version.py: dummy
2 4
 	echo __version__ = \'$(shell git describe --always --dirty --long)\' > $@
3 5
 
6
+dummy:
7
+
4 8
 DB=$(shell cat niche.ini | grep ^db= | cut -d= -f2)
5 9
 
6 10
 import: $(wildcard mofi-*.sql.gz)

+ 63 - 18
niche.py 파일 보기

@@ -13,7 +13,11 @@ import gc
13 13
 import collections
14 14
 import json
15 15
 import random
16
+import sys
17
+import pickle
18
+import urllib
16 19
 
20
+import memcache
17 21
 import web
18 22
 import bleach
19 23
 from passlib.apps import custom_app_context as pwd_context
@@ -22,6 +26,8 @@ import strings
22 26
 import utils
23 27
 import version
24 28
 
29
+web.config.debug = False
30
+
25 31
 # pylint: disable=redefined-builtin
26 32
 # pylint: disable=redefined-outer-name
27 33
 # pylint: disable=no-init
@@ -75,7 +81,7 @@ DEFAULTS = [
75 81
             'dateformat': '%B %d, %Y',
76 82
             'base': '/',
77 83
             'extra_tags': '',
78
-            'limit': 50,
84
+            'limit': 20,
79 85
             'server_type': 'dev',
80 86
             'user_fields': 'realname email homepage gravatar_email team location twitter facebook google_plus_ skype aim',
81 87
             'history_days': 7,
@@ -99,6 +105,7 @@ DEFAULTS = [
99 105
 
100 106
 counters = utils.Counters()
101 107
 
108
+
102 109
 class Config(ConfigParser.RawConfigParser):
103 110
     def set_defaults(self, defaults):
104 111
         for section, items in defaults:
@@ -153,6 +160,43 @@ db = web.database(dbn='mysql',
153 160
                   db=config.get('db','db'),
154 161
                   )
155 162
 
163
+class DBCache:
164
+    def __init__(self, db):
165
+        self._db = db
166
+        self._cache = memcache.Client(['localhost:11211'],
167
+                                      pickleProtocol=pickle.HIGHEST_PROTOCOL)
168
+
169
+    def make_key(self, table, column, value, limit):
170
+        return '/'.join((
171
+                table,
172
+                column,
173
+                hashlib.md5(str(value)).hexdigest(),
174
+                str(limit)
175
+                ))
176
+
177
+    def select(self, table, column, value, limit=None):
178
+        key = self.make_key(table, column, value, limit)
179
+
180
+        got = self._cache.get(key)
181
+        if got is not None:
182
+            counters.bump('select_cache_hit')
183
+            return got
184
+        else:
185
+            got = list(self._db.select(table, where='%s = $value' % column,
186
+                                 vars={'value': value}, limit=limit))
187
+            counters.bump('select_cache_miss')
188
+            self._cache.set(key, got, time=600)
189
+            return got
190
+
191
+    def update(self, type, id, **kwargs):
192
+        table = '1_%ss' % type
193
+        column = '%sID' % type
194
+        key = self.make_key(table, column, id, 1)
195
+        self._db.update(table, where='%s = $id' % column, vars={'id': id}, **kwargs)
196
+        self._cache.delete(key)
197
+
198
+cache = DBCache(db)
199
+
156 200
 def require_feature(name):
157 201
     if not features[name]:
158 202
         raise web.notfound()
@@ -183,7 +227,7 @@ class JSONMapper:
183 227
         if value != getattr(self, key):
184 228
             self._values[key] = value
185 229
             encoded = json.dumps(self._values)
186
-            db.update('1_users', where='userID = $id', contacts=encoded, vars={'id': self._around.userID})
230
+            cache.update('user', self._around.userID, contacts=encoded)
187 231
 
188 232
 class AutoMapper:
189 233
     def __init__(self, type, around):
@@ -220,7 +264,7 @@ class AutoMapper:
220 264
             assert self._type
221 265
             key = '%sID' % self._type
222 266
 
223
-            rows = db.select(table, where='%s = $id' % key, vars={'id': getattr(self, key)})
267
+            rows = cache.select(table, key, getattr(self, key))
224 268
             return [AutoMapper(singular, x) for x in rows]
225 269
 
226 270
         raise AttributeError(name)
@@ -255,7 +299,7 @@ def first_or_none(type, column, id, strict=False):
255 299
     no match.
256 300
     """
257 301
     table = '1_%ss' % type
258
-    vs = db.select(table, where='%s = $id' % column, vars={'id': id}, limit=1)
302
+    vs = cache.select(table, column, id, limit=1)
259 303
 
260 304
     if len(vs):
261 305
         return AutoMapper(type, vs[0])
@@ -322,7 +366,9 @@ class Model:
322 366
 
323 367
     def get_user_by_name(self, name):
324 368
         """Get a user by user name"""
325
-        return first_or_none('user', 'username', name)
369
+        name = urllib.unquote(name)
370
+        user = first('user', 'username', name)
371
+        return first('user', 'userID', user.userID)
326 372
 
327 373
     def get_gravatar(self, email):
328 374
         """Get the gravatar hash for an email"""
@@ -685,7 +731,7 @@ class hide_link:
685 731
         need_admin(_('Admin needed to hide a link'))
686 732
 
687 733
         next = not link.hidden
688
-        db.update('1_links', where='linkID = $id', hidden=next, vars={'id': id})
734
+        cache.update('link', id, hidden=next)
689 735
 
690 736
         model.inform(_("Link is hidden") if next else _("Link now shows"))
691 737
         redirect('/link/%s' % id)
@@ -697,7 +743,7 @@ class close_link:
697 743
 
698 744
         need_admin(_('Admin needed to close a link'))
699 745
         next = not link.closed
700
-        db.update('1_links', where='linkID = $id', closed=next, vars={'id': id})
746
+        cache.update('link', id, closed=next)
701 747
 
702 748
         model.inform(_("Link is closed") if next else _("Link is open"))
703 749
         redirect('/link/%s' % id)
@@ -769,21 +815,21 @@ class like_comment:
769 815
 class user:
770 816
     def GET(self, id):
771 817
         counters.bump(self)
772
-        user = first('user', 'username', id)
773
-        return render.user(user)
818
+        target = model.get_user_by_name(id)
819
+        return render.user(target)
774 820
 
775 821
 class user_links:
776 822
     def GET(self, id):
777 823
         counters.bump(self)
778
-        user = first('user', 'username', id)
779
-        return render_links(where='userID=$id', vars={'id': user.userID})
824
+        target = model.get_user_by_name(id)
825
+        return render_links(where='userID=$id', vars={'id': target.userid})
780 826
 
781 827
 class user_comments:
782 828
     def GET(self, id):
783 829
         counters.bump(self)
784
-        user = first('user', 'username', id)
830
+        userid = first('user', 'username', id).userID
785 831
         comments = db.select('1_comments', where='userID=$id', order='timestamp DESC',
786
-                             vars={'id': user.userID},
832
+                             vars={'id': userid},
787 833
                              limit=config.get('general', 'limit'))
788 834
         return render.user_comments([AutoMapper('comment', x) for x in comments])
789 835
 
@@ -874,8 +920,7 @@ class password:
874 920
                 form.note = _('Bad password')
875 921
                 return render.password(form)
876 922
 
877
-        db.update('1_users', password=pwd_context.encrypt(form.d.new_password), where='userID=$id', vars={'id': target.userID})
878
-        
923
+        cache.update('user', target.userID, password=pwd_context.encrypt(form.d.new_password))
879 924
         model.inform(_("Password changed"))
880 925
         redirect('/user/%s' % name)
881 926
 
@@ -917,12 +962,12 @@ class user_edit:
917 962
 
918 963
         for name in names:
919 964
             if target.has(name):
920
-                db.update('1_users', where='userID = $id', vars={'id': target.userID}, **{name: form[name].value})
965
+                cache.update('user', target.userID, **{name: form[name].value})
921 966
             else:
922 967
                 values.set(name, form[name].value)
923 968
 
924 969
         bio = render_input(form.d.bio)
925
-        db.update('1_users', bio=bio, where='userID = $id', vars={'id': target.userID})
970
+        cache.update('user', target.userID, bio=bio)
926 971
         redirect('/user/%s' % username)
927 972
 
928 973
 
@@ -939,7 +984,7 @@ class rss:
939 984
 class debug_counters:
940 985
     def GET(self):
941 986
         counters.bump(self)
942
-        need_admin('Only admins can access server status pages.')
987
+        need_admin(_('Only admins can access debug pages.'))
943 988
         web.header('Content-Type', 'application/json')
944 989
         return json.dumps(counters.get_snapshot())
945 990
 

+ 1 - 0
strings.py 파일 보기

@@ -28,6 +28,7 @@ admin_needed_to_close_a_link = 'Admin needed to close a link'
28 28
 bad_username_or_password = "Bad username or password"
29 29
 only_the_user_can_checkout_their_links = "Only the user can checkout their links"
30 30
 possible_cross_site_request_forgery_try_again = "Possible cross site request forgery.  Try again."
31
+only_admins_can_access_debug_pages = "Only admins can access debug pages."
31 32
 field_realname = "Real name"
32 33
 field_email = "Email"
33 34
 field_gravatar_email = "Gravatar email"