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.
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.
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ć.
Zapisz intencję i nadaj jej stabilny identyfikator.
Nie każ użytkownikowi czekać na obcy system.
Retry jest normalnym stanem, nie wyjątkiem od projektu.
Metryka, log i receipt muszą pokazać jeden efekt.
Mapa do zapisania
Od żądania do skutku: jeden kontrakt, dwa transporty
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 →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.
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 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
| Potrzeba | Azure Service Bus | Amazon SQS | Decyzja projektowa |
|---|---|---|---|
| Podstawowe dostarczenie | PeekLock daje semantykę co najmniej raz | Standard Queue daje co najmniej raz | Konsument musi tolerować powtórzenie |
| Kolejność w grupie | Sessions z SessionId | FIFO z MessageGroupId | Grupuj po agregacie, nie po całym systemie |
| Duplikaty publikacji | Duplicate detection po MessageId | FIFO deduplication po deduplication ID | To warstwa pomocnicza, nie zamiennik inbox |
| Czas na wykonanie | Lock duration i odnawianie blokady | Visibility timeout i jego zmiana | Ustaw powyżej p95, monitoruj przekroczenia |
| Trucizna | Wbudowana dead-letter queue | Redrive policy do DLQ | Alert, owner, retry policy i procedura replay |
| Duże dane | Blob Storage + klucz w komunikacie | S3 + klucz w komunikacie | Claim-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.
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”.
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);
}
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.
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.
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.
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);
}
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.
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.
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
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
Wiek wiadomości
p50, p95 i najstarsza wiadomość. Sama długość kolejki bez tempa napływu jest myląca.
Retry rate
Próby według typu błędu, dependency i wersji kontraktu. Alertuj zmianę trendu, nie pojedynczy retry.
DLQ ingress
Liczba nowych pozycji, reason, owner i czas od pojawienia do decyzji. DLQ bez właściciela rośnie po cichu.
Lock lost / timeout
Pokazuje, że czas pracy i blokady są źle dopasowane albo konsument jest przeciążony.
Duplicate prevented
Licznik potwierdza, że idempotencja realnie chroni wynik, a nie tylko istnieje w dokumentacji.
Outbox lag
Wiek najstarszego nieopublikowanego rekordu i throughput dispatchera są podstawowym SLO integracji.
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.
Dla regulara
- Wdróż outbox i inbox z unikalnym indeksem.
- Dobierz lock albo visibility timeout do p95.
- Zbuduj retry z backoff, jitter i kontrolowaną DLQ.
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?
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:
- Microsoft Learn: What's new in C# 14
- Microsoft Learn: What's new in .NET 10
- Azure Service Bus: message transfers, locks and duplicates
- Azure Service Bus: duplicate detection
- Azure Service Bus: message sessions
- AWS SDK for .NET V4: Amazon SQS APIs
- AWS Message Processing Framework for .NET
- Amazon SQS: at-least-once delivery
- Amazon SQS: visibility timeout
- Amazon SQS: dead-letter queues