def test_event_validate(create_bus_client_with_unhappy_schema): client: BusClient = create_bus_client_with_unhappy_schema() message = EventMessage(api_name="api", event_name="proc", kwargs={"p": 1}) with pytest.raises(ValidationError): validate_outgoing(config=client.config, schema=client.schema, message=message) jsonschema.validate.assert_called_with({"p": 1}, {"p": {}})
def test_validate_strict_missing_api(create_bus_client_with_unhappy_schema): client: BusClient = create_bus_client_with_unhappy_schema(strict_validation=True) # Using 'missing_api', which there is no schema for, so it # raise an error as strict_validation=True message = RpcMessage(api_name="missing_api", procedure_name="proc", kwargs={"p": 1}) with pytest.raises(UnknownApi): validate_outgoing(config=client.config, schema=client.schema, message=message)
def test_validate_non_strict(create_bus_client_with_unhappy_schema): client: BusClient = create_bus_client_with_unhappy_schema(strict_validation=False) # Using 'missing_api', which there is no schema for, so it # should validate just fine as strict_validation=False # (albeit with a warning) message = RpcMessage(api_name="missing_api", procedure_name="proc", kwargs={"p": 1}) validate_outgoing(config=client.config, schema=client.schema, message=message)
def test_result_validate(create_bus_client_with_unhappy_schema): client: BusClient = create_bus_client_with_unhappy_schema() message = ResultMessage( result="123", rpc_message_id="123", api_name="api", procedure_name="proc" ) with pytest.raises(ValidationError): validate_outgoing(config=client.config, schema=client.schema, message=message) jsonschema.validate.assert_called_with("123", {})
def test_validate_disabled(create_bus_client_with_unhappy_schema): client: BusClient = create_bus_client_with_unhappy_schema(validate=False) message = RpcMessage(api_name="api", procedure_name="proc", kwargs={"p": 1}) validate_outgoing(config=client.config, schema=client.schema, message=message)
def test_validate_outgoing_enabled(create_bus_client_with_unhappy_schema): client: BusClient = create_bus_client_with_unhappy_schema( validate={"outgoing": True}) message = RpcMessage(api_name="api", procedure_name="proc", kwargs={"p": 1}) with pytest.raises(ValidationError): validate_outgoing(config=client.config, schema=client.schema, message=message)
async def fire_event(self, api_name, name, kwargs: dict = None, options: dict = None): kwargs = kwargs or {} try: api = self.api_registry.get(api_name) except UnknownApi: raise UnknownApi( "Lightbus tried to fire the event {api_name}.{name}, but no API named {api_name} was found in the " "registry. An API being in the registry implies you are an authority on that API. Therefore, " "Lightbus requires the API to be in the registry as it is a bad idea to fire " "events on behalf of remote APIs. However, this could also be caused by a typo in the " "API name or event name, or be because the API class has not been " "registered using bus.client.register_api(). ".format(**locals()) ) validate_event_or_rpc_name(api_name, "event", name) try: event = api.get_event(name) except EventNotFound: raise EventNotFound( "Lightbus tried to fire the event {api_name}.{name}, but the API {api_name} does not " "seem to contain an event named {name}. You may need to define the event, you " "may also be using the incorrect API. Also check for typos.".format(**locals()) ) parameter_names = {p.name if isinstance(p, Parameter) else p for p in event.parameters} if set(kwargs.keys()) != parameter_names: raise InvalidEventArguments( "Invalid event arguments supplied when firing event. Attempted to fire event with " "{} arguments: {}. Event expected {}: {}".format( len(kwargs), sorted(kwargs.keys()), len(event.parameters), sorted(parameter_names), ) ) kwargs = deform_to_bus(kwargs) event_message = EventMessage( api_name=api.meta.name, event_name=name, kwargs=kwargs, version=api.meta.version ) validate_outgoing(self.config, self.schema, event_message) await self.hook_registry.execute("before_event_sent", event_message=event_message) logger.info(L("📤 Sending event {}.{}".format(Bold(api_name), Bold(name)))) await self.producer.send(SendEventCommand(message=event_message, options=options)).wait() await self.hook_registry.execute("after_event_sent", event_message=event_message)
async def handle_execute_rpc(self, command: commands.ExecuteRpcCommand): await self.schema.ensure_loaded_from_bus() validate_incoming(self.config, self.schema, command.message) await self.hook_registry.execute("before_rpc_execution", rpc_message=command.message) try: result = await self._call_rpc_local( api_name=command.message.api_name, name=command.message.procedure_name, kwargs=command.message.kwargs, ) except SuddenDeathException: # Used to simulate message failure for testing return except asyncio.CancelledError: raise except Exception as e: result = e else: result = deform_to_bus(result) result_message = ResultMessage( result=result, rpc_message_id=command.message.id, api_name=command.message.api_name, procedure_name=command.message.procedure_name, ) await self.hook_registry.execute( "after_rpc_execution", rpc_message=command.message, result_message=result_message ) if not result_message.error: validate_outgoing(self.config, self.schema, result_message) await self.producer.send( commands.SendResultCommand(message=result_message, rpc_message=command.message) ).wait()
async def call_rpc_remote( self, api_name: str, name: str, kwargs: dict = frozendict(), options: dict = frozendict() ): """ Perform an RPC call Call an RPC and return the result. """ kwargs = deform_to_bus(kwargs) rpc_message = RpcMessage(api_name=api_name, procedure_name=name, kwargs=kwargs) validate_event_or_rpc_name(api_name, "rpc", name) logger.info("📞 Calling remote RPC {}.{}".format(Bold(api_name), Bold(name))) start_time = time.time() validate_outgoing(self.config, self.schema, rpc_message) await self.hook_registry.execute("before_rpc_call", rpc_message=rpc_message) result_queue = InternalQueue() # Send the RPC await self.producer.send( commands.CallRpcCommand(message=rpc_message, options=options) ).wait() # Start a listener which will wait for results await self.producer.send( commands.ReceiveResultCommand( message=rpc_message, destination_queue=result_queue, options=options ) ).wait() # Wait for the result from the listener we started. # The RpcResultDock will handle timeouts result = await bail_on_error(self.error_queue, result_queue.get()) call_time = time.time() - start_time try: if isinstance(result, Exception): raise result except asyncio.TimeoutError: raise LightbusTimeout( f"Timeout when calling RPC {rpc_message.canonical_name} after waiting for {human_time(call_time)}. " f"It is possible no Lightbus process is serving this API, or perhaps it is taking " f"too long to process the request. In which case consider raising the 'rpc_timeout' " f"config option." ) from None else: assert isinstance(result, ResultMessage) result_message = result await self.hook_registry.execute( "after_rpc_call", rpc_message=rpc_message, result_message=result_message ) if not result_message.error: logger.info( L( "🏁 Remote call of {} completed in {}", Bold(rpc_message.canonical_name), human_time(call_time), ) ) else: logger.warning( L( "⚡ Error during remote call of RPC {}. Took {}: {}", Bold(rpc_message.canonical_name), human_time(call_time), result_message.result, ) ) raise LightbusWorkerError( "Error while calling {}: {}\nRemote stack trace:\n{}".format( rpc_message.canonical_name, result_message.result, result_message.trace ) ) validate_incoming(self.config, self.schema, result_message) return result_message.result