annotate core/whos_online.py @ 683:c83d330cb65f

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