comparison core/whos_online.py @ 581:ee87ea74d46b

For Django 1.4, rearranged project structure for new manage.py.
author Brian Neal <bgneal@gmail.com>
date Sat, 05 May 2012 17:10:48 -0500
parents gpp/core/whos_online.py@f72ace06658a
children 2f5779e9d8f8
comparison
equal deleted inserted replaced
580:c525f3e0b5d0 581:ee87ea74d46b
1 """
2 This module keeps track of who is online. We maintain records for both
3 authenticated users ("users") and non-authenticated visitors ("visitors").
4 """
5 import datetime
6 import logging
7 import time
8
9 import redis
10
11 from core.services import get_redis_connection
12 from core.models import Statistic
13
14
15 # Users and visitors each have a sorted set in a Redis database. When a user or
16 # visitor is seen, the respective set is updated with the score of the current
17 # time. Periodically we remove elements by score (time) to stale out members.
18
19 # Redis key names:
20 USER_SET_KEY = "whos_online:users"
21 VISITOR_SET_KEY = "whos_online:visitors"
22
23 CORE_STATS_KEY = "core:stats"
24
25 # the period over which we collect who's online stats:
26 MAX_AGE = datetime.timedelta(minutes=15)
27
28
29 # Logging: we don't want a Redis malfunction to bring down the site. So we
30 # catch all Redis exceptions, log them, and press on.
31 logger = logging.getLogger(__name__)
32
33
34 def _get_connection():
35 """
36 Create and return a Redis connection. Returns None on failure.
37 """
38 try:
39 conn = get_redis_connection()
40 return conn
41 except redis.RedisError, e:
42 logger.error(e)
43
44 return None
45
46
47 def to_timestamp(dt):
48 """
49 Turn the supplied datetime object into a UNIX timestamp integer.
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
59 """
60 conn = _get_connection()
61 if conn:
62 ts = to_timestamp(datetime.datetime.now())
63 try:
64 conn.zadd(key, ts, member)
65 except redis.RedisError, e:
66 logger.error(e)
67
68
69 def _zrangebyscore(key):
70 """
71 Performs a zrangebyscore operation on the set given by key.
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
76 """
77 conn = _get_connection()
78 if conn:
79 now = datetime.datetime.now()
80 min = to_timestamp(now - MAX_AGE)
81 max = to_timestamp(now)
82 try:
83 return conn.zrangebyscore(key, min, max)
84 except redis.RedisError, e:
85 logger.error(e)
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
107
108 def get_users_online():
109 """
110 Returns a list of user names from the user set.
111 sets.
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 """
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
198 if conn:
199 try:
200 stats = conn.hgetall(CORE_STATS_KEY)
201 except redis.RedisError, e:
202 logger.error(e)
203
204 if stats:
205 return Statistic(
206 id=1,
207 max_users=stats['max_users'],
208 max_users_date=datetime.datetime.fromtimestamp(
209 float(stats['max_users_date'])),
210 max_anon_users=stats['max_anon_users'],
211 max_anon_users_date=datetime.datetime.fromtimestamp(
212 float(stats['max_anon_users_date'])))
213
214 try:
215 stats = Statistic.objects.get(pk=1)
216 except Statistic.DoesNotExist:
217 return None
218 else:
219 _save_stats_to_redis(conn, stats)
220 return stats
221
222
223 def _save_stats_to_redis(conn, stats):
224 """
225 Saves the statistics to Redis. A TTL is put on the key to prevent Redis and
226 the database from becoming out of sync.
227
228 """
229 fields = dict(
230 max_users=stats.max_users,
231 max_users_date=to_timestamp(stats.max_users_date),
232 max_anon_users=stats.max_anon_users,
233 max_anon_users_date=to_timestamp(stats.max_anon_users_date))
234
235 try:
236 conn.hmset(CORE_STATS_KEY, fields)
237 conn.expire(CORE_STATS_KEY, 4 * 60 * 60)
238 except redis.RedisError, e:
239 logger.error(e)