diff gpp/core/whos_online.py @ 423:3fe60148f75c

Fixing #203; use Redis for who's online function.
author Brian Neal <bgneal@gmail.com>
date Sat, 23 Apr 2011 19:19:38 +0000
parents
children 6f5fff924877
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/core/whos_online.py	Sat Apr 23 19:19:38 2011 +0000
@@ -0,0 +1,133 @@
+"""
+This module keeps track of who is online. We maintain records for both
+authenticated users ("users") and non-authenticated visitors ("visitors").
+"""
+import logging
+
+from django.conf import settings
+import redis
+
+# Users and visitors each have 2 sets that we store in a Redis database:
+# a current set and an old set. Whenever a user or visitor is seen, the
+# current set is updated. At some interval, the current set is renamed
+# to the old set, thus destroying it. At any given time, the union of the
+# current and old sets is the "who's online" set.
+
+# Redis connection and database settings
+
+HOST = getattr(settings, 'WHOS_ONLINE_REDIS_HOST', 'localhost')
+PORT = getattr(settings, 'WHOS_ONLINE_REDIS_PORT', 6379)
+DB = getattr(settings, 'WHOS_ONLINE_REDIS_DB', 0)
+
+# Redis key names:
+USER_CURRENT_KEY = "wo_user_current"
+USER_OLD_KEY = "wo_user_old"
+USER_KEYS = [USER_CURRENT_KEY, USER_OLD_KEY]
+
+VISITOR_CURRENT_KEY = "wo_visitor_current"
+VISITOR_OLD_KEY = "wo_visitor_old"
+VISITOR_KEYS = [VISITOR_CURRENT_KEY, VISITOR_OLD_KEY]
+
+
+# Logging: we don't want a Redis malfunction to bring down the site. So we
+# catch all Redis exceptions, log them, and press on.
+logger = logging.getLogger(__name__)
+
+
+def _get_connection():
+    """
+    Create and return a Redis connection. Returns None on failure.
+    """
+    try:
+        conn = redis.Redis(host=HOST, port=PORT, db=DB)
+        return conn
+    except redis.RedisError, e:
+        logger.error(e)
+
+    return None
+
+
+def report_user(username):
+    """
+    Call this function when a user has been seen. The username will be added to
+    the current set.
+    """
+    conn = _get_connection()
+    if conn:
+        try:
+            conn.sadd(USER_CURRENT_KEY, username)
+        except redis.RedisError, e:
+            logger.error(e)
+
+
+def report_visitor(ip):
+    """
+    Call this function when a visitor has been seen. The IP address will be
+    added the current set.
+    """
+    conn = _get_connection()
+    if conn:
+        try:
+            conn.sadd(VISITOR_CURRENT_KEY, ip)
+        except redis.RedisError, e:
+            logger.error(e)
+
+
+def get_users_online():
+    """
+    Returns a set of user names which is the union of the current and old
+    sets.
+    """
+    conn = _get_connection()
+    if conn:
+        try:
+            # Note that keys that do not exist are considered empty sets
+            return conn.sunion(USER_KEYS)
+        except redis.RedisError, e:
+            logger.error(e)
+
+    return set()
+
+
+def get_visitors_online():
+    """
+    Returns a set of visitor IP addresses which is the union of the current
+    and old sets.
+    """
+    conn = _get_connection()
+    if conn:
+        try:
+            # Note that keys that do not exist are considered empty sets
+            return conn.sunion(VISITOR_KEYS)
+        except redis.RedisError, e:
+            logger.error(e)
+
+    return set()
+
+
+def _tick_set(conn, current, old):
+    """
+    This function renames the set named "current" to "old".
+    """
+    # An exception may be raised if the current key doesn't exist; if that
+    # happens we have to delete the old set because no one is online.
+    try:
+        conn.rename(current, old)
+    except redis.ResponseError:
+        try:
+            del conn[old]
+        except redis.RedisError, e:
+            logger.error(e)
+    except redis.RedisError, e:
+        logger.error(e)
+
+
+def tick():
+    """
+    Call this function to "age out" the old sets by renaming the current sets
+    to the old.
+    """
+    conn = _get_connection()
+    if conn:
+        _tick_set(conn, USER_CURRENT_KEY, USER_OLD_KEY)
+        _tick_set(conn, VISITOR_CURRENT_KEY, VISITOR_OLD_KEY)