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), ])