Scrierea aplicațiilor web robuste Arta pierdută de manipulare a excepțiilor

În calitate de dezvoltatori, dorim ca aplicațiile pe care le construim să fie rezistente atunci când vine vorba de eșec, dar cum realizați acest obiectiv? Dacă credeți că hype-ul, micro-serviciile și un protocol inteligent de comunicații sunt răspunsul la toate problemele dvs. sau poate funcționarea automată a DNS-ului. În timp ce astfel de lucruri au loc și fac o prezentare interesantă a conferinței, adevărul puțin mai plin de farmec este că o aplicație robustă începe cu codul tău. Dar, chiar și aplicațiile bine proiectate și bine testate nu au o componentă vitală a codului rezilient - manipularea excepțiilor.

Conținut sponsorizat

Acest conținut a fost comandat de Engine Yard și a fost scris și / sau editat de echipa Tuts +. Scopul nostru cu conținut sponsorizat este de a publica tutoriale relevante și obiective, studii de caz și interviuri inspirate care oferă cititorilor o valoare educațională autentică și ne permit să finanțăm crearea de conținut mai util.

Eu nu reușesc niciodată să fiu uimită de modul în care manipularea excepțională a utilizărilor tinde să fie chiar în cadrul unor coduri mature. Să ne uităm la un exemplu.


Ce este posibil să greșiți?

Spuneți că avem o aplicație Rails și unul dintre lucrurile pe care le putem face folosind această aplicație este să aducem o listă cu cele mai recente tweet-uri pentru un utilizator, dat fiind mânerul lor. Al nostru TweetsController ar putea arăta astfel:

clasa TweetsController < ApplicationController def show person = Person.find_or_create_by(handle: params[:handle]) if person.persisted? @tweets = person.fetch_tweets else flash[:error] = "Unable to create person with handle: #person.handle" end end end

Si Persoană model pe care l-am folosit ar putea fi similar cu următoarele:

persoană de clasă < ActiveRecord::Base def fetch_tweets client = Twitter::REST::Client.new do |config| config.consumer_key = configatron.twitter.consumer_key config.consumer_secret = configatron.twitter.consumer_secret config.access_token = configatron.twitter.access_token config.access_token_secret = configatron.twitter.access_token_secret end client.user_timeline(handle).map|tweet| tweet.text end end

Acest cod pare a fi perfect rezonabil, există zeci de aplicații care au codul ca și cum ar fi prezent în producție, dar să aruncăm o privire mai atentă.

  • find_or_create_by este o metodă Rails, nu este o metodă "bang", deci nu ar trebui să arunce excepții, dar dacă ne uităm la documentație putem vedea că, datorită modului în care funcționează această metodă, ActiveRecord :: RecordNotUnique eroare. Acest lucru nu se va întâmpla adesea, dar dacă aplicația noastră are un volum decent de trafic, se întâmplă mai mult decât s-ar putea aștepta (am văzut că se întâmplă de multe ori).
  • În timp ce suntem pe această temă, orice bibliotecă pe care o utilizați poate arunca erori neașteptate din cauza erorilor din cadrul bibliotecii în sine și Rails nu face excepție. În funcție de nivelul nostru de paranoia, ne putem aștepta la noi find_or_create_by pentru a arunca orice fel de eroare neasteptata in orice moment (un nivel sanatos de paranoia este un lucru bun cand vine vorba de construirea de software robust). Dacă nu avem un mod global de tratare a erorilor neașteptate (vom discuta mai jos), este posibil să ne ocupăm de acestea în mod individual.
  • Apoi este person.fetch_tweets care instanțează un client Twitter și încearcă să aducă câteva tweet-uri. Acesta va fi un apel de rețea și este predispus la tot felul de eșecuri. S-ar putea să dorim să citim documentația pentru a afla care sunt erorile posibile, dar știm că erorile nu sunt posibile doar aici, dar este foarte probabil (de exemplu, API-ul Twitter ar putea fi în jos, o persoană cu acel mâner ar putea nu există etc.). Nu punerea unor logica de tratare a excepțiilor în jurul apelurilor în rețea solicită probleme.

Suma noastră mică de cod are câteva probleme serioase, să încercăm să o facem mai bine.


Cuantumul corect al manipulării excepțiilor

Ne vom împacheta find_or_create_by și împingeți-l în jos Persoană model:

persoană de clasă < ActiveRecord::Base class << self def find_or_create_by_handle(handle) begin Person.find_or_create_by(handle: handle) rescue ActiveRecord::RecordNotUnique Rails.logger.warn  "Encountered a non-fatal RecordNotUnique error for: #handle"  retry rescue => e Rails.logger.error "A întâlnit o eroare atunci când încercăm să găsim sau să creăm o persoană pentru: # handle, # e.message # e.backtrace.join (" \ n ")" sfârșitul final

Am rezolvat problema ActiveRecord :: RecordNotUnique conform documentației și acum știm cu toții că vom obține fie Persoană obiect sau zero dacă ceva nu merge bine. Acest cod este acum solid, dar despre atragerea tweets-ului nostru:

persoană de clasă < ActiveRecord::Base def fetch_tweets client.user_timeline(handle).map|tweet| tweet.text rescue => e Rails.logger.error "Eroare la preluarea tweets pentru: # handle, # e.message # e.backtrace.join (" \ n ")" = Twitter :: REST :: Client.new do | config | config.consumer_key = configatron.twitter.consumer_key config.consumer_secret = configatron.twitter.consumer_secret config.access_token = configatron.twitter.access_token config.access_token_secret = configatron.twitter.access_token_secret capăt sfârșit sfârșit

Împușcăm instanțializarea clientului Twitter în propria sa metodă privată și din moment ce nu știm ce s-ar putea întâmpla greșit atunci când luăm tweets, salvăm totul.

S-ar putea să fi auzit undeva că ar trebui întotdeauna să prindeți erori specifice. Acesta este un obiectiv lăudabil, dar oamenii înțeleg adesea greșit că "dacă nu pot prinde ceva specific, nu voi prinde nimic". În realitate, dacă nu puteți prinde ceva specific, ar trebui să prindeți totul! În acest fel, cel puțin aveți posibilitatea să faceți ceva chiar dacă este vorba numai de înregistrarea și re-ridicarea erorii.

O parte din proiectarea OO

Pentru a face codul nostru mai robust, am fost forțați să refacem și acum codul nostru este, fără îndoială, mai bun decât înainte. Puteți folosi dorința dvs. pentru un cod mai rezistent pentru a vă informa deciziile de proiectare.

O parte din testare

De fiecare dată când adăugați o logică de tratare a excepțiilor unei metode, este și o cale suplimentară prin această metodă și trebuie testată. Este vital să testați calea excepțională, poate mai mult decât să testați calea fericită. Dacă se întâmplă ceva în calea fericită, aveți acum asigurările suplimentare salvare blocați pentru a împiedica căderea aplicației dvs. Cu toate acestea, orice logică din interiorul blocului de salvare nu are o astfel de asigurare. Testați-vă bine calea excepțională, astfel încât lucrurile prostești, cum ar fi înnegrirea unui nume variabil în interiorul salvare blocul nu provoacă apariția aplicației dvs. (acest lucru mi sa întâmplat de atâtea ori - serios, doar vă testați salvare blocuri).


Ce să facem cu erorile pe care le prindem

Am văzut acest tip de cod nenumărate ori prin ani:

începe widgetron.create de salvare # nu trebuie să faceți nimic sfârșitul

Salvăm o excepție și nu facem nimic cu ea. Aceasta este aproape întotdeauna o idee proastă. Când depanați o problemă de producție după șase luni, încercând să ne dăm seama de ce "widgetronul" dvs. nu apare în baza de date, nu vă veți aminti că va urma comentariul nevinovat și orele de frustrare.

Nu înghiți excepții! Cel puțin ar trebui să înregistrați orice excepție pe care o capturați, de exemplu:

începe foo.bar rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" sfârșit

În acest fel putem trage bustenii și vom avea cauza și stivă urmă a erorii să se uite la.

Mai bine, puteți utiliza un serviciu de monitorizare a erorilor, cum ar fi Rollbar, care este destul de frumos. Există multe avantaje:

  • Mesajele de eroare nu sunt intercalate cu alte mesaje de jurnal
  • Veți obține statistici despre cât de des sa întâmplat aceeași eroare (pentru a vă da seama dacă este o problemă serioasă sau nu)
  • Puteți trimite informații suplimentare împreună cu eroarea pentru a vă ajuta să diagnosticați problema
  • Puteți primi notificări (prin e-mail, pagerduty etc.) atunci când apar erori în aplicația dvs.
  • Puteți urmări deploys pentru a vedea când erorile speciale au fost introduse sau reparate
  • etc.
Începeți foo.bar rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" Rollbar.report_exception (e)

Puteți, desigur, atât să vă înregistrați, cât și să utilizați un serviciu de monitorizare ca mai sus.

Dacă ale tale salvare bloc este ultimul lucru dintr-o metodă, vă recomand să aveți o întoarcere explicită:

def my_method begin foo.bar rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" Rollbar.report_exception

S-ar putea să nu întotdeauna doriți să vă întoarceți zero, uneori ați putea fi mai bine cu un obiect nul sau orice altceva are sens în contextul cererii dumneavoastră. Utilizarea consistentă a valorii returnate va salva toată lumea multă confuzie.

Puteți, de asemenea, să re-ridicați aceeași eroare sau să ridicați altul în interiorul dvs. salvare bloc. Un model pe care de multe ori îl consider util este să înfășurați excepția existentă într-una nouă și să o ridicați pentru a nu pierde urme de stivă originale (chiar am scris o piatră prețioasă deoarece Ruby nu oferă această funcționalitate din cutie ). Mai târziu, în articol, când vorbim despre servicii externe, vă voi arăta de ce acest lucru poate fi util.


Gestionarea erorilor la nivel global

Rails vă permite să specificați modul de gestionare a solicitărilor de resurse ale unui anumit format (HTML, XML, JSON) utilizând răspunde la și respond_with. Apreciez rareori aplicațiile care utilizează corect această funcție, după toate dacă nu utilizați a răspunde la Blocați totul funcționează bine și Rails face șablonul corect. Am lovit controlerul nostru prin tweets / Tweets / yukihiro_matz și obțineți o pagină HTML plină de ultimele tweet-uri ale lui Matzs. Ceea ce oamenii uită de multe ori este că este foarte ușor să încerci să solicite un alt format al aceleiași resurse, de ex. /tweets/yukihiro_matz.json. În acest moment, Rails va încerca cu înverșunare să întoarcă o reprezentare JSON a tweets-ului lui Matzs, dar nu va merge bine din moment ce nu există. Un ActionView :: MissingTemplate eroarea va apărea și aplicația noastră aruncă într-un mod spectaculos. Și JSON este un format legitim, într-o aplicație cu trafic ridicat, la care ești la fel de probabil să primești o cerere /tweets/yukihiro_matz.foobar. Tuts + primește astfel de cereri tot timpul (probabil de la roboții care încearcă să fie inteligenți).

Lecția este aceasta, dacă nu intenționați să returnați un răspuns legitim pentru un anumit format, restricționați controlorii să nu încerce să îndeplinească cererile pentru aceste formate. În cazul nostru TweetsController:

clasa TweetsController < ApplicationController respond_to :html def show… respond_to do |format| format.html end end end

Acum, când primim cereri de formate falsificate, vom fi mai relevanți ActionController :: UnknownFormat eroare. Controlorii noștri se simt mai strâns, ceea ce este un lucru minunat atunci când vine vorba de a le face mai robusti.

Manipularea erorilor calea Rails

Problema pe care o avem acum este că, în ciuda erorii noastre semantic plăcute, aplicația noastră continuă să explodeze în fața utilizatorilor noștri. Aici intră manevrarea excepțiilor globale. Uneori cererea noastră va produce erori pe care vrem să le răspundem în mod consecvent, indiferent de unde provin acestea (cum ar fi ActionController :: UnknownFormat). Există, de asemenea, erori care pot fi ridicate de cadru înainte ca oricare din codul nostru să intre în joc. Un exemplu perfect al acestui lucru este ActionController :: RoutingError. Când cineva solicită o adresă URL care nu există, cum ar fi / Tweets2 / yukihiro_matz, nu există unde să ne angajăm să salvăm această eroare, folosind manipularea tradițională a excepțiilor. Aici, exceptions_app intră.

Puteți configura o aplicație Rack în application.rb pentru a fi numit atunci când o eroare pe care nu am manipulat este produs (cum ar fi ActionController :: RoutingError sau ActionController :: UnknownFormat). Modul în care veți vedea în mod normal acest lucru este de a configura aplicația rute ca exceptions_app, apoi definiți diferitele rute pentru erorile pe care doriți să le gestionați și le direcționați către un controler de erori special pe care îl creați. Deci, noi application.rb ar arata astfel:

... config.exceptions_app = auto.routes ... 

Al nostru routes.rb va conține următoarele:

... match '/ 404' => 'erori # not_found', prin:: toate meciurile '/ 406' => 'eroare # not_acceptable' :toate… 

În acest caz, nostru ActionController :: RoutingError va fi preluat de către 404 rută și ActionController :: UnknownFormat va fi preluat de către 406 traseu. Există multe erori posibile care pot apărea. Dar atâta timp cât vă ocupați de cele obișnuite (404, 500, 422 etc.), puteți adăuga alții dacă și când se întâmplă.

În cadrul controlerului nostru de erori putem reda șabloanele relevante pentru fiecare tip de eroare, împreună cu aspectul nostru (dacă nu este 500) pentru a menține branding-ul. Putem de asemenea să înregistrăm erorile și să le trimitem serviciului nostru de monitorizare, deși majoritatea serviciilor de monitorizare se vor conecta automat la acest proces, astfel încât nu trebuie să trimiteți singur erorile. Acum, când aplicația noastră aruncă în aer, procedează atât de ușor, cu codul de stare corespunzător în funcție de eroare și o pagină în care putem oferi utilizatorului o idee despre ceea ce sa întâmplat și ce pot face (contactați asistența) - o experiență infinit mai bună. Mai important, aplicația noastră va părea (și va fi de fapt) mult mai solidă.

Erori multiple de același tip într-un controler

În orice controler Rails putem defini erori specifice care trebuie tratate la nivel global în acel controller (indiferent de acțiunea pe care o produc în) - facem acest lucru prin intermediul rescue_from. Întrebarea este când să o folosiți rescue_from? De obicei, găsesc că un model bun este să îl folosiți pentru erori care pot apărea în mai multe acțiuni (de exemplu, aceeași eroare în mai multe acțiuni). Dacă o eroare va fi produsă numai printr-o singură acțiune, o puteți trata prin metoda tradițională începe ... salvarea ... sfârșitul mecanism, dar dacă suntem susceptibile de a obține aceeași eroare în mai multe locuri și dorim să ne ocupăm de același lucru - este un candidat bun pentru o rescue_from. Să spunem noi TweetsController are, de asemenea, crea acțiune:

clasa TweetsController < ApplicationController respond_to :html def show… respond_to do |format| format.html end end def create… end end

Să spunem, de asemenea, că ambele acțiuni pot întâlni a TwitterError iar dacă vrem să le spunem utilizatorilor că ceva nu este în regulă cu Twitter. Aici e locul rescue_from poate fi foarte util:

clasa TweetsController < ApplicationController respond_to :html rescue_from TwitterError, with: twitter_error private def twitter_error render :twitter_error end end

Acum nu trebuie să ne facem griji cu privire la modul în care ne ocupăm de acest lucru în acțiunile noastre și vor arăta mult mai curate și putem / ar trebui - desigur - să ne logăm eroarea și / sau să notigem serviciul nostru de monitorizare a erorilor în cadrul twitter_error metodă. Dacă utilizați rescue_from în mod corect, nu numai că vă poate ajuta să faceți aplicația mai robustă, dar puteți, de asemenea, să faceți mai curat codul dvs. de controler. Acest lucru va face mai ușor să vă mențineți și să vă testați codul, făcând aplicația dvs. puțin mai rezistentă încă o dată.


Utilizarea serviciilor externe în aplicația dvs.

Este dificil să scrieți o aplicație semnificativă în aceste zile fără a utiliza un număr de servicii externe / API-uri. În cazul nostru TweetsController, Twitter a intrat în joc printr-o bijuterie Ruby care împachetează API-ul Twitter. În mod ideal am face toate apelurile externe API în mod asincron, dar nu acoperim procesarea asincronă în acest articol și există o mulțime de aplicații acolo care fac cel puțin unele apeluri API / rețea în proces.

Efectuarea apelurilor în rețea este o sarcină extrem de predispusă la erori și o manevrare excepțională este o necesitate. Puteți obține erori de autentificare, probleme de configurare și erori de conectivitate. Biblioteca pe care o utilizați poate produce orice număr de erori de cod și apoi există o problemă de conexiuni lente. Mă gândesc la acest punct, dar este atât de important, deoarece nu puteți face legătura lentă prin tratarea excepțiilor. Trebuie să configurați timeout-urile în mod corespunzător în biblioteca dvs. de rețea sau dacă utilizați un wrapper API, asigurați-vă că oferă cârlige pentru a configura temporizările. Nu există o experiență mai proastă pentru un utilizator decât să trebuiască să stați acolo în așteptare fără ca cererea dvs. să indice ce se întâmplă. Aproape toata lumea uita sa configureze timeout-urile in mod corespunzator (stiu ca am), asa ca ia in considerare.

Dacă utilizați un serviciu extern în mai multe locații din cadrul aplicației dvs. (de exemplu, mai multe modele), expuneți părți importante ale aplicației dvs. la peisajul complet al erorilor care pot fi produse. Aceasta nu este o situație bună. Ceea ce vrem să facem este să ne limităm expunerea și o modalitate prin care putem face acest lucru este să punem tot accesul la serviciile noastre externe în spatele unei fațade, să salvăm toate erorile și să ridicăm din nou o eroare adecvată semantic TwitterError că am discutat dacă apar erori atunci când încercăm să atingem API-ul Twitter). Putem apoi să folosim cu ușurință tehnici de genul rescue_from pentru a face față acestor erori și nu expunem părți importante ale aplicației noastre la un număr necunoscut de erori din surse externe.

O idee chiar mai bună ar fi să vă faci fațada un API fără erori. Întoarceți toate răspunsurile de succes așa cum este și reveniți nils sau obiecte nul atunci când salvați orice fel de eroare (avem totuși nevoie să ne logam / notificăm noi înșine erorile prin intermediul unora dintre metodele discutate mai sus). În acest fel, nu este nevoie să amestecăm diferite tipuri de flux de control (excepție de control al fluxului vs dacă ... altceva) care ne pot obține un cod mai curat. De exemplu, să cuprindem accesul nostru API Twitter într-un TwitterClient obiect:

clasa TwitterClient attr_reader: client def initialize @client = Twitter :: REST :: Client.new do | config | config.consumer_key = configatron.twitter.consumer_key config.consumer_secret = configatron.twitter.consumer_secret config.access_token = configatron.twitter.access_token config.access_token_secret = configatron.twitter.access_token_secret sfârșitul final def latest_tweets (handle) client.user_timeline (handle). Harta | tweet | tweet.text rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")"

Acum putem face acest lucru: TwitterClient.new.latest_tweets ( 'yukihiro_matz'), oriunde în codul nostru și știm că nu va produce niciodată o eroare, sau mai degrabă nu va propaga eroarea dincolo TwitterClient. Am izolat un sistem extern pentru a ne asigura că glitches în acest sistem nu ne va reduce aplicația principală.


Dar dacă am o acoperire excelentă a testelor?

Dacă aveți un cod bine testat, vă felicit pentru diligența dvs., vă va duce mult în calea unei aplicații mai robuste. Dar o suită de testare bună poate oferi adesea un fals sentiment de securitate. Testele bune vă pot ajuta să refaceți cu încredere și să vă protejați împotriva regresiei. Dar, puteți scrie doar teste pentru lucrurile pe care le așteptați să se întâmple. Bug-urile sunt, prin însăși natura lor, neașteptate. Pentru a utiliza exemplul nostru de tweets, până când alegem să scrie un test pentru noi fetch_tweets metoda unde client.user_timeline (mâner) ridică o eroare prin aceasta obligându-ne să înfășurăm a salvare blocați în jurul codului, toate testele noastre vor fi verde și codul nostru ar fi rămas predispus la eșec.

Scriind teste, nu ne eliberează de responsabilitatea de a arunca o privire critică asupra codului nostru pentru a ne da seama cum se poate rupe acest cod. Pe de altă parte, acest tip de evaluare poate ajuta cu siguranță să scriem suite de testare mai bune, mai complete.


Concluzie

Sistemele rezistente nu se formează pe deplin dintr-o sesiune de hacking de weekend. Efectuarea unei aplicații robuste este un proces continuu. Descoperiți bug-uri, reparați-le și scrieți teste pentru a vă asigura că nu se mai întorc. Atunci când cererea dvs. scade din cauza unei defecțiuni a sistemului extern, izolați sistemul respectiv pentru a vă asigura că defecțiunea nu mai poate relua zăpada. Manipularea excepțiilor este cea mai bună prietenă atunci când vine vorba de a face acest lucru. Chiar și aplicația cu cea mai mare greșeală poate fi transformată într-o aplicație robustă dacă aplicați practici bune de tratare a excepțiilor în mod consecvent, în timp.

Desigur, tratarea excepțiilor nu este singurul instrument din arsenalul dvs. atunci când vine vorba de a face aplicațiile mai rezistente. În articolele ulterioare vom vorbi despre procesarea asincronă, cum și când să o aplicăm și ce poate face în privința toleranței la aplicarea aplicației. Vom analiza, de asemenea, câteva sfaturi de implementare și infrastructură care pot avea un impact semnificativ fără a sparge banca în termeni atât de bani, cât și de acordarea timpului.

Cod