Fail-fast nebo Fail-tolerant?

Zdá se mi (soudě dle mne samotného), že heslo "fail-fast" bylo a je po léta základní mantrou všech (Java?) vývojářů. Tento přístup má pro programátora pří vývoji aplikace řadu nesporných výhod:

  • chyby jsou detekovány rychle a je levnější je opravit
  • příčina selhání je jasně viditelná a zdroj pádu většinou přestavuje zdroj vlastní chyby
  • chyby nejsou zanedbávány - každá musí být opravena aby systém fungoval

Díky těmto výhodám se tahle technika velmi oblíbenou a intuitivně ji nasazujeme a používáme všude. Stejně tak i všichni okolo nás - od autorů aplikačních serverů, webových frameworků, až po tvůrce jednoúčelových knihoven.

Odvrácenou stránkou této techniky je, že se jí těžko zbavuje ve chvílích, kdy o ni nestojíme. Nikdy si totiž nemůžeme být jisti odkud nám co vyskočí a v jakém stavu daná věc po vyvolání chyby zůstane. Na první pohled by se mohlo zdát, že toto bude pouze problém vývojářů fail-tolerant SW, ale já si myslím, že to je problém nás všech, který více-méně ignorujeme.

Jak jsem již psal v článku o rozlišování běhových prostředí, sami bychom měli chtít od SW odlišné chování v závislosti s jakým cílem aktuálně běží. Ve vývojovém a testovacím prostředí jednoznačně preferujeme fail-fast variantu, v produkci bude naopak naším cílem fail-tolerant chování. Nemusím jít daleko do historie, abych jako příklad dal totální selhání webu České Spořitelny, ze kterého po několik hodin lítaly pouze vyjímky WebLogic serveru. Otázka je, zda prvotní příčina byla natolik fatální, aby to vyřadilo 100% funkcionality webu. Co už otázkou není je výstup stacktrace přímo uživateli - v produkčním prostředí se zcela jednoznačně musí chovat aplikace tak, aby se k uživateli podobné věci nedostaly (např. nahradit chybu rozumnou hláškou nebo prázdným místem tam, kde měla být původní data - v řadě případů by si uživatel třeba ani chyby nevšimnul, pokud by po těchto datech přímo nešel).

Pokud vyvíjíme jednoúčelovou aplikaci, se kterou pracujeme jen my jako Java vývojáři a po jejím nasazení už jen koncový uživatel není tlak na fail-tolerant tak velký (i tak bych ale prvky fail-tolerant systému doporučoval), jako v případě aplikace, která je platformou pro další práci někoho jiného. Jako příklad bych uvedl třeba námi vyvíjené CMS, které je nástrojem pro webdevelopery pro tvorbu webů. Z mé profesní minulosti bych si ale vzpomněl i na Workflow engine, který byl přes XPDL editor plně konfigurovatelný power uživateli zákazníka (tj. ti mohli vytvářet procesní pravidla, podle kterých se workflow odehrávalo).

V těchto případech se mi osvědčilo adaptovat systém tak, aby absorboval určitou míru ne-fatálních chyb a dokázal s nimi fungovat dál. Také je vhodné rozdělit systém na relativně izolované části tak, aby chyba jedné části systému nezpůsobila zhroucení systému jako celku. Zní to velmi jednoduše, ale cesta je velmi trnitá - naprosto vše je totiž psáno v duchu fail-fast a dá poměrně hodně práce vnutit systému chování odlišné.

Namátkou uvedu pár příkladů:

  1. inicializace rozhranní Servlet specifikace (listenery, filtry, servlety):

    Pokud v inicializační fázi vylítne nekontrolovaná vyjímka selže inicializace kompletní webové aplikace. Pokud tedy chcete, aby aplikace ožila i v případě že není 100% funkční, musíte ošetřit naprosto všechny servlety, filtry a listenery (i ty z knihoven 3tích stran) tak, aby nebyla propuštěna jediná Exception. Proč byste něco takového mohli chtít? Například kvůli diagnostice - pokud systém alespoň nějak naběhne, jste schopni i bez přístupu na cílové prostředí analyzovat a případně opravit problém. Podobně je problém schopný lépe opravit i technik / webdeveloper, který nemá zkušenosti s Javou jako takovou, ale dokáže analyzovat stavové informace vaší aplikace.

  2. výchozí politika zpracování chyb v knihovnách 3-tích stran je obvykle "propusť je výš":

    Většina knihoven, se kterými jsem se setkal, vyhazují při chybách vyjímky výš. Chyba kdekoliv v JSP způsobí HTTP 500 (respektive, pokud na to myslíme, můžeme dodat vlastní ExceptionHandler, nicméně přijdeme o výstup celé původní JSP stránky). Ve Freemarkeru je exception handling defaultně nastaven na DEBUG, což znamená, že web aplikace do výstupu vypíše stacktrace, což na produkci není ideální chování (naštěstí tato politika lze ve Freemarkeru oproti JSP velmi jednoduše změnit). V JSON-lib pokud dojde k cyklickému zanoření objektů (na což nemusíte při vývoji dlouho přijít), končí problém opět vyjímkou, pokud chceme být fail-tolerant, musíme nastavit LENIENT nebo NOPROP strategii. Ve webových frameworcích typu MVC, obvykle libovolná chyba při zpracování requestu způsobí selhání zpracování requestu jako celku (a to i přesto, že pro většinu vykreslované stránky by data k vykreslení byla k dispozici).

  3. inicializace Springových kontextů:

    V případě libovolné chyby při inicializaci bean ve Spring kontextu je zastavena inicializace kontextu a nic se nevytvoří (respektive aplikační kontext není funkční). Opět se jedná o vše nebo nic. Přestože v případě aplikační logiky je tento stav poměrně pochopitelný, pokud si vezmeme v potaz 2 výsledné varianty produkční instalace: a) nefunguje nic b) více méně systém funguje, ale jeho konkrétní části (jednoznačně identifikovatelné) vykazují selhání. Mohli bychom v některých případech spíš hlasovat pro variantu b).

  4. iBatis:

    Ten nám taktéž naběhne buď úplně nebo vůbec. Naštěstí zde se téměř vždy jedná o chybu programátora a tudíž, mi tu není tohle chování tak trnem v oku jako v jiných případech. Neumím si ovšem představit použití iBatisu v rukou administrátora, který on-line edituje SQL dotazy v XML konfiguračních souborech. V takovém případě by bylo dané chování opět ke škodě - opět bych radši preferoval variantu, kdy je možné provádět všechny SQL dotazy, krom toho, který daný admin právě zprznil.

  5. a dalo by se pokračovat dál a dál ...

Tento článek nenabízí a ani nemůže nabízet konkrétní praktické rady - vše je odvislé od knihoven a technologií, které používáte. Osobně si můžu snad jen povzdychnout, že v některých případech, kdy je fail-fast přístup hluboce zakořeněn v konkrétní knihovně / technologii, není práce se změnou chování vůbec jednoduchá. Zajímaly by mne ovšem vaše názory a zkušenosti s touto problematikou. Uvažovali jste nad negativy fail-fast přístupu někdy? K jakým jste, vy, dospěli závěrům? Jak k tomu přistupují autoři jiných jazyků (.NET, Ruby, PHP ...)?