Legionista kończy rejestrację, a serwis wysyła mu list powitalny. Serwer pocztowy akurat nie odpowiada - i rejestracja pada. Człowiek stoi przed bramą odrzucony, choć jego wpis do rejestru powiódł się bez zarzutu. Zawiodła rzecz uboczna, a pociągnęła za sobą rzecz główną.
Rzym rozwiązał to inaczej. Dowódca nie czeka, aż posłaniec wróci z Galii - zostawia list w skrzynce kurierskiej i wraca do swoich zajęć. Posłaniec zabierze go, gdy będzie mógł; jeśli koń okuleje, list poczeka. W systemach nazywamy tę skrzynkę kolejką wiadomości.
Zaleta jest jedna i o nią właśnie chodzi: komunikacja asynchroniczna. Producent - ten, kto wysyła - nie czeka na konsumenta, czyli tego, kto odbiera. Zostawia wiadomość i idzie dalej.
Konsekwencje są trzy. Rejestracja kończy się natychmiast, bo nie jest już związana z pocztą. Awaria konsumenta nie przewraca producenta - wiadomości czekają w kolejce, aż wstanie. A gdy listów przybywa, dokładasz kolejnych konsumentów do tej samej kolejki, nie ruszając producenta.
Aplikacja wysyłająca potrzebuje klienta połączonego z brokerem, czyli z serwerem obsługującym kolejki:
1ClientsModule.register([
2 {
3 name: 'LEGION_SERVICE',
4 transport: Transport.RMQ,
5 options: {
6 urls: ['amqp://localhost:5672'],
7 queue: 'legion_queue',
8 },
9 },
10]);Transport.RMQ wskazuje RabbitMQ - najczęściej używanego brokera. urls to adres serwera, a queue nazwa skrzynki, do której trafią listy. Nazwa LEGION_SERVICE posłuży do wstrzyknięcia klienta tam, gdzie będzie potrzebny.Mając klienta, wysyłamy wiadomość jedną z dwóch metod. Różnica między nimi jest zasadnicza:
1// emit - wyślij i zapomnij, nie czekamy na odpowiedź
2this.client.emit('legion.created', { id: 1, name: 'Legio X' });
3
4// send - wyślij i poczekaj na odpowiedź
5const result = await this.client.send('legion.count', {}).toPromise();
to fire-and-forget - zostawiasz list i idziesz dalej, nie oczekując odpowiedzi. Tak ogłaszasz fakty: legion powstał, płatność przeszła, użytkownik się zarejestrował. Nadawcy nie interesuje, co odbiorca z tym zrobi.emit()
czeka na odpowiedź - to pytanie, nie ogłoszenie. Używasz go, gdy potrzebujesz wyniku: ile jest legionów, czy ten identyfikator istnieje.send()
Wybór między nimi decyduje o tym, czy w ogóle korzystasz z zalet kolejki.
send() przywraca czekanie, a więc i sprzężenie, od którego uciekaliśmy - konsument znów musi działać, żeby producent ruszył dalej. Dlatego do powiadomień, e-maili i logów zawsze emit(), @name; send() zostaw dla przypadków, gdy naprawdę potrzebujesz odpowiedzi.Po drugiej stronie kolejki stoi serwis nasłuchujący:
1@Controller()
2export class LegionConsumer {
3 @EventPattern('legion.created')
4 async handleLegionCreated(@Payload() data: LegionCreatedDto) {
5 await this.mailService.sendWelcome(data);
6 }
7}@EventPattern('legion.created') mówi: ta metoda obsługuje wiadomości o takiej nazwie. Nazwa jest umową między producentem a konsumentem - musi zgadzać się co do znaku z tą podaną w emit(). @Payload() wyciąga treść wiadomości, czyli obiekt przekazany jako drugi argument.Zauważ, że konsument wygląda jak zwykły kontroler - bo działa podobnie. Różnica polega na tym, skąd przychodzi żądanie: nie z sieci HTTP, tylko z kolejki.
Broker musi wiedzieć, czy list dotarł i został przetworzony. Bez tej informacji nie może go usunąć - a gdyby usunął od razu po wysłaniu, awaria konsumenta w połowie pracy oznaczałaby bezpowrotną stratę wiadomości.
Stąd potwierdzenie, czyli acknowledgement:
1@EventPattern('legion.created')
2async handleLegionCreated(@Payload() data: LegionCreatedDto, @Ctx() context: RmqContext) {
3 const channel = context.getChannelRef();
4 const originalMsg = context.getMessage();
5
6 try {
7 await this.mailService.sendWelcome(data);
8
9 channel.ack(originalMsg);
10 } catch (error) {
11 channel.nack(originalMsg, false, true);
12 }
13}@Ctx() daje dostęp do kontekstu RabbitMQ. getChannelRef() zwraca kanał komunikacji z brokerem, a getMessage() - surową wiadomość, której dotyczy potwierdzenie.Sedno jest w bloku
try. channel.ack(originalMsg) potwierdza poprawne przetworzenie - dopiero wtedy broker usuwa wiadomość z kolejki. Zwróć uwagę na miejsce tego wywołania: po wykonaniu pracy, nie przed. Potwierdzenie na początku metody znaczyłoby "odebrałem", a nie "przetworzyłem", i awaria wysyłki e-maila skończyłaby się cichą utratą listu.W
catch stoi nack - odmowa potwierdzenia. Ostatni argument true każe brokerowi wstawić wiadomość z powrotem do kolejki, żeby spróbować później. To jest właśnie ta odporność, dla której kolejki wprowadziliśmy: nieudana próba nie niszczy wiadomości.Skrzynka kurierska działa, listy dochodzą:
ClientsModule.register z Transport.RMQ, adresem brokera i nazwą kolejki,emit() to fire-and-forget - ogłaszasz fakt i nie czekasz na odpowiedź; send() czeka na odpowiedź, więc przywraca sprzężenie,emit(); send() tylko wtedy, gdy naprawdę potrzebujesz wyniku,@EventPattern('nazwa'), a treść wyciąga @Payload(); nazwa musi zgadzać się z tą z emit(),channel.ack(originalMsg) potwierdza przetworzenie i dopiero wtedy broker usuwa wiadomość z kolejki,nack z ostatnim argumentem true wraca wiadomość do kolejki do ponownej próby,emit → wiadomość czeka w kolejce → konsument odbiera przez @EventPattern → potwierdza przez channel.ack.To ostatnia lekcja tego modułu. Umiesz już sprawdzić każdą część osobno, ich współpracę, całą drogę żądania, zmierzyć pokrycie i wytrzymałość, a teraz także rozdzielić serwisy tak, by awaria jednego nie pociągała reszty. A na razie zapamiętaj: kolejka to skrzynka kurierska - dowódca zostawia list i wraca do swoich spraw, a potwierdzenie odbioru przychodzi dopiero wtedy, gdy posłaniec naprawdę dotarł.