Jak obsługiwać duplikaty wiadomości? Idempotent consumers w Praktyce

Dowiedz się, jak skutecznie obsługiwać duplikaty wiadomości wykorzystując wzorzec idempotent consumers. Sprawdź to podejście, bo poprawi ono niezawodność Twoich systemów.

Problem

W systemach rozproszonych czy takich, gdzie komunikacja odbywa się za pomocą kolejek wiadomości (np. RabbitMQ), duplikaty to codzienność. Bez odpowiedniego podejścia mogą prowadzić do poważnych problemów, jak np. wielokrotne naliczenie płatności czy podwojenie zamówienia. Duplikaty wiadomości mogą powstać z różnych powodów, m.in.:

  • Problemy z siecią – retransmisje wiadomości przez nadawcę.
  • Błędy w systemie kolejek – np. wiadomość zostaje dostarczona ponownie po awarii.
  • Brak potwierdzenia przetworzenia – nadawca zakłada, że wiadomość nie dotarła.

Właśnie dlatego warto poznać koncepcję idempotentnych konsumentów (idempotent consumers) i za ich pomocą przetwarzać wiadomości w sposób bezpieczny i niezawodny.

Gwarancje dostarczania wiadomości

Zanim przejdziemy do szczegółów implementacji idempotent consumers, warto zrozumieć, jakie gwarancje dostarczania wiadomości oferują różne systemy kolejek i brokerów wiadomości. Są one zazwyczaj podzielone na trzy główne kategorie:

At most once

W tym przypadku konsument otrzymuje wiadomość co najwyżej raz. Może się jednak zdarzyć, że wiadomość w ogóle nie dotrze, na przykład z powodu awarii lub błędu w transmisji. Jest to najmniej niezawodna forma dostarczania i wymaga, by aplikacja tolerowała brak niektórych wiadomości.

At Least Once

Jest to najczęściej spotykana gwarancja w systemach rozproszonych. Wiadomość zostanie dostarczona co najmniej raz, ale może być dostarczona wielokrotnie. Powtarzające się wiadomości wynikają zazwyczaj z konieczności retransmisji w przypadku braku potwierdzenia od konsumenta, co omówimy dokładniej w dalszej części wpisu.

Exactly Once

Jest to najbardziej złożona gwarancja, która zakłada, że wiadomość zostanie dostarczona dokładnie raz. Niektóre systemy, takie jak logi zdarzeń czy wybrane brokery wiadomości, wspierają ten model, ale jego implementacja jest skomplikowana i zależy od pełnej kontroli nad producentem, brokerem i konsumentem.

Czym są idempotent consumers?

Idempotencja oznacza, że operacja wykonana wielokrotnie z tymi samymi danymi daje zawsze ten sam rezultat. W kontekście konsumentów wiadomości chodzi o zapewnienie, że niezależnie od liczby prób przetworzenia tej samej wiadomości, efekt końcowy będzie identyczny.

W kontekście komunikacji po HTTP, metody takie jak GET czy PUT są domyślnie idempotentne. Jednak w przypadku POST i PATCH by default mamy obsługę operacji, które zmieniają stan systemu, co jest fajnym przykładem (możemy to oczywiście obejść, np. wersjonowaniem).

Innym przykładem, bardziej produktowym, może być złożenie zamówienia: niezależnie od liczby przetworzeń tej samej wiadomości, zamówienie powinno zostać zapisane tylko raz.

Dlaczego idempotencja jest kluczowa?

Najczęściej spotykanym modelem dostarczania wiadomości jest At Least Once, co oznacza, że aplikacja musi być przygotowana na obsługę powtarzających się wiadomości. Nawet jeśli Twój broker wiadomości zapewnia dokładne dostarczanie, w praktyce zawsze istnieje ryzyko powtórzeń, wynikające np. z: retransmisji, awarii czy błędów.

W modelu At Least Once broker wiadomości nie uznaje wiadomości za przetworzoną, dopóki konsument nie wyśle potwierdzenia odbioru (tzw. acknowledgement). Jeśli potwierdzenie nie zostanie przesłane na czas – np. z powodu awarii konsumenta – broker spróbuje dostarczyć wiadomość ponownie.

Dzięki temu system nie traci wiadomości, ale wprowadza ryzyko ich powtórzeń, które należy obsłużyć. Właśnie w takich przypadkach idempotentni konsumenci odgrywają najbardziej kluczową rolę.

Jak zapewnić idempotent consumers?

No dobra, ale jak mam zapewnić, że moi konsumerzy będą idempotentni? Proces ten wymaga wprowadzenia mechanizmów, które umożliwią identyfikację i odpowiednie przetwarzanie wiadomości. Oto trzy proste kroki, które pozwolą Ci wdrożyć idempotencję w Twoim systemie.

Identyfikator wiadomości

Każda wiadomość powinna posiadać unikalny identyfikator. Dzięki temu konsument może sprawdzić, czy dana wiadomość była już przetwarzana poprzez sprawdzenie czy znajduje się ona w rejestrze przetworzonych wiadomości.

class Message {
  constructor(
    readonly id: string,
    readonly payload: any
  ) {}
}

class MessageProcessor {
  private processedMessages: Set<string> = new Set();

  process(message: Message): void {
    if (this.processedMessages.has(message.id)) {
      console.log(`Message ${message.id} already processed.`);
      return;
    }

    this.processMessagePayload(message.payload);
    this.processedMessages.add(message.id);
  }

  private processMessagePayload(payload: any): void {
    // logic...
    console.log(`Processing payload:`, payload);
  }
}

Trwałe przechowywanie statusu

O ile poprzednie rozwiązanie jest dobre, to w przypadku restartu aplikacji dane o przetworzonych wiadomościach ulecą z pamięci. Powinny być zatem przechowywane w trwałym magazynie, np. bazie danych lub cyklicznie zrzucane do pliku. Daje nam to pewność, że po ponownym uruchomieniu i otrzymaniu już przetworzonej przed restartem wiadomości nie ponowimy naszej logiki.

class PersistentMessageProcessor {
  constructor(private storage: PersistentStorage) {}

  process(message: Message): void {
    if (this.storage.hasMessageBeenProcessed(message.id)) {
      console.log(`Message ${message.id} already processed.`);
      return;
    }

    this.processMessagePayload(message.payload);
    this.storage.markMessageAsProcessed(message.id);
  }

  private processMessagePayload(payload: any): void {
    // logic...
    console.log(`Processing payload:`, payload);
  }
}

Blokada mutacji

Jeśli przetwarzanie wiadomości zmienia stan systemu, same operacje powinny być idempotentne. Na przykład, zamiast dodawać wartość do bazy, można użyć operacji „zapisz lub zaktualizuj”, lub po prostu sprawdzić, wykonując pewne zapytania, czy wiadomość powinna zostać przetworzona.

idempotent consumers

Podsumowanie

Obsługa zduplikowanych wiadomości to kluczowy element projektowania niezawodnych systemów. Dzięki idempotent consumers możemy zapewnić, że każda wiadomość zostanie przetworzona dokładnie raz, niezależnie od liczby jej kopii. Wdrażając identyfikatory wiadomości, trwałe przechowywanie statusu, idempotentne operacje oraz odpowiednio konfigurując brokera wiadomości znacząco zwiększysz stabilność swojego systemu.

Pamiętaj, że idempotencja to nie tylko technika – to filozofia projektowania systemów odpornych na błędy.

Autor wpisu

blog@orbisbit.com

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również