import logging from typing import Set from aio_pika.abc import ( AbstractRobustChannel, AbstractRobustQueue, AbstractIncomingMessage, ) from amqp.adapter.amq_route_factory import AMQRouteFactory from amqp.adapter.logging_utils import ( logging_error, logging_info, logging_debug, logging_warning, ) from amqp.config.amq_configuration import AMQConfiguration from amqp.model.model import ServiceMessage, AMQRoute from amqp.model.service_message_type import ServiceMessageType from amqp.router.router_base import ( CM_C_REQ_QUEUE, CM_REQUEST_EXCHANGE, CM_RESPONSE_EXCHANGE, ANY_RECIPIENT, ) from amqp.router.router_producer import RouterProducer class RouterConsumer(RouterProducer): """ * Contains Consumer logic. The logic here: * Sets a listener on CM_C_REQ_QUEUE queue to receive routing data requests * Upon receipt of routing data request, responds on CM_RESPONSE_EXCHANGE with local routing data. * It also proactively pushes the local routing data to CM_RESPONSE_EXCHANGE on startup to * notify the other (excluding self) Producers about its presence. * * Consumer is also Producer (Consumer sits alongside the CM Service, and anytime the Service needs * to communicate with other Service, it can do so via the Adapter). Therefore, the Producer * capability is also required. """ def __init__(self, configuration: AMQConfiguration): super().__init__(configuration) self.amqRouteFactory = AMQRouteFactory() logging_info("RouterConsumer.") async def init_routing_paths_consumer(self, _channel: AbstractRobustChannel): """ * Initialize router according to Consumer mode. """ logging_info( "RouterConsumer.initConsumerRouter, channel open: %s", self.is_open(_channel), ) if self.is_open(_channel): try: # 1. declare a Queue to receive the RoutingData requests _cm_request_queue: str = ( self.amq_configuration.amq_adapter.adapter_prefix() + CM_C_REQ_QUEUE ) _queue: AbstractRobustQueue = await self.channel.declare_queue( name=_cm_request_queue, durable=True, exclusive=False, auto_delete=False, ) # 1a. bind queue to CM_REQUEST_EXCHANGE w/o any conditions (empty routing key) await _queue.bind(exchange=CM_REQUEST_EXCHANGE, routing_key="") # 1b. Service adapter mode - listens on Request queue and replies to Response queue _c_tag = await _queue.consume( callback=self.handle_routing_data_request_message, no_ack=False ) logging_info( " [%s] RouterConsumer listens for RouteDataRequests on: %s", _c_tag, _cm_request_queue, ) except Exception as ioe: logging_error("RouterConsumer FATAL: cannot initialize, %s", ioe) else: logging_error( "RouterConsumer FATAL: cannot initialize, RabbitMQ channel is not open." ) """ * ServiceMessageType.ROUTING_DATA_REQUEST handler listening on queue 'CleverMicroRequest' """ async def handle_routing_data_request_message( self, message: AbstractIncomingMessage ): # some debug statements, TODO - remove for prod release delivery_tag = message.delivery_tag debug = "Delivery.Properties = " + message.properties.__str__() logging_debug( " ******[%s] (ROUTING_DATA_REQ) tag: %s, env: %s, props: %s", delivery_tag, message.consumer_tag, message.properties, debug, ) await message.ack(False) cm_message: ServiceMessage = self.service_message_factory.from_bytes( message.body ) route_mapping: str = self.wrap_own_routes() logging_debug( "******[%s] (ROUTING_DATA_REQ) msg=%s, routeMap=[%s]", delivery_tag, self.service_message_factory.to_string(cm_message), route_mapping, ) # Reply only if self adapter has own routeMapping (e.g. API-gateway-side adapter has none) if ( cm_message.is_valid() and cm_message.message_type == ServiceMessageType.ROUTING_DATA_REQUEST and route_mapping ): if cm_message.reply_to != self.amq_configuration.amq_adapter.service_name: try: service_message: ServiceMessage = self.service_message_factory.of( ServiceMessageType.ROUTING_DATA, route_mapping, cm_message.reply_to, ) await self.publish_service_message( service_message, self.cm_response_exchange ) except Exception as err: logging_error( "******[%s] (ROUTING_DATA_REQ) Response to queue: %s failed: %s", delivery_tag, CM_RESPONSE_EXCHANGE, err, ) else: logging_info( "******[%s] (ROUTING_DATA_REQ) [%s] Ignore request originated by ourselves.", delivery_tag, str(message.body), ) else: logging_error( "******[%s] (ROUTING_DATA_REQ) [%s] produces invalid or no CM msg type.", delivery_tag, str(message.body), ) async def register_route(self, route: AMQRoute): """ * Called in Consumer (Service Adapter) context. Registers the routing data for self Service * with the Master router by sending own routing data to Master via CleverMicroResponse queue. * :param route: route to register with Master """ try: if self.is_open(self.channel) and route.queue: serviceMessage: ServiceMessage = self.service_message_factory.of( ServiceMessageType.ROUTING_DATA, route.as_string(), ANY_RECIPIENT ) await self.publish_service_message( serviceMessage, self.cm_response_exchange ) self.add_own_route(route) logging_info("RouterConsumer registered Route: %s", route) else: logging_warning( "RouterConsumer couldn't register route data: %s channel isn't open.", CM_RESPONSE_EXCHANGE, ) except Exception as ioe: logging_error("RouterConsumer failed register route data: %s", ioe) async def remove_route_on_cleanup(self, route: AMQRoute) -> bool: """ * Unregister / remove route from the list * :param route: route to remove * :return result of remove operation """ logging_info("RouterConsumer.removeRoute, route: %s", route.as_string()) try: service_message: ServiceMessage = self.service_message_factory.of( ServiceMessageType.ROUTING_DATA_REMOVE, route.as_string(), ANY_RECIPIENT ) if not await self.publish_service_message( service_message, self.cm_response_exchange ): logging_warning( "RouterConsumer couldn't remove current route: %s, channel not open.", route, ) except Exception as ioe: logging_error("RouterConsumer failed remove current route: %s", ioe) result: bool = self.remove_route(route) return result def cleanup(self): """ * Cleanup - de-register routes with master. """ routes: Set[AMQRoute] = self.get_routes() logging_info( "RouterConsumer.cleanup() [%d]: %s", len(routes), self.wrap_routes() ) not_removed = sum( 1 for r in (self.remove_route_on_cleanup(route) for route in routes) if not r ) if not_removed > 0: logging_warning( "%d route(s) not removed. self may lead to failed delivery on self route", not_removed, )