import redis import json import threading import logging logger = logging.getLogger(__name__) class CannotReadUserEventsException(Exception): """ Exception raised if user events cannot be read. """ class UserEventBuilder(object): """ Defines a helper class for constructing UserEvent and UserEventListener instances. """ def __init__(self, redis_config): self._redis_config = redis_config def get_event(self, username): return UserEvent(self._redis_config, username) def get_listener(self, username, events): return UserEventListener(self._redis_config, username, events) class UserEventsBuilderModule(object): def __init__(self, app=None): self.app = app if app is not None: self.state = self.init_app(app) else: self.state = None def init_app(self, app): redis_config = app.config.get('USER_EVENTS_REDIS') if not redis_config: # This is the old key name. redis_config = { 'host': app.config.get('USER_EVENTS_REDIS_HOSTNAME'), } user_events = UserEventBuilder(redis_config) # register extension with app app.extensions = getattr(app, 'extensions', {}) app.extensions['userevents'] = user_events return user_events def __getattr__(self, name): return getattr(self.state, name, None) class UserEvent(object): """ Defines a helper class for publishing to realtime user events as backed by Redis. """ def __init__(self, redis_config, username): self._redis = redis.StrictRedis(socket_connect_timeout=5, **redis_config) self._username = username @staticmethod def _user_event_key(username, event_id): return 'user/%s/events/%s' % (username, event_id) def publish_event_data_sync(self, event_id, data_obj): return self._redis.publish(self._user_event_key(self._username, event_id), json.dumps(data_obj)) def publish_event_data(self, event_id, data_obj): """ Publishes the serialized form of the data object for the given event. Note that this occurs in a thread to prevent blocking. """ def conduct(): try: self.publish_event_data_sync(event_id, data_obj) logger.debug('Published user event %s: %s', event_id, data_obj) except Exception: logger.exception('Could not publish user event') thread = threading.Thread(target=conduct) thread.start() class UserEventListener(object): """ Defines a helper class for subscribing to realtime user events as backed by Redis. """ def __init__(self, redis_config, username, events=set([])): channels = [self._user_event_key(username, e) for e in events] try: self._redis = redis.StrictRedis(socket_connect_timeout=5, **redis_config) self._pubsub = self._redis.pubsub() self._pubsub.subscribe(channels) except redis.RedisError as re: logger.exception('Could not reach user events redis: %s', re) raise CannotReadUserEventsException @staticmethod def _user_event_key(username, event_id): return 'user/%s/events/%s' % (username, event_id) def event_stream(self): """ Starts listening for events on the channel(s), yielding for each event found. """ for item in self._pubsub.listen(): channel = item['channel'] event_id = channel.split('/')[3] # user/{username}/{events}/{id} data = None try: data = json.loads(item['data'] or '{}') except: pass if data: yield event_id, data def stop(self): """ Unsubscribes from the channel(s). Should be called once the connection has terminated. """ self._pubsub.unsubscribe()