class ArameConsumer(BrightsideConsumer): """ Implements reading a message from an RMQ broker. It uses a queue, created by subscribing to a message topic """ RETRY_OPTIONS = { 'interval_start': 1, 'interval_step': 1, 'interval_max': 1, 'max_retries': 3, } def __init__(self, connection: Connection, configuration: BrightsideConsumerConfiguration, logger: logging.Logger=None) -> None: self._exchange = Exchange(connection.exchange, type=connection.exchange_type, durable=connection.is_durable) self._routing_key = configuration.routing_key self._amqp_uri = connection.amqp_uri self._queue_name = configuration.queue_name self._routing_key = configuration.routing_key self._prefetch_count = configuration.prefetch_count self._is_durable = configuration.is_durable self._heartbeat = connection.heartbeat self._connect_timeout = connection.connect_timeout self._message_factory = ArameMessageFactory() self._logger = logger or logging.getLogger(__name__) self._conn = None consumer_arguments = {} if configuration.is_ha is True: consumer_arguments = {"x-ha-policy": "all"} self._is_long_running_handler = configuration.is_long_runing_handler self._queue = Queue(self._queue_name, exchange=self._exchange, routing_key=self._routing_key, durable=self._is_durable, consumer_arguments=consumer_arguments) self._msg = None # Kombu Message self._message = None # Brightside Message self._establish_connection(BrokerConnection(hostname=self._amqp_uri, connect_timeout=self._connect_timeout, heartbeat=self._heartbeat)) self._establish_channel() self._establish_consumer() def acknowledge(self, message: BrightsideMessage): if (self._message is not None) and self._message.id == message.id: self._msg.ack() self._msg = None def _ensure_connection(self): # We can get connection aborted before we try to read, so despite ensure() # we check the connection here if self._conn.connected is not True: self._conn = self._conn.clone() self._conn.ensure_connection(max_retries=3) self._channel = self._conn.channel() self._consumer.revive(self._channel) self._consumer.consume() def _establish_channel(self): self._channel = self._conn.channel() def _establish_consumer(self): self._consumer = Consumer(channel=self._channel, queues=[self._queue], callbacks=[self._read_message]) self._consumer.qos(prefetch_count=1) self._consumer.consume() def _establish_connection(self, conn: BrokerConnection) -> None: """ We don't use a pool here. We only have one consumer connection per process, so we get no value from a pool, and we want to use a heartbeat to keep the consumer collection alive, which does not work with a pool :return: the connection to the transport """ try: self._logger.debug("Establishing connection.") self._conn = conn.ensure_connection(max_retries=3) self._logger.debug('Got connection: %s', conn.as_uri()) except kombu_exceptions.OperationalError as oe: self._logger.error("Error connecting to RMQ, could not retry %s", oe) # Try to clean up the mess if self._conn is not None: self._conn.close() else: conn.close() def has_acknowledged(self, message): if (self._message is not None) and self._message.id == message.id: if self._msg is None: return True else: return False def purge(self, timeout: int = 5) -> None: def _purge_errors(exc, interval): self._logger.error('Purging error: %s, will retry triggering in %s seconds', exc, interval, exc_info=True) def _purge_messages(cnsmr: BrightsideConsumer): cnsmr.purge() self._message = None self._ensure_connection() ensure_kwargs = self.RETRY_OPTIONS.copy() ensure_kwargs['errback'] = _purge_errors safe_purge = self._conn.ensure(self._consumer, _purge_messages, **ensure_kwargs) safe_purge(self._consumer) def _read_message(self, body: str, msg: KombuMessage) -> None: self._logger.debug("Monitoring event received at: %s headers: %s payload: %s", datetime.utcnow().isoformat(), msg.headers, body) self._msg = msg self._message = self._message_factory.create_message(msg) def receive(self, timeout: int) -> BrightsideMessage: self._message = BrightsideMessage(BrightsideMessageHeader(uuid4(), "", BrightsideMessageType.MT_NONE), BrightsideMessageBody("")) def _consume(cnx: BrokerConnection, timesup: int) -> None: try: cnx.drain_events(timeout=timesup) except kombu_exceptions.TimeoutError: self._logger.debug("Time out reading from queue %s", self._queue_name) cnx.heartbeat_check() except(kombu_exceptions.ChannelLimitExceeded, kombu_exceptions.ConnectionLimitExceeded, kombu_exceptions.OperationalError, kombu_exceptions.NotBoundError, kombu_exceptions.MessageStateError, kombu_exceptions.LimitExceeded) as err: raise ChannelFailureException("Error connecting to RabbitMQ, see inner exception for details", err) except (OSError, IOError, ConnectionError) as socket_err: self._reset_connection() raise ChannelFailureException("Error connecting to RabbitMQ, see inner exception for details", socket_err) def _consume_errors(exc, interval: int)-> None: self._logger.error('Draining error: %s, will retry triggering in %s seconds', exc, interval, exc_info=True) self._ensure_connection() ensure_kwargs = self.RETRY_OPTIONS.copy() ensure_kwargs['errback'] = _consume_errors safe_drain = self._conn.ensure(self._consumer, _consume, **ensure_kwargs) safe_drain(self._conn, timeout) return self._message def _reset_connection(self) -> None: self._logger.debug('Reset connection to RabbitMQ following socket error') self._conn.close() self._establish_connection(BrokerConnection(hostname=self._amqp_uri, connect_timeout=self._connect_timeout, heartbeat=self._heartbeat)) self._establish_channel() self._establish_consumer() def requeue(self, message: BrightsideMessage) -> None: """ TODO: has does a consumer resend """ self._msg.requeue() def run_heartbeat_continuously(self) -> threading.Event: """ For a long runing handler, there is a danger that we do not send a heartbeat message or activity on the connection whilst we are running the handler. With a default heartbeat of 30s, for example, there is a risk that a handler which takes more than 15s will fail to send the heartbeat in time and then the broker will reset the connection. So we spin up another thread, where the user has marked the thread as having a long-running thread :return: an event to cancel the thread """ cancellation_event = threading.Event() # Effectively a no-op if we are not actually a long-running thread if not self._is_long_running_handler: return cancellation_event self._logger.debug("Running long running handler on %s", self._conn) def _send_heartbeat(cnx: BrokerConnection, period: int, logger: logging.Logger) -> None: while not cancellation_event.is_set(): cnx.heartbeat_check() cancellation_event.wait(timeout= period) logger.debug("Signalled to exit long-running handler heartbeat") heartbeat_thread = threading.Thread(target=_send_heartbeat, args=(self._conn, 1, self._logger), daemon=True) self._logger.debug("Begin heartbeat thread for %s", self._conn) heartbeat_thread.start() self._logger.debug("Heartbeat running on thread for %s", self._conn) return cancellation_event def stop(self): if self._conn is not None: self._logger.debug("Closing connection: %s", self._conn) self._conn.close() self._conn = None
class ArameConsumer(BrightsideConsumer): """ Implements reading a message from an RMQ broker. It uses a queue, created by subscribing to a message topic """ RETRY_OPTIONS = { 'interval_start': 1, 'interval_step': 1, 'interval_max': 1, 'max_retries': 3, } def __init__(self, connection: ArameConnection, queue_name: str, routing_key: str, prefetch_count: int=1, is_durable: bool=False, logger: logging.Logger=None) -> None: self._exchange = Exchange(connection.exchange, type=connection.exchange_type, durable=connection.is_durable) self._routing_key = routing_key self._amqp_uri = connection.amqp_uri self._queue_name = queue_name self._routing_key = routing_key self._prefetch_count = prefetch_count self._is_durable = is_durable self._message_factory = ArameMessageFactory() self._logger = logger or logging.getLogger(__name__) self._queue = Queue(self._queue_name, exchange=self._exchange, routing_key=self._routing_key) self._msg = None # Kombu Message self._message = None # Brightside Message # TODO: Need to fix the argument types with default types issue def acknowledge(self, message: BrightsideMessage): if (self._message is not None) and self._message.id == message.id: self._msg.ack() self._msg = None def has_acknowledged(self, message): if (self._message is not None) and self._message.id == message.id: if self._msg is None: return True else: return False def purge(self, timeout: int = 5) -> None: def _purge_errors(exc, interval): self._logger.error('Purging error: %s, will retry triggering in %s seconds', exc, interval, exc_info=True) def _purge_messages(cnsmr: BrightsideConsumer): cnsmr.purge() self._message = None connection = BrokerConnection(hostname=self._amqp_uri) with connections[connection].acquire(block=True) as conn: self._logger.debug('Got connection: %s', conn.as_uri()) with Consumer([self._queue], callbacks=[_purge_messages]) as consumer: ensure_kwargs = self.RETRY_OPTIONS.copy() ensure_kwargs['errback'] = _purge_errors safe_purge = conn.ensure(consumer, _purge_messages, **ensure_kwargs) safe_purge(consumer) def receive(self, timeout: int) -> BrightsideMessage: self._message = BrightsideMessage(BrightsideMessageHeader(uuid4(), "", BrightsideMessageType.none), BrightsideMessageBody("")) def _consume(cnx: BrokerConnection, timesup: int) -> None: try: cnx.drain_events(timeout=timesup) except kombu_exceptions.TimeoutError: pass except(kombu_exceptions.ChannelLimitExceeded, kombu_exceptions.ConnectionLimitExceeded, kombu_exceptions.OperationalError, kombu_exceptions.NotBoundError, kombu_exceptions.MessageStateError, kombu_exceptions.LimitExceeded) as err: raise ChannelFailureException("Error connecting to RabbitMQ, see inner exception for details", err) def _consume_errors(exc, interval: int)-> None: self._logger.error('Draining error: %s, will retry triggering in %s seconds', exc, interval, exc_info=True) def _read_message(body: str, msg: KombuMessage) -> None: self._logger.debug("Monitoring event received at: %s headers: %s payload: %s", datetime.utcnow().isoformat(), msg.headers, body) self._msg = msg self._message = self._message_factory.create_message(msg) connection = BrokerConnection(hostname=self._amqp_uri) with connections[connection].acquire(block=True) as conn: self._logger.debug('Got connection: %s', conn.as_uri()) with Consumer(conn, queues=[self._queue], callbacks=[_read_message]) as consumer: consumer.qos(prefetch_count=1) ensure_kwargs = self.RETRY_OPTIONS.copy() ensure_kwargs['errback'] = _consume_errors safe_drain = conn.ensure(consumer, _consume, **ensure_kwargs) safe_drain(conn, timeout) return self._message
class ArameConsumer(BrightsideConsumer): """ Implements reading a message from an RMQ broker. It uses a queue, created by subscribing to a message topic """ RETRY_OPTIONS = { 'interval_start': 1, 'interval_step': 1, 'interval_max': 1, 'max_retries': 3, } def __init__(self, connection: Connection, configuration: BrightsideConsumerConfiguration, logger: logging.Logger = None) -> None: self._exchange = Exchange(connection.exchange, type=connection.exchange_type, durable=connection.is_durable) self._routing_key = configuration.routing_key self._amqp_uri = connection.amqp_uri self._queue_name = configuration.queue_name self._routing_key = configuration.routing_key self._prefetch_count = configuration.prefetch_count self._is_durable = configuration.is_durable self._message_factory = ArameMessageFactory() self._logger = logger or logging.getLogger(__name__) self._queue = Queue(self._queue_name, exchange=self._exchange, routing_key=self._routing_key, durable=self._is_durable) self._msg = None # Kombu Message self._message = None # Brightside Message # TODO: Need to fix the argument types with default types issue def acknowledge(self, message: BrightsideMessage): if (self._message is not None) and self._message.id == message.id: self._msg.ack() self._msg = None def has_acknowledged(self, message): if (self._message is not None) and self._message.id == message.id: if self._msg is None: return True else: return False def purge(self, timeout: int = 5) -> None: def _purge_errors(exc, interval): self._logger.error( 'Purging error: %s, will retry triggering in %s seconds', exc, interval, exc_info=True) def _purge_messages(cnsmr: BrightsideConsumer): cnsmr.purge() self._message = None connection = BrokerConnection(hostname=self._amqp_uri) with connections[connection].acquire(block=True) as conn: self._logger.debug('Got connection: %s', conn.as_uri()) with Consumer(conn, queues=[self._queue], callbacks=[_purge_messages]) as consumer: ensure_kwargs = self.RETRY_OPTIONS.copy() ensure_kwargs['errback'] = _purge_errors safe_purge = conn.ensure(consumer, _purge_messages, **ensure_kwargs) safe_purge(consumer) def receive(self, timeout: int) -> BrightsideMessage: self._message = BrightsideMessage( BrightsideMessageHeader(uuid4(), "", BrightsideMessageType.MT_NONE), BrightsideMessageBody("")) def _consume(cnx: BrokerConnection, timesup: int) -> None: try: cnx.drain_events(timeout=timesup) except kombu_exceptions.TimeoutError: pass except (kombu_exceptions.ChannelLimitExceeded, kombu_exceptions.ConnectionLimitExceeded, kombu_exceptions.OperationalError, kombu_exceptions.NotBoundError, kombu_exceptions.MessageStateError, kombu_exceptions.LimitExceeded) as err: raise ChannelFailureException( "Error connecting to RabbitMQ, see inner exception for details", err) def _consume_errors(exc, interval: int) -> None: self._logger.error( 'Draining error: %s, will retry triggering in %s seconds', exc, interval, exc_info=True) def _read_message(body: str, msg: KombuMessage) -> None: self._logger.debug( "Monitoring event received at: %s headers: %s payload: %s", datetime.utcnow().isoformat(), msg.headers, body) self._msg = msg self._message = self._message_factory.create_message(msg) connection = BrokerConnection(hostname=self._amqp_uri) with connections[connection].acquire(block=True) as conn: self._logger.debug('Got connection: %s', conn.as_uri()) with Consumer(conn, queues=[self._queue], callbacks=[_read_message]) as consumer: consumer.qos(prefetch_count=1) ensure_kwargs = self.RETRY_OPTIONS.copy() ensure_kwargs['errback'] = _consume_errors safe_drain = conn.ensure(consumer, _consume, **ensure_kwargs) safe_drain(conn, timeout) return self._message def requeue(self, message: BrightsideMessage) -> None: """ TODO: has does a consumeure resend """ self._msg.requeue()