Race condition to jeden z tych błędów, które potrafią przejść przez testy, a potem uderzyć w produkcji w najmniej wygodnym momencie: przy płatności, rezerwacji zasobu, aktualizacji koszyka albo zapisie danych. W tym artykule wyjaśniam, czym jest to zjawisko, jak je rozpoznać w aplikacjach współbieżnych i jak podejść do niego w procesach QA, żeby nie opierać się na szczęściu. Pokażę też, jak odróżnić je od deadlocka i data race, bo te pojęcia często się miesza.
Najkrócej rzecz biorąc, chodzi o wynik zależny od kolejności wykonania
- Race condition pojawia się wtedy, gdy poprawność programu zależy od tego, który wątek, proces lub żądanie wykona się pierwszy.
- Objawy bywają losowe: ten sam scenariusz może raz przejść, a innym razem zakończyć się błędem.
- Najczęściej problem dotyczy wspólnego stanu, na przykład licznika, koszyka, zapasu magazynowego albo rekordu w bazie.
- W QA nie wystarczy jeden test funkcjonalny. Potrzebne są powtórzenia, testy równoległe i obserwacja logów z dokładnym czasem zdarzeń.
- Ryzyko zmniejszają m.in. blokady, operacje atomowe, transakcje, idempotencja i ograniczenie współdzielonego stanu.
Race condition co to i dlaczego jest tak podstępna
W praktyce chodzi o sytuację, w której kolejność wykonania dwóch lub więcej operacji wpływa na końcowy wynik. Sama logika może wyglądać poprawnie, ale jeśli kilka ścieżek kodu odczytuje i zapisuje ten sam zasób równolegle, rezultat przestaje być przewidywalny. To właśnie dlatego taki błąd bywa trudny do złapania w zwykłym scenariuszu testowym.
Ja najczęściej rozpoznaję ten problem po tym, że aplikacja nie psuje się stale. Ona psuje się okazjonalnie, pod obciążeniem, przy wolniejszej sieci, po szybkim kliknięciu użytkownika albo wtedy, gdy uruchomimy kilka żądań naraz. I to jest największy kłopot z perspektywy QA: test może przejść 50 razy z rzędu, a 51. raz ujawniać błąd, którego wcześniej nie było widać.
Warto też od razu rozdzielić dwie rzeczy. Race condition nie musi oznaczać awarii, zawieszenia czy wyjątku. Czasem objawia się ciszej: podwójnym zapisem, nadpisaniem danych, złą rezerwacją zasobu albo znikającym komunikatem. Dlatego sama stabilność testu funkcjonalnego nie wystarcza, jeśli w tle działa współbieżność. To prowadzi prosto do pytania, skąd taki problem w ogóle się bierze.
Skąd bierze się problem z kolejnością wykonania
Najczęściej źródłem jest wspólny stan, czyli fragment danych używany przez więcej niż jedną ścieżkę wykonania. Może to być licznik, rekord w bazie, cache, sesja użytkownika, plik, kolejka wiadomości albo nawet element UI, jeśli aplikacja reaguje na szybkie akcje użytkownika. Gdy dwa procesy próbują wykonać operację typu odczyt, zmiana, zapis bez odpowiedniej synchronizacji, powstaje luka, w której decyzja podejmowana jest na nieaktualnych danych.
Klasyczny przykład jest prosty: stan magazynu wynosi 1. Dwa zamówienia przychodzą niemal jednocześnie. Oba żądania czytają tę samą wartość, oba uznają, że produkt jest dostępny, a potem oba zapisują wynik. W efekcie system sprzedaje jeden produkt dwa razy. Kod w każdym pojedynczym kroku wygląda rozsądnie, ale cały przebieg jest błędny, bo zabrakło ochrony sekcji krytycznej, czyli fragmentu, który powinien być wykonywany wyłącznie przez jedną ścieżkę naraz.
W aplikacjach webowych i mobilnych dochodzą jeszcze retry, timeouty, opóźnienia sieci i asynchroniczne callbacki. Użytkownik widzi tylko przycisk „Zapisz”, ale pod spodem mogą działać równolegle dwa lub trzy wywołania API. Jeśli logika nie jest odporna na powtórzenia, race condition pojawia się szybciej, niż zespół zdąży go zauważyć w środowisku testowym. Z takich scenariuszy najłatwiej przejść do obrazu realnej awarii, więc warto zobaczyć, jak to wygląda w praktyce.
Jak wygląda to w aplikacjach, które testuje QA
W procesach QA najwięcej problemów widzę tam, gdzie aplikacja obsługuje dużo równoległych zdarzeń i gdzie błąd kosztuje realne pieniądze albo dane. Poniżej zestawiam kilka typowych sytuacji, które dobrze pokazują charakter tego problemu.
| Sytuacja | Co może pójść źle | Dlaczego to ważne |
|---|---|---|
| Checkout w e-commerce | Dwa zamówienia rezerwują ten sam ostatni produkt | Pojawia się overselling i reklamacje klienta |
| Edycja rekordu w panelu administracyjnym | Jedna zmiana nadpisuje drugą bez ostrzeżenia | Dochodzi do utraty danych lub błędnej konfiguracji |
| System płatności | To samo żądanie zostaje przetworzone dwa razy | Ryzyko podwójnego obciążenia lub duplikatu transakcji |
| Kolejka zadań lub webhooki | Więcej niż jeden worker wykonuje identyczną pracę | Powstają duplikaty maili, powiadomień lub zapisów |
| Interfejs webowy lub mobilny | Szybkie kliknięcie wywołuje kilka akcji naraz | Użytkownik widzi podwójny zapis albo błędny stan UI |
Najważniejszy wniosek z tych przykładów jest taki, że race condition nie ogranicza się do kodu wielowątkowego w ścisłym sensie. Potrafi pojawić się także w systemach opartych o API, kolejki, cache i komunikację asynchroniczną. Właśnie dlatego zespół QA powinien patrzeć nie tylko na poprawność pojedynczego flow, ale też na to, co dzieje się przy nakładaniu się żądań. A to wymaga innego podejścia do testowania niż zwykły happy path.

Jak wykrywać go w procesach QA
Jeśli miałbym wskazać jedną rzecz, która najczęściej utrudnia wykrycie takiego błędu, powiedziałbym: jednorazowe uruchomienie testu. Race condition lubi losowość, więc potrzebuje wielu iteracji, zmiennych warunków i odrobiny chaosu kontrolowanego przez testerów. W praktyce najlepiej działają metody, które wymuszają konkurencję o zasoby albo zwiększają prawdopodobieństwo niekorzystnej kolejności zdarzeń.
- Testy powtarzalne - ten sam scenariusz uruchamiany setki albo tysiące razy, żeby złapać rzadki układ zdarzeń.
- Testy równoległe - kilka żądań, użytkowników lub workerów działających w tym samym czasie na tym samym zasobie.
- Wstrzykiwanie opóźnień - celowe spowolnienie fragmentu logiki, żeby sprawdzić, co się stanie, gdy jedna ścieżka „spóźni się” względem drugiej.
- Testy obciążeniowe i stress - sprawdzenie, czy problem pojawia się dopiero przy wyższej liczbie operacji.
- Logowanie z kontekstem - timestamp, identyfikator żądania, thread ID, correlation ID, status transakcji.
- Analiza ścieżek retry i timeout - bo właśnie tam race condition często wychodzi spod dywanu.
W praktyce bardzo pomaga mi też obserwacja sekwencji zdarzeń, a nie tylko ich końcowego wyniku. Dwa testy mogą zakończyć się tym samym komunikatem błędu, ale przyczyna będzie inna: raz chodzi o opóźniony zapis, innym razem o podwójne odczytanie stanu. Gdy zespół QA ma dobre logi i potrafi odtworzyć kolejność akcji, diagnoza robi się znacznie szybsza. To z kolei otwiera drogę do pytań o zabezpieczenia po stronie aplikacji.
Jak ograniczać ryzyko bez spowalniania zespołu
Nie każda aplikacja wymaga ciężkiej synchronizacji wszędzie. Ja zwykle zaczynam od pytania, gdzie naprawdę istnieje wspólny stan i czy można go uprościć. Dopiero potem dobiera się mechanizm ochrony. W przeciwnym razie łatwo przesadzić i wprowadzić zamki tam, gdzie wystarczyłaby idempotencja albo transakcja.
- Blokady i mutexy - dobre, gdy jeden fragment kodu ma wyłączne prawo do zmiany zasobu, ale trzeba uważać na spadek równoległości i ryzyko deadlocka.
- Operacje atomowe - przydatne w prostych licznikach i flagach, bo wykonują się jako jedna nieprzerywalna zmiana.
- Transakcje bazodanowe - chronią spójność danych, zwłaszcza gdy kilka pól lub rekordów musi zmienić się razem.
- Optimistic locking - sprawdza, czy dane nie zostały zmienione przez kogoś innego od czasu odczytu; dobrze działa tam, gdzie kolizje są rzadkie.
- Idempotency keys - pomagają bezpiecznie obsługiwać ponowione żądania, np. przy płatnościach i webhookach.
- Ograniczenie współdzielonego stanu - często najprostsze i najskuteczniejsze rozwiązanie, bo mniej współdzielenia oznacza mniej okazji do błędu.
To właśnie tutaj zespół QA i zespół developerski powinni mówić jednym językiem. Tester nie musi implementować synchronizacji, ale musi wiedzieć, który flow jest krytyczny, jakie zasoby są współdzielone i gdzie warto sprawdzić zachowanie pod równoległym obciążeniem. Bez tego testy będą poprawne formalnie, ale mało użyteczne diagnostycznie. Żeby uniknąć chaosu w rozmowie o błędach, dobrze jest jeszcze odróżnić race condition od kilku bliskich pojęć.
Czego nie mylić z race condition
W raportach błędów te pojęcia często wrzuca się do jednego worka, a to utrudnia naprawę. Różnica jest istotna, bo inne są objawy, a inne działania naprawcze.
| Zjawisko | Na czym polega | Jak zwykle się objawia | Co sprawdzić najpierw |
|---|---|---|---|
| Race condition | Wynik zależy od kolejności wykonania równoległych operacji | Losowe, trudne do odtworzenia błędy logiczne | Wspólny stan, synchronizację, kolejność zapisów |
| Deadlock | Dwie lub więcej ścieżek czekają na siebie wzajemnie | Aplikacja lub proces się blokuje | Zależności między blokadami i zasobami |
| Data race | Niechroniony dostęp do tej samej pamięci lub zmiennej | Niestabilne, czasem bardzo dziwne zachowanie w kodzie niskopoziomowym | Atomowość, pamięć współdzielona, brak odpowiednich locków |
| Flaky test | Test raz przechodzi, raz nie, ale nie zawsze przez błąd produktu | Losowe wyniki testów automatycznych | Stabilność środowiska, dane testowe, zależności zewnętrzne |
Ta różnica ma znaczenie, bo flakiness testu nie zawsze oznacza błąd aplikacji, a race condition nie zawsze kończy się zawieszeniem. Z mojego doświadczenia wynika, że najlepiej patrzeć na kontekst: co się dzieje z danymi, jak często błąd występuje, czy pojawia się przy równoległości i czy znika po zmianie timingów. To już wystarcza, żeby wyciągnąć praktyczne wnioski i przejść do działań, które realnie poprawiają jakość.
Co wdrożyć już dziś, jeśli chcesz łapać takie błędy wcześniej
Jeśli miałbym zamknąć temat w kilku konkretach dla zespołu QA, zacząłbym od prostego priorytetu: zidentyfikuj najbardziej wrażliwe ścieżki. Zwykle są to płatności, rezerwacje, zapisy profilu, kolejki zadań, operacje masowe i każdy flow, w którym kilka akcji może dotknąć tego samego zasobu.
- Dodaj do planu testów scenariusze równoległe, a nie tylko pojedyncze przejścia przez flow.
- Uruchamiaj krytyczne testy wielokrotnie i w różnych porach, także w pipeline nocnym.
- Wymagaj logów z dokładnym czasem, identyfikatorem żądania i informacją o stanie przed oraz po zmianie.
- Sprawdzaj zachowanie przy retry, timeoutach, podwójnym kliknięciu i odświeżeniu strony.
- Wspólnie z developerami ustal, które operacje muszą być idempotentne, a które powinny być chronione blokadą lub transakcją.
Race condition rzadko daje się złapać samym klasycznym testem funkcjonalnym. Najlepsze zespoły traktują współbieżność jako osobny obszar jakości, a nie poboczny detal. Dzięki temu błędy wychodzą wcześniej, kosztują mniej i nie trafiają do klienta jako „dziwne zachowanie, którego nie da się odtworzyć”.