Fail – Fast czy Fail – Proof code?

Jest trochę dobrych praktyk programistycznych, które wydają się przeczyć zdrowemu rozsądkowi. Jedną z nich jest zdecydowanie fail-fast. Jest to strategia obsługi błędów, w myśl której preferowane jest natychmiastowe zakończenie operacji, wycofanie ewentualnych zmian i zalogowanie problemu. Na pierwszy rzut oka nie ma to sensu. Dlaczego system miałby przestawać działać, jak tylko zauważy problem? Czy przypadkiem odporność na błędy nie jest pozytywną cechą?

Fail-fast vs fail-proof

Przeciwieństwem fail-fast jest tzw. fail-proof. System tworzony zgodnie z drugą strategią stara się za wszelką cenę dokończyć błędną operację. Wiele maszyn działa w ten sposób i jest to absolutnie koniecznie. Z pewnością nikt nie chciałby lecieć w samolocie, który przestaje działać, bo jeden z systemów zgłosił błąd : )

W przypadku systemów IT sytuacja jest zgoła inna. Priorytety są inne i przede wszystkim konsekwencje przerwania operacji nieporównywalne.

Przykład kodu napisanego w stylu fail-proof:

public String failProof(String input) {
    if (input == null) {
        return null;
    }
    // do some processing
    return result;
}

Jak widać w parametrze mamy String, który potrzebny jest do wykonania logiki. Metoda wymaga do pracy parametru, więc jego brak jest poważnym błędem. W podanym przykładzie metoda ignoruje ten fakt i zwraca brak wyniku.

Drugi przykład to metoda w stylu fail-fast:

public String failFast(String input) {
    if (input == null) {
        throw new NullPointerException("Input cannot be null.");
    }
    // do some processing
    return result;
}

W tym wypadku zachowanie jest totalnie inne. Metoda rozpoznaje nieoczekiwaną sytuację i wyrzuca błąd wykonania. Zwykle taki rodzaj wyjątku prowadzi do przerwania całej operacji, lub nawet zakończenie działania całej aplikacji.

Dlaczego fail-fast jest preferowaną strategią w systemach IT?

Po chwili zastanowienia możemy dojść do prostego wniosku, że rzeczywiście być może nie warto dokańczać błędnej operacji. Szczególnie ma to zastosowanie dla systemów backendowych. Aplikacje tego typu najczęściej wykonują operacje na danych i zapisują wyniki w bazie. W praktyce kończenie procesu “na siłę”, może mieć katastrofalne skutki.

Po pierwsze ryzykujemy, że operacja zapisze błędne dane w bazie. Jest to paskudna sytuacja, ponieważ dokłada drugi problem. Nie tylko musimy naprawić błąd w aplikacji, ale też stan bazy. W przypadku systemów działających produkcyjnie może być to tragiczne. W takim wypadku lepiej wycofać całą operację.

Po drugie szybkie przerywanie operacji zwiększa szanse na dostrzeżenie błędu, zanim trafi na produkcję. Każdy kod powinien przejść przez testy dewelopera piszącego kod, potem testy automatyczne i manualne. Jeśli dana funkcja brutalnie kończy operację, to ciężko taką sytuację przeoczyć.

Po trzecie fail-fast idealnie współgra z koncepcją transakcyjności. Transakcyjność na poziomie bazy danych gwarantuje nam możliwość wycofania wszystkich zmian w operacji. Zapewnia to bardzo duże bezpieczeństwo spójności danych, bo mamy pewność, że przerwana operacja nie pozostawi po sobie błędów w bazie.

Po czwarte pisanie kodu fail-proof promuje wiele innych szkodliwych praktyk programistycznych. Zachęca do “zjadania wyjątków” i tworzenia “pancernych” metod, które ukrywają błędy.

Czy należy używać fail-fast w każdym rodzaju aplikacji?

Są sytuację kiedy fail-proof może być lepszym rozwiązaniem. Czasami wyluzowanie zasad odnośnie obsługi błędów może przynieść więcej korzyści, lub jest po prostu konieczne. Tak, czy inaczej fundamentalną zasadą jest logowanie WSZYSTKICH wyjątków. To, że system kontynuuje pracę, nie oznacza, że wszystko jest w porządku i możemy “zjadać wyjątki”.

Jeśli system wykonuje krytycznie ważną funkcję. Lepiej wtedy wykonać 90% operacji niż próbować wycofać wszystko.

Jeśli aplikacja odpowiada tylko za frontend. W przypadku takich aplikacji lepiej nie zabijać całego interfejsu przez jeden błąd. Lepiej, żeby użytkownik nadal mógł używać wszystkich innych funkcji, które działają.

Szczególnym przykładem systemów, którym ciężko jest implementować fast-fail, są systemy, które nie używają transakcji bazodanowych. W takim wypadku nie jesteśmy w stanie zagwarantować spójności danych, więc równie dobrze tak, czy inaczej możemy próbować “dopchnąć” operacje do końca. Nie polecał bym tego, ale jestem w stanie wyobrazić sobie takie sytuacje.

Podsumowując…

Fail-fast jest jedną z najczęściej pomijanych zasad. Nie dość, że kłóci się z intuicją, ale też wymaga dyscypliny w kodowaniu. Znacznie prościej jest napisać “pancerną” funkcję, która nigdy nie będzie przyczyną zepsutych testów. Nie trzeba też zastanawiać się nad schematem obsługi błędów. Niestety takie implementację mszczą się w przyszłości.

Na koniec zacytuje anegdotę, którą usłyszałem dawno temu:

Wyobraź sobie, że jesteśmy światkiem ataku nożownika. Osoba dźgnięta nożem wsiada do autobusu i jedzie dziesięć przystanków. Po czym wysiada i dopiero wtedy dzwoni po pomoc. Co w takim wypadku może zrobić policja? Czy ukrywanie faktu ataku i jechanie w autobusie, tak jakby nic się nie stało było dobrym pomysłem?

Dokładnie to samo dzieje się w dżungli kodu fail-safe. Sytuacja błędna jest ukrywana przez wszystkie kolejne metody, tak jakby wszystko było w porządku. Taki błąd “wędruje” czasem bardzo daleko. Im dalej uda się go ukryć tym gorzej. Co gorsza błędna wartość może być przesłana do innych systemów, lub zapisana w bazie, co produkuje kolejne problemy.

Dlatego też moim zdaniem, pisanie i używanie funkcji fail-fast jest standardową procedurą. Powinno się od niej odchodzić, jeśli mamy na to dobre uzasadnienie.

Leave A Comment