Kolejki bez nadgodzin: C#, Azure Service Bus i AWS SQS w jednym odpornym przepływie

Interaktywny warsztat o C# 14, .NET 10, Azure Service Bus i Amazon SQS: outbox, idempotencja, Blob/S3 claim-check, DLQ, koszty awarii, animowana mapa i quiz z raportem e-mail.

Poniżej znajduje się pełna treść artykułu. Quizy i symulacje wymagają włączenia JavaScript.

LAB 01 · ODPORNY PRZEPŁYW C# 14 · .NET 10 · Azure Service Bus · Amazon SQS
Decyzje w artykule 0/4 Rozwiązuj decyzje w kolejnych rozdziałach.

Wyczyść wynik

Piątek, 16:47

Kolejka nie ma ukrywać awarii. Ma kupić zespołowi czas na bezpieczną reakcję

Klient zatwierdza zlecenie. API odpowiada poprawnie, ale zewnętrzny system rozliczeń zwalnia. Worker traci połączenie tuż po wykonaniu operacji, więc ten sam komunikat wraca drugi raz. Jeżeli projekt zakłada, że wiadomość pojawi się dokładnie raz, sobotni poranek zacznie się od podwójnego skutku i ręcznego sprzątania danych.

Ten materiał jest warsztatem, nie opisem fikcyjnego sukcesu klienta. Zbudujemy przepływ, który zakłada powtórzenia, opóźnienia i częściowe awarie. Użyjemy wspólnego portu w C#, strategii transportu, transactional outbox, idempotentnego konsumenta, retry, dead-letter queue oraz wzorca claim-check dla plików w Azure Blob Storage lub Amazon S3. Na końcu czeka dziesięciopytaniowy egzamin i indywidualny raport e-mail.

01

Model mentalny

Kolejka to kontrolowana poczekalnia, a nie magiczny przyspieszacz

W systemie synchronicznym użytkownik czeka, aż każdy zależny krok zakończy pracę. W systemie asynchronicznym API przyjmuje intencję, zapisuje własny stan i przekazuje wykonanie dalej. Dzięki temu chwilowa awaria integracji nie musi zatrzymać całej ścieżki. Koszt tej swobody jest konkretny: trzeba umieć rozpoznać duplikat, wznowić pracę, obserwować zaległości i świadomie rozstrzygać wiadomości, których automat nie potrafi już naprawić.

01Przyjmij

Zapisz intencję i nadaj jej stabilny identyfikator.

02Odczep

Nie każ użytkownikowi czekać na obcy system.

03Powtarzaj

Retry jest normalnym stanem, nie wyjątkiem od projektu.

04Udowodnij

Metryka, log i receipt muszą pokazać jeden efekt.

Mapa do zapisania

Od żądania do skutku: jeden kontrakt, dwa transporty

przepływ aktywny
01APIcommand + requestId
02DB + Outboxjeden COMMIT
03Dispatcherpublikacja po zapisie
AAzure Service BusPeekLock · Sessions · DLQ
BAmazon SQSVisibility · FIFO · DLQ
04Consumer + Inboxidempotencja
05Efektreceipt + telemetria
Retrybackoff + jitterDLQ
Kod QR prowadzący do mobilnej mapy przepływu kolejkowego
OTWÓRZ NA TELEFONIE

Zeskanuj mapę i zachowaj ją jako checklistę do review

Kod prowadzi bezpośrednio do tej animowanej mapy. Możesz otworzyć ją podczas projektowania workera albo przeglądu incydentu, bez szukania właściwego rozdziału.

Otwórz mapę bez skanowania →
Decyzja 1/4

API zapisało zlecenie, ale broker jest chwilowo niedostępny. Co robisz?

Czekam i ponawiam publikację w żądaniu HTTP.

Zapisuję stan i outbox w jednej transakcji; dispatcher wznowi wysyłkę.

Wybierz rozwiązanie, które nie zgubi przyjętego zlecenia.

Stan technologii w chwili publikacji

Nowe C# nie naprawi złej granicy odpowiedzialności, ale może uprościć jej zapis

C# 14 jest dostarczany z .NET 10, który ma status LTS. Extension members, null-conditional assignment i dalsze ulepszenia pracy ze spanami poprawiają ergonomię, lecz niezawodność nadal wynika z kontraktu i danych. W kodzie integracyjnym ważniejsze od syntaktycznego skrótu jest to, czy komunikat ma identyfikator, wersję, korelację, datę wystąpienia i minimalny zestaw danych potrzebnych konsumentowi.

Ważna migracja Azure

Microsoft zapowiedział zakończenie wsparcia starszych bibliotek WindowsAzure.ServiceBus i Microsoft.Azure.ServiceBus 30 września 2026 r. Nowy kod powinien korzystać z Azure.Messaging.ServiceBus, a istniejące systemy powinny mieć zaplanowaną migrację i test odbiorczy.

AWS dla .NET

AWS SDK for .NET V4 udostępnia bezpośrednie API SQS, a AWS Message Processing Framework for .NET może uporządkować publikowanie i obsługę komunikatów. Framework nie zwalnia jednak z idempotencji, poprawnego visibility timeout i kontroli DLQ.

Dobór transportu

Azure Service Bus i Amazon SQS rozwiązują podobny problem innym zestawem narzędzi

PotrzebaAzure Service BusAmazon SQSDecyzja projektowa
Podstawowe dostarczeniePeekLock daje semantykę co najmniej razStandard Queue daje co najmniej razKonsument musi tolerować powtórzenie
Kolejność w grupieSessions z SessionIdFIFO z MessageGroupIdGrupuj po agregacie, nie po całym systemie
Duplikaty publikacjiDuplicate detection po MessageIdFIFO deduplication po deduplication IDTo warstwa pomocnicza, nie zamiennik inbox
Czas na wykonanieLock duration i odnawianie blokadyVisibility timeout i jego zmianaUstaw powyżej p95, monitoruj przekroczenia
TruciznaWbudowana dead-letter queueRedrive policy do DLQAlert, owner, retry policy i procedura replay
Duże daneBlob Storage + klucz w komunikacieS3 + klucz w komunikacieClaim-check, checksum i ograniczony czas dostępu

„Exactly once” nie powinno pojawiać się w projekcie jako życzenie. Bezpieczny cel to: wiadomość może dotrzeć ponownie, ale obserwowalny skutek biznesowy powstaje raz.

02

Kontrakt C#

Najpierw koperta wiadomości, później wybór chmury

Model domenowy nie powinien importować typów z SDK Azure ani AWS. Najpierw definiujemy własny, mały kontrakt. Adapter transportu tłumaczy go na właściwości brokera.

public sealed record WorkEnvelope<T>(
    Guid MessageId,
    Guid CorrelationId,
    string TenantId,
    int SchemaVersion,
    DateTimeOffset OccurredAt,
    T Payload);

public interface IWorkQueue
{
    Task PublishAsync<T>(
        WorkEnvelope<T> message,
        QueueRoute route,
        CancellationToken cancellationToken);
}

MessageId chroni przed ponownym skutkiem, CorrelationId spina ślad między API i workerem, TenantId pozwala egzekwować izolację, a SchemaVersion daje miejsce na bezpieczną ewolucję. Do koperty nie wkładamy sekretów, pełnych dokumentów ani danych „na wszelki wypadek”.

STRATEGY + ADAPTER

Transport wybierany przez politykę

public interface IQueueStrategy
{
    bool Supports(CloudBroker broker);
    Task PublishAsync<T>(
        WorkEnvelope<T> message,
        QueueRoute route,
        CancellationToken ct);
}

public sealed class QueueRouter(
    IEnumerable<IQueueStrategy> strategies)
{
    public Task PublishAsync<T>(
        CloudBroker broker,
        WorkEnvelope<T> message,
        QueueRoute route,
        CancellationToken ct)
        => strategies.Single(x => x.Supports(broker))
            .PublishAsync(message, route, ct);
}
POLITYKA, NIE IF-OLOGIA

Kiedy router ma sens?

  • różne regiony lub tenanty wymagają innego transportu,
  • trwa migracja z jednej chmury do drugiej,
  • potrzebny jest kontrolowany failover,
  • testy mają używać transportu in-memory.

Jeżeli cały system zawsze używa jednego brokera, jeden adapter jest prostszy. Abstrakcja ma usuwać realną zmienność, nie tworzyć jej na zapas.

Decyzja 2/4

Komunikat ma przenieść plik PDF o rozmiarze 18 MB. Co umieszczasz w kolejce?

Cały plik jako Base64, żeby worker miał wszystko od razu.

Klucz obiektu, checksum i metadane; plik trafia do Blob Storage albo S3.

Wybierz rozwiązanie, które utrzyma mały i bezpieczny kontrakt.

Claim-check

Blob Storage albo S3 przechowuje ciężar. Kolejka przechowuje intencję

API zapisuje plik pod nieprzewidywalnym kluczem, wylicza SHA-256 i publikuje tylko referencję. Worker pobiera obiekt przy użyciu tożsamości zarządzanej albo roli IAM, weryfikuje hash i przetwarza strumieniowo. Dostęp publiczny jest wyłączony, a retencja i szyfrowanie wynikają z klasy danych.

uploadBlob / S3objectKey + SHA-256Queueworker
03

Atomowość

Outbox zamyka okno pomiędzy COMMIT a publikacją

Nie ma rozproszonej transakcji pomiędzy PostgreSQL i brokerem. Zamiast udawać, że jest, zapisujemy zmianę biznesową i rekord do publikacji w tej samej lokalnej transakcji.

await using var transaction =
    await db.Database.BeginTransactionAsync(ct);

var order = WorkOrder.Accept(command.RequestId, command.CustomerId);
db.WorkOrders.Add(order);

db.OutboxMessages.Add(OutboxMessage.Create(
    messageId: command.RequestId,
    type: "work-order.accepted.v1",
    payload: serializer.Serialize(new WorkOrderAccepted(order.Id))));

await db.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);

// Dispatcher odczyta OutboxMessages dopiero po COMMIT.

Dispatcher wybiera nieprzetworzone rekordy porcjami, publikuje je i zapisuje znacznik wykonania. Dwa dispatchery nie mogą przejąć tego samego rekordu bez kontroli współbieżności. W PostgreSQL można użyć blokady wiersza i SKIP LOCKED albo jawnego statusu leasingu. Zawsze potrzebna jest metryka wieku najstarszej pozycji outbox, bo sama liczba rekordów nie mówi, jak długo klient czeka.

04

Idempotentny odbiór

Inbox rozstrzyga duplikat przed wykonaniem nieodwracalnego skutku

Service Bus w PeekLock i SQS Standard mogą dostarczyć komunikat ponownie. Konsument najpierw sprawdza własny receipt, potem wykonuje efekt i dopiero po trwałym zapisie rozstrzyga wiadomość.

public async Task HandleAsync(
    WorkEnvelope<WorkOrderAccepted> message,
    CancellationToken ct)
{
    if (await db.InboxReceipts.AnyAsync(
        x => x.MessageId == message.MessageId, ct))
        return;

    await using var tx = await db.Database.BeginTransactionAsync(ct);

    await pricing.CreateQuoteAsync(
        message.Payload.WorkOrderId,
        idempotencyKey: message.MessageId.ToString("N"),
        ct);

    db.InboxReceipts.Add(new InboxReceipt(message.MessageId));
    await db.SaveChangesAsync(ct);
    await tx.CommitAsync(ct);
}
Decyzja 3/4

Worker wykonał operację, ale stracił połączenie przed Complete/Delete. Wiadomość wraca. Co ma zrobić?

Rozpoznać receipt po MessageId i zakończyć bez ponownego skutku.

Wykonać wszystko ponownie, bo broker ponowił wiadomość celowo.

Pomyśl, co jest dowodem zakończenia, gdy potwierdzenie brokera zaginęło.

Ekonomia awarii

Kolejka oszczędza pieniądze dopiero wtedy, gdy zmniejsza koszt ręcznej reakcji

Poniższy rachunek jest przykładem do podstawienia własnych danych, nie wynikiem case study ArcCloud Studio. Załóżmy cztery incydenty miesięcznie, trzy osoby zaangażowane średnio przez 2,5 godziny i pełny koszt godziny pracy równy 180 PLN.

4 incydenty×3 osoby×2,5 h×180 PLN= 5 400 PLN / mies.

Do tego dochodzi utracone skupienie, opóźnienie roadmapy i ryzyko dla klienta. Porównuj ten koszt z ceną utrzymania brokera, telemetryki i automatycznych testów. Najtańsza architektura to nie ta z najmniejszym rachunkiem chmurowym, lecz ta z najniższym całkowitym kosztem bezpiecznego wyniku.

Gra architektoniczna

Ułóż siedem kroków bezpiecznego przetwarzania

Klikaj kafelki w kolejności. Błędny wybór nie zabiera punktu; pokazuje, której odpowiedzialności jeszcze brakuje.

Postęp0/7

BrokerASB albo SQS przechowuje wiadomość

CommandAPI przyjmuje stabilny requestId

Efekt biznesowywykonanie z idempotency key

Dispatcherpublikuje zapisany outbox

SettlementComplete albo Delete po trwałym wyniku

DB + Outboxstan i komunikat w jednym COMMIT

Consumer + Inboxkontrola duplikatu i korelacji

Zacznij od intencji, która ma stabilny identyfikator.

Ułóż od nowa

Decyzja 4/4

W DLQ pojawiło się 37 wiadomości. Jaka jest pierwsza poprawna reakcja?

Natychmiast odesłać wszystkie wiadomości do głównej kolejki.

Sklasyfikować przyczynę, naprawić ją i wykonać kontrolowany replay małej partii.

DLQ jest kolejką operacyjną, nie koszem do opróżnienia jednym kliknięciem.

Symulator incydentu

Przepuść komunikat przez timeout, retry i duplikat

Kontrolowany przebieg sprawdza requestId, outbox, transport, inbox, efekt biznesowy, receipt oraz settlement.

Uruchom przebieg

Laboratorium gotowe. Uruchom kontrolowany przebieg.

Runbook produkcyjny

Co obserwować, zanim telefon zadzwoni po godzinach

01

Wiek wiadomości

p50, p95 i najstarsza wiadomość. Sama długość kolejki bez tempa napływu jest myląca.

02

Retry rate

Próby według typu błędu, dependency i wersji kontraktu. Alertuj zmianę trendu, nie pojedynczy retry.

03

DLQ ingress

Liczba nowych pozycji, reason, owner i czas od pojawienia do decyzji. DLQ bez właściciela rośnie po cichu.

04

Lock lost / timeout

Pokazuje, że czas pracy i blokady są źle dopasowane albo konsument jest przeciążony.

05

Duplicate prevented

Licznik potwierdza, że idempotencja realnie chroni wynik, a nie tylko istnieje w dokumentacji.

06

Outbox lag

Wiek najstarszego nieopublikowanego rekordu i throughput dispatchera są podstawowym SLO integracji.

START

Dla początkującego

  • Rozróżnij command, event i document message.
  • Naucz się, kiedy Complete/Delete jest bezpieczne.
  • Dodaj MessageId, correlationId i jeden test duplikatu.
PRAKTYKA

Dla regulara

  • Wdróż outbox i inbox z unikalnym indeksem.
  • Dobierz lock albo visibility timeout do p95.
  • Zbuduj retry z backoff, jitter i kontrolowaną DLQ.
SENIOR

Dla seniora

  • Ustal SLO, budżet błędu i ownership kolejki.
  • Przetestuj ewolucję schematu oraz replay historycznych danych.
  • Policz koszt operacyjny i zaprojektuj bezpieczny rollback.

Checklist przed produkcją

Jeżeli choć jedna odpowiedź brzmi „nie wiem”, kolejka nie jest jeszcze gotowa

  • Czy każdy komunikat ma stabilny MessageId i correlationId?
  • Czy zapis stanu i outbox jest atomowy?
  • Czy duplikat daje ten sam obserwowalny wynik?
  • Czy duże payloady używają Blob Storage/S3 i claim-check?
  • Czy retry rozróżnia błąd przejściowy od trwałego?
  • Czy DLQ ma alert, właściciela i procedurę replay?
  • Czy lock/visibility timeout odpowiada realnemu p95?
  • Czy dashboard pokazuje wiek wiadomości, nie tylko ich liczbę?
  • Czy potrafimy zatrzymać konsumenta bez utraty przyjętych zleceń?
  • Czy test odtwarza timeout po skutku, ale przed settlement?
FINAŁ · 10 PYTAŃ

Sprawdź, czy projektujesz kolejkę, czy tylko przesuwasz awarię w czasie

Quiz poniżej obejmuje Azure Service Bus, Amazon SQS, outbox, inbox, claim-check, idempotencję, retry i telemetrię. Po udzieleniu wszystkich odpowiedzi potwierdzisz adres e-mail jednorazowym kodem. Wynik, poziom oraz kolejne kroki otrzymasz w dedykowanym raporcie. Adres nie jest publikowany w blogu.

Źródła techniczne

Materiał opiera się na dokumentacji producentów aktualnej w chwili publikacji: