""" * Maps the request to RabbitMQ exchange. Handles the service discovery logic. * * Router can be either in 'Master' or 'Service (Consumer)' modes. 'Master' mode is the outward * facing side that receives the requests and routes it to queue associated with requested service. * 'Service' mode is consumer side for given service, which receives the message from AMQ and * forwards it to the application. """ import logging import os import pika from pika import BasicProperties, spec from pika.adapters.blocking_connection import BlockingChannel from pika.exchange_type import ExchangeType from amqp.adapter.service_message_factory import CleverMicroServiceMessageFactory from amqp.config.amq_configuration import AMQConfiguration from amqp.model.model import CleverMicroServiceMessage, ServiceMessageType from amqp.router.router_base import RouterBase, CM_REQUEST_EXCHANGE, CM_RESPONSE_EXCHANGE, CM_C_AMQ_QUEUE, \ CM_P_RESP_QUEUE, CM_P_REPLY_TO, ANY_RECIPIENT class RouterProducer(RouterBase): def __init__(self, configuration: AMQConfiguration): """ * Initialize router - need to supply the config values and RabbitMQ channel. * :param configuration: adapter's configuration reference. """ super().__init__() self.connection = None self.service_message_factory = CleverMicroServiceMessageFactory(configuration.amq_adapter.service_name) self.amq_configuration: AMQConfiguration = configuration self.channel: pika.adapters.blocking_connection.BlockingChannel | None = None logging.info(f" [*] RouterProducer.: %s, route: %s", self.amq_configuration.amq_adapter.service_name, self.amq_configuration.amq_adapter.route_mapping) def amq_consumer(self): credentials = pika.PlainCredentials( os.environ['RABBIT_MQ_USER'], os.environ['RABBIT_MQ_PASSWORD'] ) self.connection = pika.BlockingConnection( pika.ConnectionParameters(host='localhost', credentials=credentials) ) self.channel = self.connection.channel() self.channel.start_consuming() def init_routing_paths(self, channel: pika.adapters.blocking_connection.BlockingChannel | None): """ * Initialize router according to Producer mode. * The logic here sets a producer and listener on CleverMicroRequest/CleverMicroResponse queues * created to enable the service discovery. * The Producer mode sends out routing data request on CleverMicroRequest and listens * on CleverMicroResponse for any service responding with its routing data. """ self.channel = channel logging.info(" [*] RouterProducer.initRoutingPaths, channel open: %s", self.is_open(self.channel)) if self.is_open(self.channel): try: # **** set up the service discovery internal exchanges **** # 1. declare RequestExchange to where to publish the RoutingData requests self.channel.exchange_declare(exchange=CM_REQUEST_EXCHANGE, exchange_type=ExchangeType.fanout) # # 2. declare ResponseExchange where to publish current RoutingData (response to # a RoutingData request) self.channel.exchange_declare(exchange=CM_RESPONSE_EXCHANGE, exchange_type=ExchangeType.fanout) # 2a. declare a Response queue for Routing Data replies from Services cm_response_queue: str = self.get_response_queue_name() self.channel.queue_declare(cm_response_queue, durable=True, exclusive=False, auto_delete=False, arguments={}) # 2b. bind queue to CM_RESPONSE_EXCHANGE w/o any conditions (empty routing key) self.channel.queue_bind(queue=cm_response_queue, exchange=CM_RESPONSE_EXCHANGE, routing_key="#") # 2c. listen for incoming RoutingData messages self.channel.basic_consume( queue=cm_response_queue, auto_ack=False, on_message_callback=self.handle_routing_data_message) self.channel.start_consuming() logging.info(" [*] Routing master listens for Route updates on %s", cm_response_queue) except Exception as ioe: logging.error(" [E] RouterProducer FATAL: cannot initialize, %s", ioe) else: logging.error(" [E] RouterProducer FATAL: cannot initialize, RabbitMQ channel is not open.") def handle_routing_data_message(self, ch: BlockingChannel, method: spec.Basic.Deliver, properties: spec.BasicProperties, body: bytes): """ * Handler for CLEVER_MICRO_RESPONSE queue messages. Used in Master mode to handle receipt of * routing data, invokes addRoute or removeRoute depending on the type of the message. """ print(f" [x] Received {body}, m={method}, p={properties}, ch={ch}") """ logging.info(" [DBG] ******[%s] ROUTING_DATA %s >>> %s", delivery.getEnvelope().getDeliveryTag(), delivery.getEnvelope().toString(), str(delivery.getBody())) """ # ACK message now so that it is not being redelivered self.channel.basic_ack(method.delivery_tag, False) message: CleverMicroServiceMessage = self.service_message_factory.from_bytes(body) # ignore the message if it comes from ourselves (even if from different instance) logging.info(" [DBG] ******[%s] ROUTING_DATA, msg=%s, routeMap=[{] sn={, replyTo={", method.delivery_tag, self.service_message_factory.to_string(message), self.amq_configuration.amq_adapter.route_mapping, self.amq_configuration.amq_adapter.service_name, message.reply_to) if message.is_valid() and not self.amq_configuration.amq_adapter.service_name == message.reply_to: if message.message_type == ServiceMessageType.ROUTING_DATA: self.add_routes(message.message) if message.message_type == ServiceMessageType.ROUTING_DATA_REMOVE: self.remove_routes(message.message) def request_routes_from_adapters(self): """ * Send a broadcast message prompting all listening adapters to reply with own routing data. """ logging.info(" [*] RouterProducer.requestRoutesFromAdapters, queue name: %s", CM_REQUEST_EXCHANGE) try: serviceMessage: CleverMicroServiceMessage = self.service_message_factory.of( ServiceMessageType.ROUTING_DATA_REQUEST, "", ANY_RECIPIENT ) if not self.publish_service_message(serviceMessage, CM_REQUEST_EXCHANGE): logging.warning(" [E] RouterProducer could not request route data: %s channel is not open.", CM_REQUEST_EXCHANGE) except Exception as ioe: logging.error(" [E] RouterProducer failed request routing data: %s", ioe) BASIC: BasicProperties = BasicProperties(content_type="application/octet-stream", content_encoding=None, headers=None, delivery_mode=1, priority=0, correlation_id=None) def publish_service_message(self, message: CleverMicroServiceMessage, exchange_name: str) -> bool: """ * Publishes a service message to the service queue and logs success log entry. * * :param message: Service Message to be sent * :param exchangeName: target exchange name where to publish the message * :param queueName target: queue name (Optional, purpose is to get the queue size only) * :return True if channel is open, False otherwise * @throws IOException if something else goes wrong """ if self.is_open(self.channel): binary_content: bytes = self.service_message_factory.serialize(message) self.channel.basic_publish(exchange=exchange_name, routing_key="", # empty routing key properties=self.BASIC, body=binary_content) logging.info(" [x] Service Message Sent to %s, msg: %s", exchange_name, str(binary_content)) return True return False def get_reply_to_queue_name(self) -> str: """ * self should create a unique name for self process and also when there's several instances. * @return unique reply-to queue name. """ return self.amq_configuration.amq_adapter.adapter_prefix() + CM_P_REPLY_TO def get_response_queue_name(self) -> str: """ * self should create a unique name for self process and also when there's several instances. * @return unique reply-to queue name. """ return self.amq_configuration.amq_adapter.adapter_prefix() + CM_P_RESP_QUEUE def get_consume_queue_name(self) -> str: """ * self should create a unique name for self process and also when there's several instances. * @return unique reply-to queue name. """ return self.amq_configuration.amq_adapter.adapter_prefix() + CM_C_AMQ_QUEUE def is_open(self, _channel: pika.adapters.blocking_connection.BlockingChannel) -> bool: """ * Test if the channel is usable (opened). * :param _channel: the channel * :return True if the channel is opened. """ return _channel is not None and _channel.is_open