Când vorbim despre conceptul de "craftsmanship", ne putem referi la o anumită calitate a codului, una ridicată, de care așa-zișii meșteri care au scris acel cod să poată fi mândri și care să reflecte atât abilitățile avansate de care aceștia dau dovadă, cât și dorința lor de a dezvolta o aplicație cât mai bună posibil. De cele mai multe ori, pentru a se putea ajunge la un astfel de nivel al calității este nevoie de multă experiență și de un proces de dezvoltare amănunțit și detaliat.
Cu toate acestea, ca niște buni meșteri, știm că uneori ceva neprevăzut poate apărea, lucru care poate duce la unele probleme și, ulterior, la o scădere graduală sau chiar drastică a calității unui cod bun, făcându-l să scadă sub standardele pe care vrem să le avem. Acest lucru este, poate, mai volatil când vine vorba de aplicațiile mobile sau web. Ele folosesc, de obicei, unul sau mai multe API-uri care se integrează cu alte servicii sau componente auxiliare, atât externe, cât și interne, aspect care poate duce la apariția celor mai des întâlnite probleme așa-zis "neprevăzute", și anume, diversele erori de comunicare cu acele servicii.
Spre deosebire de defectele relativ generice care pot apărea în cod, acestea sunt mai dificil de abordat, întrucât în mare parte din cazuri nu le putem repara direct, fie pentru că nu se reproduc în condiții normale, fie pentru că problema nu este una sub controlul direct al echipei, caz întâlnit cel mai des la integrările cu servicii externe.
Dincolo de aceste aspecte, faptul că sunt mai dificil de tratat nu face acest lucru imposibil, ci doar duce la nevoia unei noi abordări, una mai concentrată pe negocierea acestor probleme, astfel încât ele să dispară sau să nu fie simțite de către utilizatori.
Acesta este conceptul pe care ne place să îl numim "Reziliența Aplicației", unul bazat atât pe posibila tratare a erorilor înainte să apară, cât și pe grațioasa gestionare a unor erori care, în pofida tuturor asigurărilor, își fac apariția.
Fiind cea mai comună dintre cele două, încercarea de gestionare a erorilor este așa-numitul minim de reziliență pe care o zonă de cod, dependentă de servicii externe, ar trebui să o aibă, aceasta servind rolul unui plan secundar care ar trebui să se întâmple în situația în care dependența externă nu se comportă cum este de așteptat.
Retry - Când vine vorba despre includerea acesteia în arhitectura aplicației, un pas de început este, de cele mai multe ori, implementarea unui mecanism de Retry, mecanism care facilitează reîncercarea unui request eșuat de mai multe ori. Această abordare este recomandată ca un prim mecanism de gestionare a erorilor de comunicare cu servicii externe, deoarece, în multe cazuri, problemele care cauzează eșecul apelului către dependență sunt relativ mici, fiind spontane și care dispar sau par să se rezolve de la sine la încercări ulterioare. Deși această metodă este una ușor de implementat și de folosit, ea nu ar trebui să fie foarte des aplicată (întrucât poate solicita mult sistemul). Mai ales că e sugerată utilizarea ei doar pentru zonele care "ating" dependențe externe sau care apelează alte servicii. Totodată, pentru a mări șansa succesului la încercările subsecvente, este recomandată și adăugarea unei perioade de așteptare, un așa-zis timp mort care dictează viteza cu care sunt repetate acțiunile, acordându-i, astfel, sistemului mai mult timp să-și revină.
Circuit Breaker - Desigur că, de la un punct, numărul repetat de încercări începe să ofere un randament descrescător, făcând repetatele încercări redundante. Pentru acest motiv, conceptul de Circuit Breaker este unul foarte important, făcând referire la un mod de a închide bucla de încercări atât pentru o perioadă predefinită, cât și definitiv. Pe lângă avantajul ușor de observat de a pune capăt așa-numitului circuit, principalul lui beneficiu este de a oferi posibilitatea de suspendare a execuției cât timp este rulat un alt segment de cod de rezervă. Spre exemplu, după câteva execuții eșuate, se poate suspenda circuitul pentru a se trimite un apel de repornire a serviciului, reluând încercarea de execuție o dată ce serviciul a răspuns unui ping cu succes.
Failover - Totuși, nimic nu surprinde existența unui plan B mai bine decât un sistem (sau mai multe) de rezervă, folosit atunci când cel principal eșuează din diverse motive. În cazul existenței unui asemenea sistem, încercările ulterioare pot să fie redirecționate spre el (mai ales într-o suspendare rapidă a circuitului), astfel oferind o șansă mai mare de succes. Pentru a o crește și mai tare, în cazul existenței mai multor astfel de sisteme de rezervă, este recomandată încercarea de folosire a mai multora în mod paralel, crescând exponențial numărul de încercări odată cu numărul de eșecuri. De precizat că, dacă există, astfel de servicii nu sunt mereu la același nivel de performanță cu cel al serviciului principal. În acest fel, apar probleme de altă natură, dar nu eșec total al sistemului.
Graceful Degradation - Uneori, toate încercările de gestionare a unei erori nu sunt suficiente. În aceste situații, este de preferat ca sistemul să eșueze într-un mod mai "grațios", astfel încât utilizatorii să resimtă acest lucru cât mai puțin. Această practică nu este atât de des întâlnită, iar atunci când este folosită, este, de obicei, limitată la utilizarea încercărilor de obținere a unor date de la un API sau de la un serviciu extern. În astfel de situații, este recomandată mascarea erorii prin returnarea unor date care țin locul celor reale unde este posibil, sau prin folosirea unor date anterioare care au fost salvate într-o memorie de tip cache a aplicației, acolo unde riscul unei astfel de schimbări este minim și probabil nu ar avea efecte asupra experienței utilizatorului.
Spre deosebire de abordarea anterioară, aceasta este mult mai rar întâlnită, întrucât necesită un nivel de planificare mult mai mare, care trebuie să includă posibilitatea prevederii tuturor sau măcar a majorității erorilor care pot apărea, indiferent de cât de puțin probabilă este apariția lor. De înțeles că acest lucru este relativ greu de atins, motiv pentru care vom propune câteva metode generale de prevenire a erorilor sau a altor lucruri care nu pot să meargă conform așteptărilor.
Timeout - De multe ori, acesta unealtă, timeout, este utilizată de majoritatea celor care o folosesc fără măcar să fie văzută ca un mecanism de prevenție a erorilor, fiind inclusă pur și simplu în integrările cu alte servicii. Acest lucru, dacă o face să fie cea mai întâlnită, îi limitează sever potențialul, nerealizându-se utilizarea ei completă. În situația folosirii sale complete, mecanismul de timeout poate să ofere un mod de opri intenționat unele apeluri sau execuții pentru a trece direct la una din modalitățile de gestionare a erorii, condiție necesară pentru o experiență rapidă a utilizatorului. După o anumită durată, succesul e tot mai puțin probabil, motiv pentru care timeout-ul ne lasă să ne degradăm grațios și controlat mai rapid.
Bulkhead Isolation - Întâlnit de multe ori și în alte industrii, când vine vorba despre arhitectura unui API, conceptul de izolare a diferitelor componente nu este mereu pe primul plan, fiind uneori omis în întregime. În cazul nostru, acest concept nu se referă doar la separarea la un nivel arhitectural, ci și la o separare a serviciilor printr-o limitare de resurse de care ele pot dispune în cazul unei erori. Astfel, dacă o parte din sistemul nostru întâlnește o eroare, acea eroare va rămâne în "carantină" în zona sa, consumând un maxim alocat de resurse care nu ar trebui să se repercuteze asupra funcționării fluide a celorlalte componente.
Cache - Uneori, poate cel mai bun mod de a preveni apariția unei noi erori la folosirea unui serviciu extern este chiar evitarea folosirii serviciului. Pentru a reuși acest lucru, o modalitate de caching poate păstra date puțin probabil să se schimbe la fiecare apel către un serviciu extern, care nu ar mai trebui luate de fiecare dată, scăzând astfel riscul de eșec. Chiar dacă este extrem de util, acest tip de mecanism ar trebui să fie folosit doar atunci când există certitudinea că utilizarea unor date "vechi" nu reprezintă un impact major pentru utilizator. Totuși, în situațiile în care poate să fie folosit, cachingul scade riscul de eșec până la 0, făcându-l cel mai de încredere mod de prevenire a erorilor neprevăzute, motiv pentru care includerea lui unde este fezabil este mai mult decât recomandată.
Pentru a rezuma, reziliența unei aplicații este un concept extrem de important pentru o funcționare de calitate a unui sistem, chiar dacă de multe ori nu este luat în calcul în activ sau este tratat ca o adăugare secundară, în timp ce alte cerințe sunt implementate. Chiar și așa, orice aplicație sau orice API care depinde de alte servicii externe ar trebui să includă cel puțin minimul de reziliență prin gestionarea erorilor menționate mai sus. Chiar dacă prevenirea este mai recomandată decât gestionarea, cele două sunt gândite pentru a conlucra, putând atinge nivelul ideal de redresare și reziliență doar ca un întreg.
Metodele menționate mai sus asigură un minim de reziliență de la care se poate pleca pentru a încerca modalități mai avansate și mai bine croite pentru fiecare sistem, întrucât fiecare este diferit și are nevoile proprii.
În încheiere, orice meșter bun trebuie să știe să-și aleagă bine uneltele. De aceea, încurajează utilizarea diferitelor librării pentru reziliență deja existente, care fac adăugarea acestui concept mult mai ușoară, oferind modalități de implementare care nu pleacă de la 0 și care pot să crească eficiența și corectitudinea implementării, dar și să scadă timpul pentru finalizarea implementării.
de Péter Török