Ejemplo n.º 1
0
class ConfigsManager(PublisherSubscriberComponent):
    """
    This class reads all configurations and sends them over to the "config"
    topic in Rabbit MQ. Updated configs are sent as well
    """
    def __init__(self,
                 name: str,
                 logger: logging.Logger,
                 config_directory: str,
                 rabbit_ip: str,
                 file_patterns: Optional[List[str]] = None,
                 ignore_file_patterns: Optional[List[str]] = None,
                 ignore_directories: bool = True,
                 case_sensitive: bool = False):
        """
        Constructs the ConfigsManager instance
        :param config_directory: The root config directory to watch.
            This is searched recursively.
        :param file_patterns: The file patterns in the directory to watch.
            Defaults to all ini files
        :param ignore_file_patterns: Any file patterns to ignore.
            Defaults to None
        :param ignore_directories: Whether changes in directories should be
            ignored. Default: True
        :param case_sensitive: Whether the patterns in `file_patterns` and
            `ignore_file_patterns` are case sensitive. Defaults to False
        """
        if not file_patterns:
            file_patterns = ['*.ini']

        self._name = name
        self._config_directory = config_directory
        self._file_patterns = file_patterns
        self._watching = False
        self._connected_to_rabbit = False

        logger.debug("Creating config RabbitMQ connection")
        rabbitmq = RabbitMQApi(logger.getChild("config_{}".format(
            RabbitMQApi.__name__)),
                               host=rabbit_ip)

        super().__init__(logger, rabbitmq)

        self._logger.debug("Creating heartbeat RabbitMQ connection")
        self._heartbeat_rabbit = RabbitMQApi(logger.getChild(
            "heartbeat_{}".format(RabbitMQApi.__name__)),
                                             host=rabbit_ip)

        self._event_handler = ConfigFileEventHandler(
            self._logger.getChild(ConfigFileEventHandler.__name__),
            self._on_event_thrown, file_patterns, ignore_file_patterns,
            ignore_directories, case_sensitive)
        self._observer = PollingObserver()
        self._observer.schedule(self._event_handler,
                                config_directory,
                                recursive=True)

    def __str__(self) -> str:
        return self.name

    @property
    def name(self) -> str:
        return self._name

    def _initialise_rabbitmq(self) -> None:
        while True:
            try:
                self._connect_to_rabbit()
                self._logger.info("Connected to Rabbit")
                self.rabbitmq.confirm_delivery()
                self._logger.info("Enabled delivery confirmation on configs"
                                  "RabbitMQ channel")

                self.rabbitmq.exchange_declare(CONFIG_EXCHANGE, 'topic', False,
                                               True, False, False)
                self._logger.info("Declared %s exchange in Rabbit",
                                  CONFIG_EXCHANGE)

                self._heartbeat_rabbit.confirm_delivery()
                self._logger.info("Enabled delivery confirmation on heartbeat"
                                  "RabbitMQ channel")

                self._heartbeat_rabbit.exchange_declare(
                    HEALTH_CHECK_EXCHANGE, 'topic', False, True, False, False)
                self._logger.info("Declared %s exchange in Rabbit",
                                  HEALTH_CHECK_EXCHANGE)

                self._logger.info(
                    "Creating and binding queue '%s' to exchange '%s' with "
                    "routing key '%s", CONFIG_PING_QUEUE,
                    HEALTH_CHECK_EXCHANGE, _HEARTBEAT_ROUTING_KEY)

                self._heartbeat_rabbit.queue_declare(CONFIG_PING_QUEUE, False,
                                                     True, False, False)
                self._logger.debug("Declared '%s' queue", CONFIG_PING_QUEUE)

                self._heartbeat_rabbit.queue_bind(CONFIG_PING_QUEUE,
                                                  HEALTH_CHECK_EXCHANGE,
                                                  'ping')
                self._logger.debug("Bound queue '%s' to exchange '%s'",
                                   CONFIG_PING_QUEUE, HEALTH_CHECK_EXCHANGE)

                # Pre-fetch count is set to 300
                prefetch_count = round(300)
                self._heartbeat_rabbit.basic_qos(prefetch_count=prefetch_count)
                self._logger.debug("Declaring consuming intentions")
                self._heartbeat_rabbit.basic_consume(CONFIG_PING_QUEUE,
                                                     self._process_ping, True,
                                                     False, None)
                break
            except (ConnectionNotInitialisedException,
                    AMQPConnectionError) as connection_error:
                # Should be impossible, but since exchange_declare can throw
                # it we shall ensure to log that the error passed through here
                # too.
                self._logger.error(
                    "Something went wrong that meant a connection was not made"
                )
                self._logger.error(connection_error.message)
                raise connection_error
            except AMQPChannelError:
                # This error would have already been logged by the RabbitMQ
                # logger and handled by RabbitMQ. As a result we don't need to
                # anything here, just re-try.
                time.sleep(RE_INITIALISE_SLEEPING_PERIOD)

    def _connect_to_rabbit(self) -> None:
        if not self._connected_to_rabbit:
            self._logger.info("Connecting to the config RabbitMQ")
            self.rabbitmq.connect_till_successful()
            self._logger.info("Connected to config RabbitMQ")
            self._logger.info("Connecting to the heartbeat RabbitMQ")
            self._heartbeat_rabbit.connect_till_successful()
            self._logger.info("Connected to heartbeat RabbitMQ")
            self._connected_to_rabbit = True
        else:
            self._logger.info(
                "Already connected to RabbitMQ, will not connect again")

    def disconnect_from_rabbit(self) -> None:
        if self._connected_to_rabbit:
            self._logger.info("Disconnecting from the config RabbitMQ")
            self.rabbitmq.disconnect_till_successful()
            self._logger.info("Disconnected from the config RabbitMQ")
            self._logger.info("Disconnecting from the heartbeat RabbitMQ")
            self._heartbeat_rabbit.disconnect_till_successful()
            self._logger.info("Disconnected from the heartbeat RabbitMQ")
            self._connected_to_rabbit = False
        else:
            self._logger.info("Already disconnected from RabbitMQ")

    def _send_heartbeat(self, data_to_send: dict) -> None:
        self._logger.debug("Sending heartbeat to the %s exchange",
                           HEALTH_CHECK_EXCHANGE)
        self._logger.debug("Sending %s", data_to_send)
        self._heartbeat_rabbit.basic_publish_confirm(
            exchange=HEALTH_CHECK_EXCHANGE,
            routing_key=_HEARTBEAT_ROUTING_KEY,
            body=data_to_send,
            is_body_dict=True,
            properties=pika.BasicProperties(delivery_mode=2),
            mandatory=True)
        self._logger.debug("Sent heartbeat to %s exchange",
                           HEALTH_CHECK_EXCHANGE)

    def _process_ping(self, ch: BlockingChannel,
                      method: pika.spec.Basic.Deliver,
                      properties: pika.spec.BasicProperties,
                      body: bytes) -> None:

        self._logger.debug("Received %s. Let's pong", body)
        try:
            heartbeat = {
                'component_name': self.name,
                'is_alive': self._observer.is_alive(),
                'timestamp': datetime.now().timestamp(),
            }

            self._send_heartbeat(heartbeat)
        except MessageWasNotDeliveredException as e:
            # Log the message and do not raise it as heartbeats must be
            # real-time
            self._logger.error("Error when sending heartbeat")
            self._logger.exception(e)

    def _send_data(self, config: Dict[str, Any], route_key: str) -> None:
        self._logger.debug("Sending %s with routing key %s", config, route_key)

        while True:
            try:
                self._logger.debug(
                    "Attempting to send config with routing key %s", route_key)
                # We need to definitely send this
                self.rabbitmq.basic_publish_confirm(
                    CONFIG_EXCHANGE,
                    route_key,
                    config,
                    mandatory=True,
                    is_body_dict=True,
                    properties=BasicProperties(delivery_mode=2))
                self._logger.info("Configuration update sent")
                break
            except MessageWasNotDeliveredException as mwnde:
                self._logger.error(
                    "Config was not successfully sent with "
                    "routing key %s", route_key)
                self._logger.exception(mwnde)
                self._logger.info(
                    "Will attempt sending the config again with "
                    "routing key %s", route_key)
                self.rabbitmq.connection.sleep(10)
            except (ConnectionNotInitialisedException,
                    AMQPConnectionError) as connection_error:
                # If the connection is not initialised or there is a connection
                # error, we need to restart the connection and try it again
                self._logger.error("There has been a connection error")
                self._logger.exception(connection_error)
                self._logger.info("Restarting the connection")
                self._connected_to_rabbit = False

                # Wait some time before reconnecting and then retrying
                time.sleep(RE_INITIALISE_SLEEPING_PERIOD)
                self._connect_to_rabbit()

                self._logger.info(
                    "Connection restored, will attempt sending "
                    "the config with routing key %s", route_key)
            except AMQPChannelError:
                # This error would have already been logged by the RabbitMQ
                # logger and handled by RabbitMQ. Since a new channel is created
                # we need to re-initialise RabbitMQ
                self._initialise_rabbitmq()

    def _on_event_thrown(self, event: FileSystemEvent) -> None:
        """
        When an event is thrown, it reads the config and sends it as a dict via
        rabbitmq to the config exchange of type topic
        with the routing key determined by the relative file path.
        :param event: The event passed by watchdog
        :return None
        """

        self._logger.debug("Event thrown: %s", event)
        self._logger.info("Detected a config %s in %s", event.event_type,
                          event.src_path)

        if event.event_type == "deleted":
            self._logger.debug("Creating empty dict")
            config_dict = {}
        else:
            config = ConfigParser()

            self._logger.debug("Reading configuration")
            try:
                config.read(event.src_path)
            except (DuplicateSectionError, DuplicateOptionError,
                    InterpolationError, ParsingError) as e:
                self._logger.error(e.message)
                # When the config is invalid, we do nothing and discard this
                # event.
                return None

            self._logger.debug("Config read successfully")

            config_dict = {key: dict(config[key]) for key in config}
        self._logger.debug("Config converted to dict: %s", config_dict)
        # Since the watcher is configured to watch files in
        # self._config_directory we only need check that (for get_routing_key)
        config_folder = os.path.normpath(self._config_directory)

        key = routing_key.get_routing_key(event.src_path, config_folder)
        self._logger.debug("Sending config %s to RabbitMQ with routing key %s",
                           config_dict, key)
        self._send_data(config_dict, key)

    @property
    def config_directory(self) -> str:
        return self._config_directory

    @property
    def watching(self) -> bool:
        return self._watching

    @property
    def connected_to_rabbit(self) -> bool:
        return self._connected_to_rabbit

    def start(self) -> None:
        """
        This method is used to start rabbit and the observer and begin watching
        the config files. It also sends the configuration files for the first
        time
        :return None
        """
        log_and_print("{} started.".format(self), self._logger)

        self._initialise_rabbitmq()

        def do_first_run_event(name: str) -> None:
            event = FileSystemEvent(name)
            event.event_type = _FIRST_RUN_EVENT
            self._on_event_thrown(event)

        self._logger.info("Throwing first run event for all config files")
        self.foreach_config_file(do_first_run_event)

        if not self._watching:
            self._logger.info("Starting config file observer")
            self._observer.start()
            self._watching = True
        else:
            self._logger.info("File observer is already running")

        self._logger.debug("Config file observer started")
        self._connect_to_rabbit()
        self._listen_for_data()

    def _listen_for_data(self) -> None:
        self._logger.info("Starting the config ping listener")
        self._heartbeat_rabbit.start_consuming()

    def _on_terminate(self, signum: int, stack: FrameType) -> None:
        """
        This method is used to stop the observer and join the threads
        """
        log_and_print(
            "{} is terminating. Connections with RabbitMQ will be "
            "closed, and afterwards the process will exit.".format(self),
            self._logger)

        if self._watching:
            self._logger.info("Stopping config file observer")
            self._observer.stop()
            self._observer.join()
            self._watching = False
            self._logger.debug("Config file observer stopped")
        else:
            self._logger.info("Config file observer already stopped")
        self.disconnect_from_rabbit()
        log_and_print("{} terminated.".format(self), self._logger)
        sys.exit()

    def foreach_config_file(self, callback: Callable[[str], None]) -> None:
        """
        Runs a function over all the files being watched by this class
        :param callback: The function to watch. Must accept a string for the
            file path as {config_directory} + {file path}
        :return: Nothing
        """
        for root, dirs, files in os.walk(self.config_directory):
            for name in files:
                if any([
                        fnmatch.fnmatch(name, pattern)
                        for pattern in self._file_patterns
                ]):
                    callback(os.path.join(root, name))
Ejemplo n.º 2
0
class TestGithubAlertersManager(unittest.TestCase):
    def setUp(self) -> None:
        self.dummy_logger = logging.getLogger('Dummy')
        self.dummy_logger.disabled = True
        self.connection_check_time_interval = timedelta(seconds=0)
        self.rabbitmq = RabbitMQApi(
            self.dummy_logger, env.RABBIT_IP,
            connection_check_time_interval=self.connection_check_time_interval)

        self.manager_name = 'test_github_alerters_manager'
        self.test_queue_name = 'Test Queue'
        self.test_data_str = 'test data'
        self.test_heartbeat = {
            'component_name': self.manager_name,
            'is_alive': True,
            'timestamp': datetime(2012, 1, 1).timestamp(),
        }
        self.github_alerter_name = GITHUB_ALERTER_NAME
        self.dummy_process1 = Process(target=infinite_fn, args=())
        self.dummy_process1.daemon = True
        self.dummy_process2 = Process(target=infinite_fn, args=())
        self.dummy_process2.daemon = True
        self.dummy_process3 = Process(target=infinite_fn, args=())
        self.dummy_process3.daemon = True

        self.test_rabbit_manager = RabbitMQApi(
            self.dummy_logger, env.RABBIT_IP,
            connection_check_time_interval=self.connection_check_time_interval)

        self.test_manager = GithubAlerterManager(
            self.dummy_logger, self.manager_name, self.rabbitmq)
        self.test_exception = PANICException('test_exception', 1)

    def tearDown(self) -> None:
        # Delete any queues and exchanges which are common across many tests
        try:
            self.test_rabbit_manager.connect()
            self.test_manager.rabbitmq.connect()
            # Declare queues incase they haven't been declared already
            self.test_manager.rabbitmq.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=False
            )
            self.test_manager.rabbitmq.queue_declare(
                queue=GITHUB_MANAGER_INPUT_QUEUE, durable=True,
                exclusive=False, auto_delete=False, passive=False
            )
            self.test_manager.rabbitmq.queue_purge(self.test_queue_name)
            self.test_manager.rabbitmq.queue_purge(GITHUB_MANAGER_INPUT_QUEUE)
            self.test_manager.rabbitmq.queue_delete(self.test_queue_name)
            self.test_manager.rabbitmq.queue_delete(GITHUB_MANAGER_INPUT_QUEUE)
            self.test_manager.rabbitmq.exchange_delete(HEALTH_CHECK_EXCHANGE)
            self.test_manager.rabbitmq.disconnect()
            self.test_rabbit_manager.disconnect()
        except Exception as e:
            print("Test failed: {}".format(e))

        self.dummy_logger = None
        self.rabbitmq = None
        self.test_manager = None
        self.test_exception = None
        self.test_rabbit_manager = None

        self.dummy_process1 = None
        self.dummy_process2 = None
        self.dummy_process3 = None

    def test_str_returns_manager_name(self) -> None:
        self.assertEqual(self.manager_name, self.test_manager.__str__())

    def test_name_returns_manager_name(self) -> None:
        self.assertEqual(self.manager_name, self.test_manager.name)

    @mock.patch.object(RabbitMQApi, "start_consuming")
    def test_listen_for_data_calls_start_consuming(
            self, mock_start_consuming) -> None:
        mock_start_consuming.return_value = None
        self.test_manager._listen_for_data()
        self.assertEqual(1, mock_start_consuming.call_count)

    @mock.patch.object(GithubAlerterManager, "_process_ping")
    def test_initialise_rabbitmq_initialises_everything_as_expected(
            self, mock_process_ping) -> None:
        mock_process_ping.return_value = None
        try:
            self.test_rabbit_manager.connect()
            # To make sure that there is no connection/channel already
            # established
            self.assertIsNone(self.rabbitmq.connection)
            self.assertIsNone(self.rabbitmq.channel)

            # To make sure that the exchanges and queues have not already been
            # declared
            self.test_manager.rabbitmq.connect()
            self.test_manager.rabbitmq.queue_delete(GITHUB_MANAGER_INPUT_QUEUE)
            self.test_manager.rabbitmq.exchange_delete(HEALTH_CHECK_EXCHANGE)

            self.test_manager._initialise_rabbitmq()

            # Perform checks that the connection has been opened, marked as open
            # and that the delivery confirmation variable is set.
            self.assertTrue(self.test_manager.rabbitmq.is_connected)
            self.assertTrue(self.test_manager.rabbitmq.connection.is_open)
            self.assertTrue(
                self.test_manager.rabbitmq.channel._delivery_confirmation)

            # Check whether the exchanges and queues have been creating by
            # sending messages with the same routing keys as for the queues. We
            # will also check if the size of the queues is 0 to confirm that
            # basic_consume was called (it will store the msg in the component
            # memory immediately). If one of the exchanges or queues is not
            # created, then either an exception will be thrown or the queue size
            # would be 1. Note when deleting the exchanges in the beginning we
            # also released every binding, hence there are no other queue binded
            # with the same routing key to any exchange at this point.
            self.test_rabbit_manager.basic_publish_confirm(
                exchange=HEALTH_CHECK_EXCHANGE,
                routing_key=GITHUB_MANAGER_INPUT_ROUTING_KEY,
                body=self.test_data_str, is_body_dict=False,
                properties=pika.BasicProperties(delivery_mode=2),
                mandatory=True)

            # Re-declare queue to get the number of messages
            res = self.test_rabbit_manager.queue_declare(
                GITHUB_MANAGER_INPUT_QUEUE, False, True, False, False)
            self.assertEqual(0, res.method.message_count)
        except Exception as e:
            self.fail("Test failed: {}".format(e))

    def test_send_heartbeat_sends_a_heartbeat_correctly(self) -> None:
        # This test creates a queue which receives messages with the same
        # routing key as the ones sent by send_heartbeat, and checks that the
        # heartbeat is received
        try:
            self.test_manager._initialise_rabbitmq()
            self.test_rabbit_manager.connect()

            res = self.test_rabbit_manager.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=False
            )
            self.assertEqual(0, res.method.message_count)
            self.test_manager.rabbitmq.queue_bind(
                queue=self.test_queue_name, exchange=HEALTH_CHECK_EXCHANGE,
                routing_key='heartbeat.manager')
            self.test_manager._send_heartbeat(self.test_heartbeat)

            # By re-declaring the queue again we can get the number of messages
            # in the queue.
            res = self.test_rabbit_manager.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=True
            )
            self.assertEqual(1, res.method.message_count)

            # Check that the message received is actually the HB
            _, _, body = self.test_rabbit_manager.basic_get(
                self.test_queue_name)
            self.assertEqual(self.test_heartbeat, json.loads(body))
        except Exception as e:
            self.fail("Test failed: {}".format(e))

    @mock.patch.object(multiprocessing.Process, "start")
    def test_create_and_start_alerter_process_creates_the_correct_process(
            self, mock_start) -> None:
        mock_start.return_value = None

        self.test_manager._start_alerters_processes()

        new_entry_process = self.test_manager.alerter_process_dict[
            GITHUB_ALERTER_NAME]

        self.assertTrue(new_entry_process.daemon)
        self.assertEqual(0, len(new_entry_process._args))
        self.assertEqual(start_github_alerter, new_entry_process._target)

    @mock.patch("src.alerter.alerter_starters.create_logger")
    def test_start_alerters_process_starts_the_process(
            self, mock_create_logger) -> None:
        mock_create_logger.return_value = self.dummy_logger
        self.test_manager._start_alerters_processes()

        # We need to sleep to give some time for the alerter to be initialised,
        # otherwise the process would not terminate
        time.sleep(1)

        new_entry_process = self.test_manager.alerter_process_dict[
            GITHUB_ALERTER_NAME]
        self.assertTrue(new_entry_process.is_alive())

        new_entry_process.terminate()
        new_entry_process.join()

    @freeze_time("2012-01-01")
    @mock.patch.object(RabbitMQApi, "basic_ack")
    @mock.patch.object(multiprocessing.Process, "is_alive")
    @mock.patch.object(multiprocessing.Process, "start")
    @mock.patch.object(multiprocessing.Process, "join")
    @mock.patch.object(multiprocessing.Process, "terminate")
    def test_process_ping_sends_a_valid_hb_if_process_is_alive(
            self, mock_terminate, mock_join, mock_start, mock_is_alive,
            mock_ack) -> None:
        # This test creates a queue which receives messages with the same
        # routing key as the ones sent by send_heartbeat, and checks that the
        # received heartbeat is valid.
        mock_ack.return_value = None
        mock_is_alive.return_value = True
        mock_start.return_value = None
        mock_join.return_value = None
        mock_terminate.return_value = None
        try:
            self.test_manager._initialise_rabbitmq()
            self.test_manager._start_alerters_processes()

            # Delete the queue before to avoid messages in the queue on error.
            self.test_manager.rabbitmq.queue_delete(self.test_queue_name)

            # initialise
            blocking_channel = self.test_manager.rabbitmq.channel
            properties = pika.spec.BasicProperties()
            method_hb = pika.spec.Basic.Deliver(routing_key='heartbeat.manager')
            body = 'ping'
            res = self.test_manager.rabbitmq.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=False
            )
            self.assertEqual(0, res.method.message_count)
            self.test_manager.rabbitmq.queue_bind(
                queue=self.test_queue_name, exchange=HEALTH_CHECK_EXCHANGE,
                routing_key='heartbeat.manager')
            self.test_manager._process_ping(blocking_channel, method_hb,
                                            properties, body)

            # By re-declaring the queue again we can get the number of messages
            # in the queue.
            res = self.test_manager.rabbitmq.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=True
            )
            self.assertEqual(1, res.method.message_count)
            expected_output = {
                "component_name": self.manager_name,
                "dead_processes": [],
                "running_processes": [GITHUB_ALERTER_NAME],
                "timestamp": datetime(2012, 1, 1).timestamp()
            }
            # Check that the message received is a valid HB
            _, _, body = self.test_manager.rabbitmq.basic_get(
                self.test_queue_name)
            self.assertEqual(expected_output, json.loads(body))
        except Exception as e:
            self.fail("Test failed: {}".format(e))

    @freeze_time("2012-01-01")
    @mock.patch.object(RabbitMQApi, "basic_ack")
    @mock.patch.object(multiprocessing.Process, "is_alive")
    @mock.patch.object(multiprocessing.Process, "start")
    @mock.patch.object(multiprocessing.Process, "join")
    @mock.patch.object(multiprocessing.Process, "terminate")
    def test_process_ping_sends_a_valid_hb_if_process_is_dead(
            self, mock_terminate, mock_join, mock_start, mock_is_alive,
            mock_ack) -> None:
        # This test creates a queue which receives messages with the same
        # routing key as the ones sent by send_heartbeat, and checks that the
        # received heartbeat is valid.
        mock_ack.return_value = None
        mock_is_alive.return_value = False
        mock_start.return_value = None
        mock_join.return_value = None
        mock_terminate.return_value = None
        try:
            self.test_manager._initialise_rabbitmq()
            self.test_manager._start_alerters_processes()

            # Delete the queue before to avoid messages in the queue on error.
            self.test_manager.rabbitmq.queue_delete(self.test_queue_name)

            # initialise
            blocking_channel = self.test_manager.rabbitmq.channel
            properties = pika.spec.BasicProperties()
            method_hb = pika.spec.Basic.Deliver(routing_key='heartbeat.manager')
            body = 'ping'
            res = self.test_manager.rabbitmq.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=False
            )
            self.assertEqual(0, res.method.message_count)
            self.test_manager.rabbitmq.queue_bind(
                queue=self.test_queue_name, exchange=HEALTH_CHECK_EXCHANGE,
                routing_key='heartbeat.manager')
            self.test_manager._process_ping(blocking_channel, method_hb,
                                            properties, body)

            # By re-declaring the queue again we can get the number of messages
            # in the queue.
            res = self.test_manager.rabbitmq.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=True
            )
            self.assertEqual(1, res.method.message_count)
            expected_output = {
                "component_name": self.manager_name,
                "dead_processes": [GITHUB_ALERTER_NAME],
                "running_processes": [],
                "timestamp": datetime(2012, 1, 1).timestamp()
            }
            # Check that the message received is a valid HB
            _, _, body = self.test_manager.rabbitmq.basic_get(
                self.test_queue_name)
            self.assertEqual(expected_output, json.loads(body))
        except Exception as e:
            self.fail("Test failed: {}".format(e))

    @freeze_time("2012-01-01")
    @mock.patch.object(RabbitMQApi, "basic_ack")
    @mock.patch("src.alerter.alerter_starters.create_logger")
    @mock.patch.object(GithubAlerterManager, "_send_heartbeat")
    def test_process_ping_restarts_dead_processes(
            self, send_hb_mock, mock_create_logger, mock_ack) -> None:
        send_hb_mock.return_value = None
        mock_create_logger.return_value = self.dummy_logger
        mock_ack.return_value = None
        try:
            self.test_manager._initialise_rabbitmq()
            self.test_manager._start_alerters_processes()

            # Give time for the processes to start
            time.sleep(1)

            # Automate the case when having all processes dead
            self.test_manager.alerter_process_dict[
                GITHUB_ALERTER_NAME].terminate()
            self.test_manager.alerter_process_dict[GITHUB_ALERTER_NAME].join()

            # Give time for the processes to terminate
            time.sleep(1)

            # Check that that the processes have terminated
            self.assertFalse(self.test_manager.alerter_process_dict[
                                 GITHUB_ALERTER_NAME].is_alive())

            # initialise
            blocking_channel = self.test_manager.rabbitmq.channel
            properties = pika.spec.BasicProperties()
            method_hb = pika.spec.Basic.Deliver(routing_key='heartbeat.manager')
            body = 'ping'
            self.test_manager._process_ping(blocking_channel, method_hb,
                                            properties, body)

            # Give time for the processes to start
            time.sleep(1)

            self.assertTrue(self.test_manager.alerter_process_dict[
                                GITHUB_ALERTER_NAME].is_alive())

            # Clean before test finishes
            self.test_manager.alerter_process_dict[
                GITHUB_ALERTER_NAME].terminate()
            self.test_manager.alerter_process_dict[GITHUB_ALERTER_NAME].join()
        except Exception as e:
            self.fail("Test failed: {}".format(e))

    @mock.patch.object(multiprocessing.Process, "is_alive")
    @mock.patch.object(multiprocessing.Process, "start")
    @mock.patch.object(multiprocessing, 'Process')
    def test_process_ping_does_not_send_hb_if_processing_fails(
            self, mock_process, mock_start, is_alive_mock) -> None:
        # This test creates a queue which receives messages with the same
        # routing key as the ones sent by send_heartbeat. In this test we will
        # check that no heartbeat is sent when mocking a raised exception.
        is_alive_mock.side_effect = self.test_exception
        mock_start.return_value = None
        mock_process.side_effect = self.dummy_process1
        try:
            self.test_manager._initialise_rabbitmq()
            self.test_manager._start_alerters_processes()

            self.test_manager.rabbitmq.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=False
            )

            # Delete the queue before to avoid messages in the queue on error.
            self.test_manager.rabbitmq.queue_delete(self.test_queue_name)

            # initialise
            blocking_channel = self.test_manager.rabbitmq.channel
            method = pika.spec.Basic.Deliver(routing_key='heartbeat.manager')
            properties = pika.spec.BasicProperties()
            body = 'ping'
            res = self.test_manager.rabbitmq.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=False
            )
            self.assertEqual(0, res.method.message_count)
            self.test_manager.rabbitmq.queue_bind(
                queue=self.test_queue_name, exchange=HEALTH_CHECK_EXCHANGE,
                routing_key='heartbeat.manager')
            self.test_manager._process_ping(blocking_channel, method,
                                            properties, body)

            # By re-declaring the queue again we can get the number of messages
            # in the queue.
            res = self.test_manager.rabbitmq.queue_declare(
                queue=self.test_queue_name, durable=True, exclusive=False,
                auto_delete=False, passive=True
            )
            self.assertEqual(0, res.method.message_count)
        except Exception as e:
            self.fail("Test failed: {}".format(e))

    def test_proc_ping_send_hb_does_not_raise_msg_not_del_exce_if_hb_not_routed(
            self) -> None:
        try:
            self.test_manager._initialise_rabbitmq()
            self.test_manager._start_alerters_processes()

            # initialise
            blocking_channel = self.test_manager.rabbitmq.channel
            method = pika.spec.Basic.Deliver(routing_key='heartbeat.manager')
            properties = pika.spec.BasicProperties()
            body = 'ping'

            self.test_manager._process_ping(blocking_channel, method,
                                            properties, body)
        except Exception as e:
            self.fail("Test failed: {}".format(e))

    @parameterized.expand([
        ("pika.exceptions.AMQPChannelError('test')",
         "pika.exceptions.AMQPChannelError"),
        ("self.test_exception", "PANICException"),
    ])
    @mock.patch.object(GithubAlerterManager, "_send_heartbeat")
    def test_process_ping_send_hb_raises_exceptions(
            self, param_input, param_expected, hb_mock) -> None:
        hb_mock.side_effect = eval(param_input)
        try:
            self.test_manager._initialise_rabbitmq()

            # initialise
            blocking_channel = self.test_manager.rabbitmq.channel
            method = pika.spec.Basic.Deliver(routing_key='heartbeat.manager')
            properties = pika.spec.BasicProperties()
            body = 'ping'

            self.assertRaises(eval(param_expected),
                              self.test_manager._process_ping,
                              blocking_channel,
                              method, properties, body)
        except Exception as e:
            self.fail("Test failed: {}".format(e))