bgneal@423: """ bgneal@423: This module keeps track of who is online. We maintain records for both bgneal@423: authenticated users ("users") and non-authenticated visitors ("visitors"). bgneal@423: """ bgneal@423: import logging bgneal@423: bgneal@423: import redis bgneal@423: bgneal@508: from core.services import get_redis_connection bgneal@508: bgneal@508: bgneal@423: # Users and visitors each have 2 sets that we store in a Redis database: bgneal@423: # a current set and an old set. Whenever a user or visitor is seen, the bgneal@423: # current set is updated. At some interval, the current set is renamed bgneal@423: # to the old set, thus destroying it. At any given time, the union of the bgneal@423: # current and old sets is the "who's online" set. bgneal@423: bgneal@423: # Redis key names: bgneal@423: USER_CURRENT_KEY = "wo_user_current" bgneal@423: USER_OLD_KEY = "wo_user_old" bgneal@423: USER_KEYS = [USER_CURRENT_KEY, USER_OLD_KEY] bgneal@423: bgneal@423: VISITOR_CURRENT_KEY = "wo_visitor_current" bgneal@423: VISITOR_OLD_KEY = "wo_visitor_old" bgneal@423: VISITOR_KEYS = [VISITOR_CURRENT_KEY, VISITOR_OLD_KEY] bgneal@423: bgneal@423: bgneal@423: # Logging: we don't want a Redis malfunction to bring down the site. So we bgneal@423: # catch all Redis exceptions, log them, and press on. bgneal@423: logger = logging.getLogger(__name__) bgneal@423: bgneal@423: bgneal@423: def _get_connection(): bgneal@423: """ bgneal@423: Create and return a Redis connection. Returns None on failure. bgneal@423: """ bgneal@423: try: bgneal@508: conn = get_redis_connection() bgneal@423: return conn bgneal@423: except redis.RedisError, e: bgneal@423: logger.error(e) bgneal@423: bgneal@423: return None bgneal@423: bgneal@423: bgneal@423: def report_user(username): bgneal@423: """ bgneal@423: Call this function when a user has been seen. The username will be added to bgneal@423: the current set. bgneal@423: """ bgneal@423: conn = _get_connection() bgneal@423: if conn: bgneal@423: try: bgneal@423: conn.sadd(USER_CURRENT_KEY, username) bgneal@423: except redis.RedisError, e: bgneal@423: logger.error(e) bgneal@423: bgneal@423: bgneal@423: def report_visitor(ip): bgneal@423: """ bgneal@423: Call this function when a visitor has been seen. The IP address will be bgneal@423: added the current set. bgneal@423: """ bgneal@423: conn = _get_connection() bgneal@423: if conn: bgneal@423: try: bgneal@423: conn.sadd(VISITOR_CURRENT_KEY, ip) bgneal@423: except redis.RedisError, e: bgneal@423: logger.error(e) bgneal@423: bgneal@423: bgneal@423: def get_users_online(): bgneal@423: """ bgneal@423: Returns a set of user names which is the union of the current and old bgneal@423: sets. bgneal@423: """ bgneal@423: conn = _get_connection() bgneal@423: if conn: bgneal@423: try: bgneal@423: # Note that keys that do not exist are considered empty sets bgneal@423: return conn.sunion(USER_KEYS) bgneal@423: except redis.RedisError, e: bgneal@423: logger.error(e) bgneal@423: bgneal@423: return set() bgneal@423: bgneal@423: bgneal@423: def get_visitors_online(): bgneal@423: """ bgneal@423: Returns a set of visitor IP addresses which is the union of the current bgneal@423: and old sets. bgneal@423: """ bgneal@423: conn = _get_connection() bgneal@423: if conn: bgneal@423: try: bgneal@423: # Note that keys that do not exist are considered empty sets bgneal@423: return conn.sunion(VISITOR_KEYS) bgneal@423: except redis.RedisError, e: bgneal@423: logger.error(e) bgneal@423: bgneal@423: return set() bgneal@423: bgneal@423: bgneal@423: def _tick_set(conn, current, old): bgneal@423: """ bgneal@423: This function renames the set named "current" to "old". bgneal@423: """ bgneal@423: # An exception may be raised if the current key doesn't exist; if that bgneal@423: # happens we have to delete the old set because no one is online. bgneal@423: try: bgneal@423: conn.rename(current, old) bgneal@423: except redis.ResponseError: bgneal@423: try: bgneal@423: del conn[old] bgneal@423: except redis.RedisError, e: bgneal@423: logger.error(e) bgneal@423: except redis.RedisError, e: bgneal@423: logger.error(e) bgneal@423: bgneal@423: bgneal@423: def tick(): bgneal@423: """ bgneal@423: Call this function to "age out" the old sets by renaming the current sets bgneal@423: to the old. bgneal@423: """ bgneal@423: conn = _get_connection() bgneal@423: if conn: bgneal@423: _tick_set(conn, USER_CURRENT_KEY, USER_OLD_KEY) bgneal@423: _tick_set(conn, VISITOR_CURRENT_KEY, VISITOR_OLD_KEY)