Events
Publish what happened instead of asking for an answer: the gateway emits a 'cat.adopted' event and answers 202 at once, and two handlers in the cats service react to it, each for its own purpose.
What you'll learn
- Tell request-response from events: a message waits for a reply, an event is published and nobody answers
- Handle events with @EventPattern, and register more than one handler for the same event
- Publish with ClientProxy.emit(), a hot Observable that sends whether or not anyone subscribes, unlike send()
- Answer 202 Accepted for work that happens after the response, and know what the caller cannot learn from an event
When someone adopts a cat, several things should happen: the cat's record gets its adopter, the notice board announces it, later a welcome email and a vet appointment. The gateway could send one message per task and wait for each reply, but it does not need any answer. It only needs to say what happened. The service, and any other service that cares, decides what to do about it.
That is the other half of microservice messaging. A message asks and waits for a reply. An event announces that something happened, and nobody replies. The caller moves on at once, and each listener reacts on its own time.
Handling an event
An event handler is a controller method marked with @EventPattern(). By convention its pattern is a string naming what happened:
@Controller()
export class InvoicesController {
@EventPattern('order.placed')
createInvoice(order: OrderPlacedEvent): void {
this.invoices.create(order.id, order.total);
}
}
The payload is the first argument, as with messages. The handler returns nothing, because there is nobody to return it to.
More than one handler may listen to the same event, in the same controller or in different ones, and every one of them receives it. A stock controller can reserve items on 'order.placed' while the invoices controller bills it, and neither knows about the other. This is the point of events: adding a reaction never touches the code that publishes.
Only handlers marked with @EventPattern share an event. A @MessagePattern for the same pattern takes the event for itself, and the event handlers never see it.
Publishing: emit()
The client publishes with emit(pattern, payload):
@Post()
@HttpCode(202)
place(@Body() order: PlaceOrderDto) {
this.orders.emit('order.placed', { id: order.id, total: order.total });
return { status: 'accepted' };
}
emit() returns an Observable too, but a hot one: the event goes out as soon as emit() is called, whether or not anyone subscribes. send() is cold, so a send() nobody subscribes to sends nothing at all. That makes send() used for fire-and-forget a silent bug: no error, no message.
202 Accepted is the honest status here. It says the request was taken and the work will happen after the response. 201 would claim something was created, and at the time of the response nothing has been.
What the caller cannot know
Because nothing comes back, the gateway learns nothing about what the service made of the event. It cannot tell whether the cat exists, whether a handler threw, or whether any handler listens at all. An event for an unknown cat is accepted exactly like a real one. A handler that throws is logged in the service (ERROR [RpcExceptionsHandler]) and the error ends there. An event whose pattern no handler matches is logged by the service as well:
ERROR [Server] There is no matching event handler defined in the remote service. Event pattern: cats.adopted
So an event handler must cope with any payload itself, here by ignoring an id it does not know. And a caller that needs to know the outcome should send a message and wait for the reply.
Your task
Lesson 1's app is here, grown a little. CatsService.adopt(id, by) records an adopter, and the service has a second controller, NoticesController, which already answers { cmd: 'notices' } with its list. The gateway serves GET /notices.
- In the service's
CatsController, handle the'cat.adopted'event (payload{ id, by }) by recording the adopter with the service. - In
NoticesController, handle'cat.adopted'as well. For a cat the service knows, add the notice<name> went home with <adopter>, for exampleTom went home with Ann. Ignore an unknown id. - In the gateway,
POST /cats/:id/adoptwith a body{ "by": "Ann" }publishes'cat.adopted'with the id as a number and the adopter, and answers202with{ "status": "accepted" }, without waiting for the service.
Adopt a cat that does not exist and look at the response: the gateway has no way to know.
When it fails
- The response is 202 but nothing changes: the pattern published and the pattern handled differ (
'cats.adopted'against'cat.adopted'). The service's console saysThere is no matching event handler defined in the remote service. The gateway's never will. - The response is 202, nothing changes, and nothing is logged anywhere: the gateway used
send()and never subscribed, so no message left. Publish withemit(). - The adopter is recorded but the notice board stays empty: one of the handlers has
@MessagePattern, which takes the event from the@EventPatternhandlers. 201instead of202: a POST answers 201 unless@HttpCode(202)says otherwise.
Remember
- A message waits for a reply; an event announces and moves on.
@EventPattern(pattern)handles an event, and every event handler for a pattern receives it.emit()is hot and sends at once;send()is cold and sends only when subscribed.- Nothing comes back from an event: not the result, not the error. Answer
202.
Stuck? Show a hint
Service: @EventPattern('cat.adopted') on a method in CatsController and on another in NoticesController; the payload is the first argument, as with messages. Gateway: @Post(':id/adopt') with @HttpCode(202), @Param('id', ParseIntPipe) and @Body('by'); this.catsService.emit('cat.adopted', { id, by }) needs no subscribe().