""" * 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. """ from aio_pika import Message from aio_pika.abc import ( AbstractIncomingMessage, AbstractRobustChannel, AbstractRobustConnection, AbstractRobustExchange, AbstractRobustQueue, ) from pika.exchange_type import ExchangeType from amqp.adapter.logging_utils import ( logging_debug, logging_error, logging_info, logging_warning, ) from amqp.adapter.service_message_factory import ServiceMessageFactory from amqp.config.amq_configuration import AMQConfiguration from amqp.model.model import ServiceMessage from amqp.model.service_message_type import ServiceMessageType from amqp.router.router_base import ( ANY_RECIPIENT, CM_C_AMQ_QUEUE, CM_P_REPLY_TO, CM_P_RESP_QUEUE, CM_REQUEST_EXCHANGE, CM_RESPONSE_EXCHANGE, RouterBase, ) 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: AbstractRobustConnection | None = None self.service_message_factory = ServiceMessageFactory(configuration.amq_adapter.service_name) self.amq_configuration: AMQConfiguration = configuration self.channel: AbstractRobustChannel | None = None self.cm_request_exchange: AbstractRobustExchange | None = None self.cm_response_exchange: AbstractRobustExchange | None = None logging_info( "RouterProducer.: %s, route: %s", self.amq_configuration.amq_adapter.service_name, self.amq_configuration.amq_adapter.route_mapping, ) async def init_internal_routing_paths(self, channel: AbstractRobustChannel | None): """ * Initialize internal routes for ServiceMessages. * 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.cm_request_exchange = await self.channel.declare_exchange( name=CM_REQUEST_EXCHANGE, type=ExchangeType.fanout ) # # 2. declare ResponseExchange where to publish current RoutingData (response to # a RoutingData request) self.cm_response_exchange = await self.channel.declare_exchange( name=CM_RESPONSE_EXCHANGE, type=ExchangeType.fanout ) # 2a. declare a Response queue for Routing Data replies from Services _cm_response_queue: str = self.get_response_queue_name() _response_queue: AbstractRobustQueue = await self.channel.declare_queue( name=_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) await _response_queue.bind(exchange=CM_RESPONSE_EXCHANGE, routing_key="#") # 2c. listen for incoming RoutingData messages _c_tag = await _response_queue.consume( callback=self.handle_routing_data_message, no_ack=False ) logging_info( " [%s] Routing master listens for Route updates on %s", _c_tag, _cm_response_queue, ) except Exception as ioe: logging_error("RouterProducer FATAL: cannot initialize, %s", ioe) else: logging_error("RouterProducer FATAL: cannot initialize, RabbitMQ channel is not open.") async def handle_routing_data_message(self, message: AbstractIncomingMessage): """ * 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_string of the message. """ logging_info( "[%s] Received ROUTING DATA: [%s], msgId=%s", message.delivery_tag, message.body, message.message_id, ) await message.ack(multiple=False) cm_message: ServiceMessage = self.service_message_factory.from_bytes(message.body) # ignore the message if it comes from ourselves (even if from different instance) logging_debug( "******[%s] ROUTING_DATA, msg=%s, routeMap=[%s] sn=%s, replyTo=%s", message.delivery_tag, self.service_message_factory.to_string(cm_message), self.amq_configuration.amq_adapter.route_mapping, self.amq_configuration.amq_adapter.service_name, cm_message.reply_to, ) if ( cm_message.is_valid() and not self.amq_configuration.amq_adapter.service_name == cm_message.reply_to ): if cm_message.message_type == ServiceMessageType.ROUTING_DATA: self.add_routes(cm_message.message) if cm_message.message_type == ServiceMessageType.ROUTING_DATA_REMOVE: self.remove_routes(cm_message.message) async 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: ServiceMessage = self.service_message_factory.of( ServiceMessageType.ROUTING_DATA_REQUEST, "", ANY_RECIPIENT ) if not await self.publish_service_message(serviceMessage, self.cm_request_exchange): logging_warning( "RouterProducer could not request route data: %s channel is not open.", CM_REQUEST_EXCHANGE, ) except Exception as ioe: logging_error("RouterProducer failed request routing data: %s", ioe) async def publish_service_message( self, message: ServiceMessage, exchange: AbstractRobustExchange ) -> bool: """ * Publishes a service message to the service queue and logs success log entry. * * :param message: Service Message to be sent * :param exchange: target exchange name where to publish the message * :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) pika_message: Message = Message( body=binary_content, content_encoding="utf-8", delivery_mode=2, content_type="application/octet-stream", headers=None, priority=0, correlation_id=None, ) await exchange.publish(message=pika_message, routing_key="") logging_info( "Service Message Published 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: AbstractRobustChannel) -> 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 not _channel.is_closed