Exemple #1
0
    def new_service():
        logger.info("Service {service_name} received request",
                    extra={'service_name': service_name})
        if request.data:
            msg = pickle.loads(request.data)
        else:
            msg = communication.Message()

        logger.debug("Unpickled message is: {microservice_message}",
                     extra={'microservice_message': msg})

        if settings.communication_mode == settings.CommunicationMode.SYN:
            # Use flasks thread-safe globals for access to the current message.
            settings.set_current_message(msg)
            result = carry_out_local_service(msg)
            return_message = communication.Message(
                results={settings.ServiceWaypost.local_service: result})
            logger.debug("Return message is: {microservice_message}",
                         extra={'microservice_message': return_message})

            settings.set_current_message(None)
            return pickle.dumps(return_message)
        elif settings.communication_mode == settings.CommunicationMode.ACTOR:
            # Kick off the process to do the work and send the response.
            logger.debug("Submitting work to executor")

            # Unlike the synchronous mode, we don't set the current_message variable here because we're
            # going to handle it using thread locals in the async call.
            executor.submit(perform_service_async, msg)

            logger.debug("Asynchronous request has been scheduled.")
            # Ack the request.
            return pickle.dumps(True)
        raise ValueError("Invalid deployment mode: {}".format(
            settings.communication_mode))
    def test_construct_and_send_call_to_service(
            self, mock_send_object_to_service: MagicMock):
        target_service = "my-service-name"
        local_service = "local-service"

        with self.subTest(msg="Test basic message creation"):
            args = self.sample_msg_dict['args']  # type: tuple
            kwargs = self.sample_msg_dict['kwargs']  # type: dict
            communication.construct_and_send_call_to_service(
                target_service,
                communication.Message(results={'previous-service': (1, 5, 7)},
                                      request_id=123456),
                *args,
                **kwargs,
            )

            expected_result = communication.Message(
                **{
                    'args': self.sample_msg_dict['args'],
                    'kwargs': self.sample_msg_dict['kwargs'],
                    'via':
                    [communication.ViaHeader(local_service, tuple(), {})],
                    'results': {
                        'previous-service': (1, 5, 7)
                    },
                    'request_id': self.sample_msg_dict['request_id']
                })

            result_message = mock_send_object_to_service.call_args[0][1]
            mock_send_object_to_service.assert_called_once()
            self.assertEqual(mock_send_object_to_service.call_args[0][0],
                             target_service)
            self.assertEqual(result_message, expected_result)

        mock_send_object_to_service.reset_mock()
        with self.subTest(msg="Test multiple vias"):
            communication.construct_and_send_call_to_service(
                target_service,
                result_message,
                *args,
                **kwargs,
            )

            expected_result.via.append(
                communication.ViaHeader(local_service, args, kwargs))

            result_message2 = mock_send_object_to_service.call_args[0][1]

            mock_send_object_to_service.assert_called_once()
            self.assertEqual(mock_send_object_to_service.call_args[0][0],
                             target_service)
            self.assertEqual(result_message2, expected_result)
    def setUp(self):
        self.mocked_request_result = MockRequestResult()
        self.original_requests_get = requests.get
        self.mocked_requests_get = MagicMock(
            return_value=self.mocked_request_result)
        requests.get = self.mocked_requests_get

        self.sample_msg_dict = {
            'args': (1, 2, 3),
            'kwargs': {
                'a': 'asdf',
                'b': 123
            },
            'via': [
                ("service_name", (4, 5, 6), {
                    'gfd': 123,
                    'ert': 908
                }),
                ("service_name2", (2, 6, 7), {
                    'dfg': 123,
                    'wer': 908
                }),
            ],
            'results': {
                'service_name3': ('asdf', 345, 'yes')
            },
            'request_id':
            123456,
        }

        self.sample_message = communication.Message(**self.sample_msg_dict)
    def test_interface_response(self):
        """
        This test covers the handling of a response to a request made from an interface.
        It relies on the results being handled (and cached) separately to the request being made. It does:
            - Set up the local microservice handler in interface mode.
            - Send the response to this local handler (flask app)
            - Ensure that response is stored correctly
            - Make a request that would wait for that response, but finishes immediately because it's already available.

        The reason for doing this back to front (response, then request) is to avoid complicating the test with
        threading (as the request blocks until the response is received).
        """
        self.mock_setup('my_app', interface=True)

        args = (3, 4, 5)
        kwargs = {'erty': 5, 'asdf': 'asddfg'}
        expected_result = ("nonsense", 35)

        target_service_name = 'microservice.tests.microservices_for_testing.echo_as_dict'
        result_key = communication.create_result_key(target_service_name, args,
                                                     kwargs)

        # State that we're expecting a response for a request matching self.request_id
        settings.set_interface_request(self.request_id, result_key)

        # Construct the message that we'd expect to see back in response
        response_message = communication.Message(request_id=self.request_id, )
        response_message.add_result(target_service_name, args, kwargs,
                                    expected_result)

        # Send the response - the answer should get placed in the response storage.
        response = self.app.get('/',
                                data=response_message.pickle,
                                content_type='application/json')
        response_result = pickle.loads(response.data)

        self.assertEqual(response_result, True)

        # Wait for the thread pool to complete the work.
        time.sleep(self.THREAD_TIMER)

        # Check that the result has been logged
        self.assertIn(self.request_id, settings.interface_results.keys())
        self.assertEqual(expected_result,
                         settings.interface_results[self.request_id])

        # Make the request that this corresponds to, to check that the result is picked up correctly.
        result = microservices_for_testing.echo_as_dict(*args, **kwargs)

        self.assertEqual(result, expected_result)

        # Remove the hanging request id to tidy up for any other tests.
        # This is only required because we're doing this test in the reverse order (response, then request).
        del settings.interface_requests[self.request_id]
    def test_blank_request(self):
        local_service_name = 'microservice.tests.microservices_for_testing.echo_as_dict'
        self.mock_setup(local_service_name)

        response = self.app.get('/')
        result = pickle.loads(response.data)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(
            result,
            communication.Message(
                results={local_service_name: {
                    '_args': tuple()
                }}))
    def test_nested_request(self):
        service_name = 'microservice.tests.microservices_for_testing.echo_as_dict2'
        nested_service_name = 'microservice.tests.microservices_for_testing.echo_as_dict'
        self.mock_setup(service_name)

        response = self.app.get('/',
                                data=self.sample_message.pickle,
                                content_type='application/json')
        result = pickle.loads(response.data)

        expected_call = communication.Message(
            **{
                'via':
                [('microservice.tests.microservices_for_testing.echo_as_dict2',
                  (1, 2, 3), {
                      'a': 'asdf',
                      'b': 123
                  })],
                'args':
                microservices_for_testing.echo_as_dict2_args,
                'kwargs':
                microservices_for_testing.echo_as_dict2_kwargs,
                'request_id':
                123456,
            })
        expected_result = communication.Message(
            results={
                service_name: ({
                    '_args': self.sample_message.args,
                    **self.sample_message.kwargs
                }, MockRequestResult.args)
            })

        self.assertEqual(response.status_code, 200)
        self.assertEqual(expected_result, result)

        self.mocked_send_object_to_service.assert_has_calls(
            [call(nested_service_name, expected_call)])
    def test_request(self):
        """
        Test that:
         - A 200 response is received to making a request to a microservice
         - A separate request is made back to the calling service with the result.
        """
        local_service_name = 'microservice.tests.microservices_for_testing.echo_as_dict'
        self.mock_setup(local_service_name)

        orig_msg = communication.Message()
        orig_msg.add_result('other_service_name', (), {}, [1, 3, 5, 7])
        test_msg = communication.construct_message_add_via(
            orig_msg,
            *self.args,
            **self.kwargs,
        )

        response = self.app.get('/',
                                data=test_msg.pickle,
                                content_type='application/json')
        result = pickle.loads(response.data)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(result, True)
        # Wait for the thread pool to complete the work.
        time.sleep(self.THREAD_TIMER)

        expected_message = communication.Message()
        expected_message.add_result('other_service_name', (), {}, [1, 3, 5, 7])
        expected_message.add_result(local_service_name, self.args, self.kwargs,
                                    {
                                        '_args': tuple(self.args),
                                        **self.kwargs
                                    })
        self.mocked_send_object_to_service.assert_has_calls([
            call(local_service_name, expected_message),
        ])
    def test_Message(self):
        msg_dict = self.sample_msg_dict

        msg = communication.Message(**msg_dict)

        self.assertEqual(msg.via[0],
                         communication.ViaHeader(*msg_dict['via'][0]))

        with self.subTest(msg="Test to and from dict"):
            self.assertEqual(msg_dict, msg.to_dict)
            self.assertEqual(msg, communication.Message.from_dict(msg_dict))

        with self.subTest(msg="Test pickle and unpickle"):
            pickled = msg.pickle
            unpickled = communication.Message.unpickle(pickled)
            self.assertEqual(msg, unpickled)
    def test_request_with_originating_args(self):
        """
        Test that:
         - The call back to the originating microservice contains the args and kwargs that that microservice was
            originally called with
        """
        local_service_name = 'microservice.tests.microservices_for_testing.echo_as_dict'
        self.mock_setup(local_service_name)

        previous_service_args = [1, 2, 6]
        previous_service_kwargs = {3: 6, 'asdf': 'wryt'}

        test_msg = communication.construct_message_add_via(
            communication.Message(
                args=previous_service_args,
                kwargs=previous_service_kwargs,
            ),
            *self.args,
            **self.kwargs,
        )

        response = self.app.get('/',
                                data=test_msg.pickle,
                                content_type='application/json')
        result = pickle.loads(response.data)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(result, True)
        # Wait for the thread pool to complete the work.
        time.sleep(self.THREAD_TIMER)

        expected_message = communication.Message.from_dict({
            'args':
            previous_service_args,
            'kwargs':
            previous_service_kwargs,
        })
        expected_message.add_result(local_service_name, self.args, self.kwargs,
                                    {
                                        '_args': tuple(self.args),
                                        **self.kwargs
                                    })

        self.mocked_send_object_to_service.assert_has_calls([
            call(local_service_name, expected_message),
        ])
    def test_request(self):
        local_service_name = 'microservice.tests.microservices_for_testing.echo_as_dict'
        self.mock_setup(local_service_name)

        response = self.app.get('/',
                                data=self.sample_message.pickle,
                                content_type='application/json')
        result = pickle.loads(response.data)

        expected_result = communication.Message(results={
            local_service_name: {
                '_args': (1, 2, 3),
                'a': 'asdf',
                'b': 123
            }
        })

        self.assertEqual(response.status_code, 200)
        self.assertEqual(result, expected_result)
    def test_request_resulting_in_exception(self):
        """
        Test that:
         - A 200 response is received to making a request to a microservice
         - A separate request is made back to the calling service with the result, which is an exception
        """
        local_service_name = 'microservice.tests.microservices_for_testing.exception_raiser'
        self.mock_setup(local_service_name)

        test_msg = communication.construct_message_add_via(
            communication.Message(),
            *self.args,
            **self.kwargs,
        )

        response = self.app.get('/',
                                data=test_msg.pickle,
                                content_type='application/json')
        result = pickle.loads(response.data)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(result, True)
        # Wait for the thread pool to complete the work.
        time.sleep(self.THREAD_TIMER)

        # Python in-built comparison for exceptions doesn't work for different instances, so
        # have to compare the arguments directly.
        self.mocked_send_object_to_service.assert_called_once()

        # Check the service name is as expected
        self.assertEqual(
            local_service_name,
            self.mocked_send_object_to_service.mock_calls[0][1][0])

        # Check that the details of the exception are as expected
        expected = RuntimeError("Called with: {}; {}".format(
            self.args, self.kwargs))
        result_message = self.mocked_send_object_to_service.mock_calls[0][1][1]
        actual = result_message.get_result(local_service_name, self.args,
                                           self.kwargs)
        self.assertEqual(type(expected), type(actual))
        self.assertEqual(expected.args, actual.args)
    def test_interface_response_resulting_in_exception(self):
        """
        This test is identical to the test `test_interface_response`, but the response is an exception, so we expect
        to see it raised instead of simply returned.
        """
        self.mock_setup('my_app', interface=True)

        args = (3, 4, 5)
        kwargs = {'erty': 5, 'asdf': 'asddfg'}
        expected_result = RuntimeError("Sample error that should get raised.")

        target_service_name = 'microservice.tests.microservices_for_testing.echo_as_dict'
        result_key = communication.create_result_key(target_service_name, args,
                                                     kwargs)

        # State that we're expecting a response for a request matching self.request_id
        settings.set_interface_request(self.request_id, result_key)

        # Construct the message that we'd expect to see back in response
        response_message = communication.Message(request_id=self.request_id, )
        response_message.add_result(target_service_name, args, kwargs,
                                    expected_result)

        # Send the response - the answer should get placed in the response storage.
        response = self.app.get('/',
                                data=response_message.pickle,
                                content_type='application/json')
        response_result = pickle.loads(response.data)

        self.assertEqual(response_result, True)

        # Wait for the thread pool to complete the work.
        time.sleep(self.THREAD_TIMER)

        # Make the request that this corresponds to, to check that the result is picked up correctly.
        with self.assertRaises(type(expected_result)):
            microservices_for_testing.echo_as_dict(*args, **kwargs)

        # Remove the hanging request id to tidy up for any other tests.
        # This is only required because we're doing this test in the reverse order (response, then request).
        del settings.interface_requests[self.request_id]
    def test_nested_request(self):
        nested_service_name = "microservice.tests.microservices_for_testing.echo_as_dict"
        local_service_name = 'microservice.tests.microservices_for_testing.echo_as_dict2'
        self.mock_setup(local_service_name)

        previous_service_args = (1, 2, 6)
        previous_service_kwargs = {3: 6, 'asdf': 'wryt'}

        test_msg = communication.construct_message_add_via(
            communication.Message(
                args=previous_service_args,
                kwargs=previous_service_kwargs,
            ),
            *self.args,
            **self.kwargs,
        )
        response = self.app.get('/',
                                data=test_msg.pickle,
                                content_type='application/json')
        result = pickle.loads(response.data)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(result, True)
        # Wait for the thread pool to complete the work.
        time.sleep(self.THREAD_TIMER)

        expected_message = communication.Message.from_dict({
            'args': (5, 2, 5),
            'kwargs': {
                'asdf': 'asdrf'
            },
            'via':
            [(local_service_name, previous_service_args,
              previous_service_kwargs),
             ('microservice.tests.microservices_for_testing.echo_as_dict2',
              self.args, self.kwargs)],
        })

        self.mocked_send_object_to_service.assert_has_calls([
            call(nested_service_name, expected_message),
        ])
    def test_nested_call_is_not_made_if_already_calculated(self):
        """
        The nested service result should be stored in the `results` dict of the call back to the original
        actor, and that should be used to save calling into the nested service again.
        """
        local_service_name = 'microservice.tests.microservices_for_testing.echo_as_dict2'
        self.mock_setup(local_service_name)

        previous_service_args = [1, 2, 6]
        previous_service_kwargs = {3: 6, 'asdf': 'wryt'}

        echo_as_dict_expected_result = {
            '_args': microservices_for_testing.echo_as_dict2_args,
            **microservices_for_testing.echo_as_dict2_kwargs
        }

        test_msg = communication.construct_message_add_via(
            communication.Message(
                args=previous_service_args,
                kwargs=previous_service_kwargs,
            ),
            *self.args,
            **self.kwargs,
        )
        test_msg.add_result(
            'microservice.tests.microservices_for_testing.echo_as_dict',
            microservices_for_testing.echo_as_dict2_args,
            microservices_for_testing.echo_as_dict2_kwargs,
            echo_as_dict_expected_result)

        response = self.app.get('/',
                                data=test_msg.pickle,
                                content_type='application/json')
        result = pickle.loads(response.data)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(result, True)
        # Wait for the thread pool to complete the work.
        time.sleep(self.THREAD_TIMER)

        expected_message = communication.Message.from_dict({
            'args':
            previous_service_args,
            'kwargs':
            previous_service_kwargs,
        })
        expected_message.add_result(
            'microservice.tests.microservices_for_testing.echo_as_dict',
            microservices_for_testing.echo_as_dict2_args,
            microservices_for_testing.echo_as_dict2_kwargs,
            echo_as_dict_expected_result)
        expected_message.add_result(
            'microservice.tests.microservices_for_testing.echo_as_dict2',
            self.args, self.kwargs, (
                {
                    '_args': self.args,
                    **self.kwargs
                },
                echo_as_dict_expected_result,
            ))
        self.mocked_send_object_to_service.assert_has_calls([
            call(local_service_name, expected_message),
        ])