|
|
|
import time
|
|
|
|
import traceback
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
# from concurrent.futures.thread import _shutdown
|
|
|
|
from typing import Dict, Type, Union
|
|
|
|
|
|
|
|
from amqpstorm import AMQPError
|
|
|
|
from loguru import logger
|
|
|
|
|
|
|
|
from amqpworker import conf
|
|
|
|
from amqpworker.easyqueue.message import AMQPMessage
|
|
|
|
from amqpworker.easyqueue.queue import JsonQueue, QueueConsumerDelegate
|
|
|
|
from amqpworker.options import Events, Options
|
|
|
|
from amqpworker.routes import AMQPRoute
|
|
|
|
from .bucket import Bucket
|
|
|
|
from .rabbitmq import RabbitMQMessage
|
|
|
|
from .scheduled import ScheduledThreadPoolExecutor
|
|
|
|
from .utils import call
|
|
|
|
|
|
|
|
|
|
|
|
class Consumer(QueueConsumerDelegate):
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
route_info: Union[Dict, AMQPRoute],
|
|
|
|
host: str,
|
|
|
|
port: int,
|
|
|
|
username: str,
|
|
|
|
password: str,
|
|
|
|
prefetch_count: int = 128,
|
|
|
|
bucket_class: Type[Bucket] = Bucket[RabbitMQMessage],
|
|
|
|
) -> None:
|
|
|
|
self.route = route_info
|
|
|
|
self._handler = route_info["handler"]
|
|
|
|
self._queue_name = route_info["routes"]
|
|
|
|
self._route_options = route_info["options"]
|
|
|
|
self.host = host
|
|
|
|
self.vhost = route_info.get("vhost", "/")
|
|
|
|
self.bucket = bucket_class(
|
|
|
|
size=min(self._route_options["bulk_size"], prefetch_count)
|
|
|
|
)
|
|
|
|
self.running = False
|
|
|
|
self.queue: JsonQueue = JsonQueue(
|
|
|
|
host,
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
port,
|
|
|
|
virtual_host=self.vhost,
|
|
|
|
delegate=self,
|
|
|
|
prefetch_count=prefetch_count,
|
|
|
|
logger=conf.logger,
|
|
|
|
connection_fail_callback=self._route_options.get(
|
|
|
|
Options.CONNECTION_FAIL_CALLBACK, None
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
self.pool = ThreadPoolExecutor(thread_name_prefix=f'submit-{"-".join(self._queue_name)}-',
|
|
|
|
max_workers=self._route_options.get('max_workers', 8))
|
|
|
|
self.multiple_queue_pool = ThreadPoolExecutor(thread_name_prefix='consumer-queue-',
|
|
|
|
max_workers=len(self._queue_name))
|
|
|
|
self.clock = ScheduledThreadPoolExecutor(max_workers=1, name='flush-rabbit-')
|
|
|
|
self.clock_task = None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def queue_name(self) -> str:
|
|
|
|
return self._queue_name
|
|
|
|
|
|
|
|
def on_before_start_consumption(
|
|
|
|
self, queue_name: str, queue: "JsonQueue"
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
Coroutine called before queue consumption starts. May be overwritten to
|
|
|
|
implement further custom initialization.
|
|
|
|
|
|
|
|
:param queue_name: Queue name that will be consumed
|
|
|
|
:type queue_name: str
|
|
|
|
:param queue: AsynQueue instanced
|
|
|
|
:type queue: JsonQueue
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
def on_consumption_start(self, consumer_tag: str, queue: "JsonQueue"):
|
|
|
|
"""
|
|
|
|
Coroutine called once consumption started.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
def on_queue_message(self, msg: AMQPMessage):
|
|
|
|
"""
|
|
|
|
Callback called every time that a new, valid and deserialized message
|
|
|
|
is ready to be handled.
|
|
|
|
"""
|
|
|
|
if not self.bucket.is_full():
|
|
|
|
message = RabbitMQMessage(
|
|
|
|
delivery_tag=msg.delivery_tag,
|
|
|
|
amqp_message=msg,
|
|
|
|
on_success=self._route_options[Events.ON_SUCCESS],
|
|
|
|
on_exception=self._route_options[Events.ON_EXCEPTION],
|
|
|
|
)
|
|
|
|
self.bucket.put(message)
|
|
|
|
|
|
|
|
if self.bucket.is_full():
|
|
|
|
return self._flush_bucket_if_needed()
|
|
|
|
|
|
|
|
def _flush_clocked(self, *args, **kwargs):
|
|
|
|
try:
|
|
|
|
self._flush_bucket_if_needed()
|
|
|
|
except Exception as e:
|
|
|
|
conf.logger.error(
|
|
|
|
{
|
|
|
|
"type": "flush-bucket-failed",
|
|
|
|
"dest": self.host,
|
|
|
|
"retry": True,
|
|
|
|
"exc_traceback": traceback.format_exc(),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
def _flush_bucket_if_needed(self):
|
|
|
|
|
|
|
|
if not self.bucket.is_empty():
|
|
|
|
all_messages = self.bucket.pop_all()
|
|
|
|
try:
|
|
|
|
conf.logger.debug(
|
|
|
|
{
|
|
|
|
"event": "bucket-flush",
|
|
|
|
"bucket-size": len(all_messages),
|
|
|
|
"handler": self._handler.__name__,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
if self.running:
|
|
|
|
rv = self._handler(all_messages)
|
|
|
|
for _ in self.pool.map(call, [m.process_success for m in all_messages]):
|
|
|
|
pass
|
|
|
|
return rv
|
|
|
|
else:
|
|
|
|
for m in all_messages:
|
|
|
|
m.process_exception()
|
|
|
|
except AMQPError:
|
|
|
|
raise
|
|
|
|
except Exception as e:
|
|
|
|
traceback.print_exc()
|
|
|
|
if self.running:
|
|
|
|
for _ in self.pool.map(call, [m.process_exception for m in all_messages]):
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
for m in all_messages:
|
|
|
|
m.process_exception()
|
|
|
|
raise e
|
|
|
|
|
|
|
|
# def on_queue_error(self, body, delivery_tag, error, queue):
|
|
|
|
# """
|
|
|
|
# Callback called every time that an error occurred during the validation
|
|
|
|
# or deserialization stage.
|
|
|
|
#
|
|
|
|
# :param body: unparsed, raw message content
|
|
|
|
# :type body: Any
|
|
|
|
# :param delivery_tag: delivery_tag of the consumed message
|
|
|
|
# :type delivery_tag: int
|
|
|
|
# :param error: THe error that caused the callback to be called
|
|
|
|
# :type error: MessageError
|
|
|
|
# :type queue: JsonQueue
|
|
|
|
# """
|
|
|
|
# conf.logger.error(
|
|
|
|
# {
|
|
|
|
# "parse-error": True,
|
|
|
|
# "exception": "Error: not a JSON",
|
|
|
|
# "original_msg": body,
|
|
|
|
# }
|
|
|
|
# )
|
|
|
|
# try:
|
|
|
|
# queue.ack(delivery_tag=delivery_tag)
|
|
|
|
# except AMQPError as e:
|
|
|
|
# self._log_exception(e)
|
|
|
|
|
|
|
|
def on_message_handle_error(self, handler_error: Exception, **kwargs):
|
|
|
|
"""
|
|
|
|
Callback called when an uncaught exception was raised during message
|
|
|
|
handling stage.
|
|
|
|
|
|
|
|
:param handler_error: The exception that triggered
|
|
|
|
:param kwargs: arguments used to call the coroutine that handled
|
|
|
|
the message
|
|
|
|
"""
|
|
|
|
self._log_exception(handler_error)
|
|
|
|
|
|
|
|
def on_connection_error(self, exception: Exception):
|
|
|
|
"""
|
|
|
|
Called when the connection fails
|
|
|
|
"""
|
|
|
|
self._log_exception(exception)
|
|
|
|
|
|
|
|
def _log_exception(self, exception):
|
|
|
|
current_exception = {
|
|
|
|
"exc_message": str(exception),
|
|
|
|
"exc_traceback": traceback.format_exc(),
|
|
|
|
}
|
|
|
|
conf.logger.error(current_exception)
|
|
|
|
|
|
|
|
def consume_all_queues(self, queue: JsonQueue):
|
|
|
|
|
|
|
|
# for r in self.multiple_queue_pool.map(queue.consume, self._queue_name, repeat(self)):
|
|
|
|
# pass
|
|
|
|
for queue_name in self._queue_name:
|
|
|
|
# Por enquanto não estamos guardando a consumer_tag retornada
|
|
|
|
# se precisar, podemos passar a guardar.
|
|
|
|
conf.logger.debug(
|
|
|
|
{"queue": queue_name, "event": "start-consume"}
|
|
|
|
)
|
|
|
|
queue.consume(queue_name=queue_name, pool=self.multiple_queue_pool, delegate=self)
|
|
|
|
|
|
|
|
def keep_runnig(self):
|
|
|
|
return self.running
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
logger.debug('stop consumer')
|
|
|
|
self.running = False
|
|
|
|
self.queue.connection.close()
|
|
|
|
self.pool.shutdown(False)
|
|
|
|
self.multiple_queue_pool.shutdown(False)
|
|
|
|
self.clock.stop(None)
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
self.running = True
|
|
|
|
if not self.clock_task:
|
|
|
|
seconds = self._route_options.get(
|
|
|
|
Options.BULK_FLUSH_INTERVAL, conf.settings.FLUSH_TIMEOUT
|
|
|
|
)
|
|
|
|
self.clock.schedule_at_fixed_rate(self._flush_clocked, seconds, seconds)
|
|
|
|
self.clock.start(None)
|
|
|
|
self.clock_task = self._flush_clocked
|
|
|
|
while self.keep_runnig():
|
|
|
|
if not self.queue.connection.has_channel_ready():
|
|
|
|
try:
|
|
|
|
self.consume_all_queues(self.queue)
|
|
|
|
except RuntimeError as e:
|
|
|
|
traceback.print_exc()
|
|
|
|
if self.multiple_queue_pool._shutdown: # or _shutdown:
|
|
|
|
conf.logger.info('app shutdown')
|
|
|
|
# if 'interpreter shutdown' in str(e):
|
|
|
|
# return
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
self.stop()
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
conf.logger.error(
|
|
|
|
{
|
|
|
|
"type": "connection-failed",
|
|
|
|
"dest": self.host,
|
|
|
|
"retry": True,
|
|
|
|
"exc_traceback": traceback.format_exc(),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
time.sleep(1)
|