Mercurial > public > sg101
comparison gpp/core/whos_online.py @ 519:f72ace06658a
For #194, rework the who's online and max users functions.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Sat, 17 Dec 2011 19:29:24 +0000 |
parents | 6f5fff924877 |
children |
comparison
equal
deleted
inserted
replaced
518:5171a5e9353b | 519:f72ace06658a |
---|---|
1 """ | 1 """ |
2 This module keeps track of who is online. We maintain records for both | 2 This module keeps track of who is online. We maintain records for both |
3 authenticated users ("users") and non-authenticated visitors ("visitors"). | 3 authenticated users ("users") and non-authenticated visitors ("visitors"). |
4 """ | 4 """ |
5 import datetime | |
5 import logging | 6 import logging |
7 import time | |
6 | 8 |
7 import redis | 9 import redis |
8 | 10 |
9 from core.services import get_redis_connection | 11 from core.services import get_redis_connection |
10 | 12 from core.models import Statistic |
11 | 13 |
12 # Users and visitors each have 2 sets that we store in a Redis database: | 14 |
13 # a current set and an old set. Whenever a user or visitor is seen, the | 15 # Users and visitors each have a sorted set in a Redis database. When a user or |
14 # current set is updated. At some interval, the current set is renamed | 16 # visitor is seen, the respective set is updated with the score of the current |
15 # to the old set, thus destroying it. At any given time, the union of the | 17 # time. Periodically we remove elements by score (time) to stale out members. |
16 # current and old sets is the "who's online" set. | |
17 | 18 |
18 # Redis key names: | 19 # Redis key names: |
19 USER_CURRENT_KEY = "wo_user_current" | 20 USER_SET_KEY = "whos_online:users" |
20 USER_OLD_KEY = "wo_user_old" | 21 VISITOR_SET_KEY = "whos_online:visitors" |
21 USER_KEYS = [USER_CURRENT_KEY, USER_OLD_KEY] | 22 |
22 | 23 CORE_STATS_KEY = "core:stats" |
23 VISITOR_CURRENT_KEY = "wo_visitor_current" | 24 |
24 VISITOR_OLD_KEY = "wo_visitor_old" | 25 # the period over which we collect who's online stats: |
25 VISITOR_KEYS = [VISITOR_CURRENT_KEY, VISITOR_OLD_KEY] | 26 MAX_AGE = datetime.timedelta(minutes=15) |
26 | 27 |
27 | 28 |
28 # Logging: we don't want a Redis malfunction to bring down the site. So we | 29 # Logging: we don't want a Redis malfunction to bring down the site. So we |
29 # catch all Redis exceptions, log them, and press on. | 30 # catch all Redis exceptions, log them, and press on. |
30 logger = logging.getLogger(__name__) | 31 logger = logging.getLogger(__name__) |
41 logger.error(e) | 42 logger.error(e) |
42 | 43 |
43 return None | 44 return None |
44 | 45 |
45 | 46 |
46 def report_user(username): | 47 def to_timestamp(dt): |
47 """ | 48 """ |
48 Call this function when a user has been seen. The username will be added to | 49 Turn the supplied datetime object into a UNIX timestamp integer. |
49 the current set. | 50 |
51 """ | |
52 return int(time.mktime(dt.timetuple())) | |
53 | |
54 | |
55 def _zadd(key, member): | |
56 """ | |
57 Adds the member to the given set key, using the current time as the score. | |
58 | |
50 """ | 59 """ |
51 conn = _get_connection() | 60 conn = _get_connection() |
52 if conn: | 61 if conn: |
62 ts = to_timestamp(datetime.datetime.now()) | |
53 try: | 63 try: |
54 conn.sadd(USER_CURRENT_KEY, username) | 64 conn.zadd(key, ts, member) |
55 except redis.RedisError, e: | 65 except redis.RedisError, e: |
56 logger.error(e) | 66 logger.error(e) |
57 | 67 |
58 | 68 |
59 def report_visitor(ip): | 69 def _zrangebyscore(key): |
60 """ | 70 """ |
61 Call this function when a visitor has been seen. The IP address will be | 71 Performs a zrangebyscore operation on the set given by key. |
62 added the current set. | 72 The minimum score will be a timestap equal to the current time |
73 minus MAX_AGE. The maximum score will be a timestap equal to the | |
74 current time. | |
75 | |
63 """ | 76 """ |
64 conn = _get_connection() | 77 conn = _get_connection() |
65 if conn: | 78 if conn: |
79 now = datetime.datetime.now() | |
80 min = to_timestamp(now - MAX_AGE) | |
81 max = to_timestamp(now) | |
66 try: | 82 try: |
67 conn.sadd(VISITOR_CURRENT_KEY, ip) | 83 return conn.zrangebyscore(key, min, max) |
68 except redis.RedisError, e: | 84 except redis.RedisError, e: |
69 logger.error(e) | 85 logger.error(e) |
70 | 86 |
87 return [] | |
88 | |
89 | |
90 def report_user(username): | |
91 """ | |
92 Call this function when a user has been seen. The username will be added to | |
93 the set of users online. | |
94 | |
95 """ | |
96 _zadd(USER_SET_KEY, username) | |
97 | |
98 | |
99 def report_visitor(ip): | |
100 """ | |
101 Call this function when a visitor has been seen. The IP address will be | |
102 added to the set of visitors online. | |
103 | |
104 """ | |
105 _zadd(VISITOR_SET_KEY, ip) | |
106 | |
71 | 107 |
72 def get_users_online(): | 108 def get_users_online(): |
73 """ | 109 """ |
74 Returns a set of user names which is the union of the current and old | 110 Returns a list of user names from the user set. |
75 sets. | 111 sets. |
76 """ | 112 """ |
113 return _zrangebyscore(USER_SET_KEY) | |
114 | |
115 | |
116 def get_visitors_online(): | |
117 """ | |
118 Returns a list of visitor IP addresses from the visitor set. | |
119 """ | |
120 return _zrangebyscore(VISITOR_SET_KEY) | |
121 | |
122 | |
123 def _tick(conn): | |
124 """ | |
125 Call this function to "age out" the sets by removing old users/visitors. | |
126 It then returns a tuple of the form: | |
127 (zcard users, zcard visitors) | |
128 | |
129 """ | |
130 cutoff = to_timestamp(datetime.datetime.now() - MAX_AGE) | |
131 | |
132 try: | |
133 pipeline = conn.pipeline(transaction=False) | |
134 pipeline.zremrangebyscore(USER_SET_KEY, 0, cutoff) | |
135 pipeline.zremrangebyscore(VISITOR_SET_KEY, 0, cutoff) | |
136 pipeline.zcard(USER_SET_KEY) | |
137 pipeline.zcard(VISITOR_SET_KEY) | |
138 result = pipeline.execute() | |
139 except redis.RedisError, e: | |
140 logger.error(e) | |
141 return 0, 0 | |
142 | |
143 return result[2], result[3] | |
144 | |
145 | |
146 def max_users(): | |
147 """ | |
148 Run this function periodically to clean out the sets and to compute our max | |
149 users and max visitors statistics. | |
150 | |
151 """ | |
77 conn = _get_connection() | 152 conn = _get_connection() |
153 if not conn: | |
154 return | |
155 | |
156 num_users, num_visitors = _tick(conn) | |
157 now = datetime.datetime.now() | |
158 | |
159 stats = get_stats(conn) | |
160 update = False | |
161 | |
162 if stats is None: | |
163 stats = Statistic(id=1, | |
164 max_users=num_users, | |
165 max_users_date=now, | |
166 max_anon_users=num_visitors, | |
167 max_anon_users_date=now) | |
168 update = True | |
169 else: | |
170 if num_users > stats.max_users: | |
171 stats.max_users = num_users | |
172 stats.max_users_date = now | |
173 update = True | |
174 | |
175 if num_visitors > stats.max_anon_users: | |
176 stats.max_anon_users = num_visitors | |
177 stats.max_anon_users_date = now | |
178 update = True | |
179 | |
180 if update: | |
181 _save_stats_to_redis(conn, stats) | |
182 stats.save() | |
183 | |
184 | |
185 def get_stats(conn=None): | |
186 """ | |
187 This function retrieves the who's online max user stats out of Redis. If | |
188 the keys do not exist in Redis, we fall back to the database. If the stats | |
189 are not available, None is returned. | |
190 Note that if we can find stats data, it will be returned as a Statistic | |
191 object. | |
192 | |
193 """ | |
194 if conn is None: | |
195 conn = _get_connection() | |
196 | |
197 stats = None | |
78 if conn: | 198 if conn: |
79 try: | 199 try: |
80 # Note that keys that do not exist are considered empty sets | 200 stats = conn.hgetall(CORE_STATS_KEY) |
81 return conn.sunion(USER_KEYS) | |
82 except redis.RedisError, e: | 201 except redis.RedisError, e: |
83 logger.error(e) | 202 logger.error(e) |
84 | 203 |
85 return set() | 204 if stats: |
86 | 205 return Statistic( |
87 | 206 id=1, |
88 def get_visitors_online(): | 207 max_users=stats['max_users'], |
89 """ | 208 max_users_date=datetime.datetime.fromtimestamp( |
90 Returns a set of visitor IP addresses which is the union of the current | 209 float(stats['max_users_date'])), |
91 and old sets. | 210 max_anon_users=stats['max_anon_users'], |
92 """ | 211 max_anon_users_date=datetime.datetime.fromtimestamp( |
93 conn = _get_connection() | 212 float(stats['max_anon_users_date']))) |
94 if conn: | 213 |
95 try: | 214 try: |
96 # Note that keys that do not exist are considered empty sets | 215 stats = Statistic.objects.get(pk=1) |
97 return conn.sunion(VISITOR_KEYS) | 216 except Statistic.DoesNotExist: |
98 except redis.RedisError, e: | 217 return None |
99 logger.error(e) | 218 else: |
100 | 219 _save_stats_to_redis(conn, stats) |
101 return set() | 220 return stats |
102 | 221 |
103 | 222 |
104 def _tick_set(conn, current, old): | 223 def _save_stats_to_redis(conn, stats): |
105 """ | 224 """ |
106 This function renames the set named "current" to "old". | 225 Saves the statistics to Redis. A TTL is put on the key to prevent Redis and |
107 """ | 226 the database from becoming out of sync. |
108 # An exception may be raised if the current key doesn't exist; if that | 227 |
109 # happens we have to delete the old set because no one is online. | 228 """ |
110 try: | 229 fields = dict( |
111 conn.rename(current, old) | 230 max_users=stats.max_users, |
112 except redis.ResponseError: | 231 max_users_date=to_timestamp(stats.max_users_date), |
113 try: | 232 max_anon_users=stats.max_anon_users, |
114 del conn[old] | 233 max_anon_users_date=to_timestamp(stats.max_anon_users_date)) |
115 except redis.RedisError, e: | 234 |
116 logger.error(e) | 235 try: |
236 conn.hmset(CORE_STATS_KEY, fields) | |
237 conn.expire(CORE_STATS_KEY, 4 * 60 * 60) | |
117 except redis.RedisError, e: | 238 except redis.RedisError, e: |
118 logger.error(e) | 239 logger.error(e) |
119 | |
120 | |
121 def tick(): | |
122 """ | |
123 Call this function to "age out" the old sets by renaming the current sets | |
124 to the old. | |
125 """ | |
126 conn = _get_connection() | |
127 if conn: | |
128 _tick_set(conn, USER_CURRENT_KEY, USER_OLD_KEY) | |
129 _tick_set(conn, VISITOR_CURRENT_KEY, VISITOR_OLD_KEY) |