Skriva robusta webbapplikationer Den förlorade arten av undantagshantering

Som utvecklare vill vi att de applikationer vi bygger är resistenta när det gäller misslyckande, men hur uppnår du det här målet? Om du tror att hype, mikrotjänster och ett smart kommunikationsprotokoll är svaret på alla dina problem, eller kanske automatiskt DNS-failover. Medan den typen av saker har sin plats och skapar en intressant konferenspresentation, är det något mindre glamorösa sanning att en bra applikation börjar med din kod. Men även väl utformade och välprövade applikationer saknas ofta en viktig komponent i fjädrande kod - undantagshantering.

Sponsrat innehåll

Detta innehåll har beställts av Engine Yard och skrevs och / eller redigerats av Tuts + -laget. Vårt mål med sponsrat innehåll är att publicera relevanta och objektiva handledningar, fallstudier och inspirerande intervjuer som erbjuder genuint pedagogiskt värde till våra läsare och gör det möjligt för oss att finansiera skapandet av mer användbart innehåll.

Jag misslyckas aldrig med att bli förvånad över hur underutnyttjad undantagshantering tenderar att vara jämn inom mogna kodbaser. Låt oss titta på ett exempel.


Vad kan eventuellt gå fel?

Säg att vi har en Rails-app, och en av de saker vi kan göra med den här appen är att hämta en lista över de senaste tweetsna för en användare, med tanke på deras handtag. Vår TweetsController kan se ut så här:

klass 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

Och den Person modell som vi använde kan likna följande:

klass person < 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

Denna kod verkar helt rimlig, det finns dussintals appar som har kod så här som sitter i produktion, men låt oss se lite närmare.

  • find_or_create_by är en Rails metod, det är inte en "bang" -metod, så det borde inte slänga undantag, men om vi tittar på dokumentationen kan vi se att på grund av hur denna metod fungerar kan den höja en Active :: RecordNotUnique fel. Detta kommer inte att hända ofta, men om vår ansökan har en anständig mängd trafik är det mer sannolikt än vad du kan förvänta dig (jag har sett det hända många gånger).
  • Medan vi är på ämnet kan alla bibliotek du använder slänga oväntade fel på grund av fel i biblioteket själv och Rails är inget undantag. Beroende på vår nivå av paranoia kan vi förvänta oss vårt find_or_create_by att kasta någon form av oväntat fel när som helst (en hälsosam nivå av paranoia är en bra sak när det gäller att bygga robust programvara). Om vi ​​inte har något globalt sätt att hantera oväntade fel (vi diskuterar det här nedan), kanske vi vill hantera dessa individuellt.
  • Då finns det person.fetch_tweets som instantiates en Twitter klient och försöker hämta några tweets. Detta kommer att vara ett nätverkssamtal och är benägen för all slags misslyckande. Vi kanske vill läsa dokumentationen för att ta reda på vilka möjliga fel vi kan förvänta oss, men vi vet att det inte bara är fel här, men sannolikt (till exempel, Twitter-API kan vara nere, en person med det handtaget kanske existerar inte etc.). Att inte lägga till några undantagshanteringslogik kring nätverkssamtal kräver att det blir problem.

Vår lilla mängd kod har några allvarliga problem, låt oss försöka göra det bättre.


Rätt mängd Exception Handling

Vi sätter ihop vår find_or_create_by och tryck ner den i Person modell:

klass person < 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 "Uppstod ett fel vid försök att hitta eller skapa Person för: # hand, # e.message # e.backtrace.join (" \ n ")" nil änden änden

Vi har hanterat Active :: RecordNotUnique enligt dokumentationen och nu vet vi för ett faktum att vi antingen får en Person objekt eller noll om något går fel Den här koden är nu solid, men hur hämtar vi våra tweets:

klass person < ActiveRecord::Base def fetch_tweets client.user_timeline(handle).map|tweet| tweet.text rescue => e Rails.logger.error "Fel vid hämtning av tweets för: # handle, # e.message # e.backtrace.join (" \ n ")" nil slut privat def klient @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 slutet änden

Vi trycker på att inställa Twitter-klienten i sin egen privata metod och eftersom vi inte visste vad som kunde gå fel när vi hämtar tweetsna räddar vi allt.

Du kanske har hört någonstans att du alltid ska fånga specifika fel. Detta är ett lovvärt mål, men folk tolkar ofta det som "om jag inte kan fånga något specifikt, kommer jag inte fånga någonting". I verkligheten, om du inte kan fånga något specifikt bör du fånga allt! På så sätt har du åtminstone möjlighet att göra något även om det bara är att logga och återuppta felet.

En bortsett från OO Design

För att göra vår kod mer robust, var vi tvungna att refactor och nu är vår kod antagligen bättre än tidigare. Du kan använda din önskan om mer känslig kod för att informera dina designbeslut.

En bortsett från testning

Varje gång du lägger till några undantagshanteringslogik på en metod, är det också en extra väg genom den metoden och den måste testas. Det är viktigt att du testar den exceptionella vägen, kanske mer än att testa den lyckliga vägen. Om något går fel på den lyckliga vägen har du nu extra försäkring av rädda blockera för att förhindra att din app faller över. Emellertid har någon logik inuti räddningsblocket i sig ingen sådan försäkring. Testa din exceptionella väg bra, så att dumma saker som misstänker ett variabelt namn inuti rädda blockera inte att din ansökan ska spränga (det har hänt mig så många gånger - allvarligt, testa bara din rädda block).


Vad man ska göra med de fel vi fångar

Jag har sett denna typ av kod otaliga gånger genom åren:

börja widgetron.create rescue # behöver inte göra något slut

Vi räddar ett undantag och gör ingenting med det. Det här är nästan alltid en dålig idé. När du debuggar en produktionsproblem sex månader från och med, försöker vi hitta varför din "widgetron" inte dyker upp i databasen, kommer du inte ihåg att oskyldig kommentar och timmar av frustration kommer att följa.

Svälj inte undantag! Åtminstone bör du logga något undantag som du får, till exempel:

starta foo.bar rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" slutet

På det här sättet kan vi tråla loggarna och vi kommer att få orsaken och stacken spår av felet att titta på.

Bättre än, du kan använda en felövervakningstjänst, till exempel Rollbar vilket är ganska trevligt. Det finns många fördelar med detta:

  • Dina felmeddelanden ignoreras inte med andra loggmeddelanden
  • Du får statistik om hur ofta samma fel har hänt (så du kan ta reda på om det är en allvarlig fråga eller inte)
  • Du kan skicka extra information tillsammans med felet för att hjälpa dig att diagnostisera problemet
  • Du kan få meddelanden (via email, pagerduty etc.) när fel uppstår i din app
  • Du kan spåra deployer för att se när vissa fel introducerades eller fixades
  • etc.
starta foo.bar rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" Rollbar.report_exception (e) avsluta

Du kan förstås logga och använda en övervakningstjänst som ovan.

Om din rädda block är det sista i en metod, jag rekommenderar att du har en uttrycklig avkastning:

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

Du kanske inte alltid vill återvända noll, ibland kan du vara bättre med ett nollobjekt eller vad som helst annat är meningsfullt i samband med din ansökan. Att konsekvent använda uttryckliga returvärden kommer att spara alla mycket förvirring.

Du kan också hämta samma fel eller höja en annan i din rädda blockera. Ett mönster som jag ofta tycker är användbart är att lägga in det befintliga undantaget i en ny och höja den för att inte förlora det ursprungliga stackspåret (jag skrev till och med en pärla för detta eftersom Ruby inte ger den här funktionen ur lådan ). Senare i artikeln när vi pratar om externa tjänster kommer jag att visa dig varför det här kan vara användbart.


Hantera fel globalt

Rails kan du ange hur man hanterar förfrågningar om resurser av ett visst format (HTML, XML, JSON) genom att använda svara till och respond_with. Jag ser sällan apps som korrekt använder denna funktion, trots allt om du inte använder en svara till blockera allt fungerar bra och Rails gör din mall korrekt. Vi träffade vår tweets controller via / Tweets / yukihiro_matz och få en HTML-sida full av Matzs senaste tweets. Vad folk ofta glömmer är att det är väldigt lätt att försöka be om ett annat format av samma resurs, t.ex.. /tweets/yukihiro_matz.json. Vid denna tidpunkt kommer Rails försiktigt att försöka återställa en JSON-representation av Matzs tweets, men det går inte bra eftersom utsikten för den inte existerar. En ActionView :: MissingTemplate fel kommer att höjas och vår app blåser upp på ett spektakulärt sätt. Och JSON är ett legitimt format, i en applikation med hög trafik är du lika sannolikt att få en förfrågan /tweets/yukihiro_matz.foobar. Tuts + får denna typ av förfrågningar hela tiden (troligen från bots som försöker vara kloka).

Lektionen är detta, om du inte planerar att returnera ett legitimt svar för ett visst format, begränsar dina kontrollanter från att försöka uppfylla förfrågningar för dessa format. I fallet med vår TweetsController:

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

Nu när vi får förfrågningar om falska format får vi en mer relevant ActionController :: UnknownFormat fel. Våra kontrollanter känner sig något stramare vilket är en bra sak när det gäller att göra dem mer robusta.

Hantera Fel i Rails Way

Det problem som vi har nu är att trots vår semantiskt tilltalande fel är vår applikation fortfarande uppblåst i användarnas ansikte. Det här är där global undantagshantering kommer in. Ibland kommer vår ansökan att skapa fel som vi vill svara på konsekvent, oavsett var de kommer ifrån (som vår ActionController :: UnknownFormat). Det finns också fel som kan höjas av ramverket innan någon av våra kod kommer till spel. Ett perfekt exempel på detta är ActionController :: RoutingError. När någon begär en webbadress som inte existerar, gillar / Tweets2 / yukihiro_matz, Det finns ingenstans för oss att haka in för att rädda detta fel, med hjälp av traditionell undantagshantering. Det är här Rails ' exceptions_app kommer in.

Du kan konfigurera en Rack-app i application.rb att kallas när ett fel som vi inte har hanterat produceras (som vår ActionController :: RoutingError eller ActionController :: UnknownFormat). Så här brukar du normalt använda det här är att konfigurera din ruttapp som den exceptions_app, Definiera sedan de olika rutterna för de fel du vill hantera och rikta dem till en speciell felkontroll som du skapar. Så vår application.rb skulle se ut så här:

... config.exceptions_app = self.routes ... 

Vår routes.rb kommer då att innehålla följande:

... match '/ 404' => 'fel # not_found', via:: all match '/ 406' => 'fel # not_acceptable', via:: all match '/ 500' => 'fel # internal_server_error' via: :Allt… 

I detta fall vår ActionController :: RoutingError skulle hämtas av 404 väg och ActionController :: UnknownFormat kommer att hämtas av 406 rutt. Det finns många möjliga fel som kan uppstå. Men så länge du hanterar de gemensamma (404, 500, 422 etc.) till att börja med kan du lägga till andra om och när de händer.

Inom vår felkontrollant kan vi nu göra relevanta mallar för varje typ av fel tillsammans med vår layout (om det inte är en 500) för att behålla varumärket. Vi kan också logga in felen och skicka dem till vår övervakningstjänst, även om de flesta övervakningstjänster automatiskt kommer att ansluta till processen så att du inte behöver skicka felen själv. Nu när vår applikation blåser upp gör det så försiktigt med rätt statuskod beroende på felet och en sida där vi kan ge användaren en uppfattning om vad som hände och vad de kan göra (kontakta support) - en oändligt bättre upplevelse. Ännu viktigare, vår app kommer att verkar (och kommer faktiskt bli) mycket mer solid.

Flera fel av samma typ i en kontroller

I vilken Rails controller som helst kan vi definiera specifika fel som ska hanteras globalt inom den kontrollen (oavsett vilken åtgärd de får producera i) - vi gör det via redning_from. Frågan är när du ska använda rescue_from? Jag brukar hitta att ett bra mönster är att använda det för fel som kan uppstå i flera åtgärder (till exempel samma fel i mer än en åtgärd). Om ett fel endast produceras av en åtgärd, hantera det via den traditionella börja ... rädda ... slutet mekanism, men om vi sannolikt kommer att få samma fel på flera ställen och vi vill hantera det på samma sätt - det är en bra kandidat för en rescue_from. Låt oss säga vårt TweetsController har också a skapa verkan:

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

Låt oss också säga att båda dessa åtgärder kan stöta på en TwitterError och om de vill vill vi berätta för användaren att något är fel med Twitter. Det är här rescue_from kan vara väldigt praktisk:

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

Nu behöver vi inte oroa oss för att hantera detta i våra handlingar och de kommer att se mycket renare ut och vi kan / borde - logga in vårt fel och / eller meddela vår felövervakningstjänst inom twitter_error metod. Om du använder rescue_from korrekt kan det inte bara hjälpa dig att göra din ansökan mer robust men kan också göra din styrenhetskod renare. Detta kommer att göra det enklare att behålla och testa din kod, vilket gör din ansökan lite mer robust än en gång.


Använda externa tjänster i din ansökan

Det är svårt att skriva en betydande ansökan dessa dagar utan att använda ett antal externa tjänster / API. I fallet med vår TweetsController, Twitter kom till spel via en Ruby-pärla som bryter in Twitter-API. Helst skulle vi göra alla våra externa API-samtal asynkront, men vi täcker inte asynkron bearbetning i den här artikeln och det finns många applikationer där ute som gör åtminstone några API / nätverkssamtal under bearbetning.

Att ringa nätverkssamtal är en extremt felaktig uppgift och bra undantagshantering är ett måste. Du kan få autentiseringsfel, konfigurationsproblem och anslutningsfel. Biblioteket du använder kan producera ett antal kodfel och då är det fråga om långsamma anslutningar. Jag glänsar över denna punkt, men det är oj så viktigt eftersom du inte kan hantera långsamma anslutningar via undantagshantering. Du måste konfigurera timeouts i ditt nätverksbibliotek på lämpligt sätt, eller om du använder ett API-omslag, se till att det ger krokar för att konfigurera timeouts. Det finns ingen sämre erfarenhet för en användare än att behöva sitta där och vänta utan att din ansökan ger någon indikation på vad som händer. Bara om alla glömmer att konfigurera timeouts på lämpligt sätt (jag vet att jag har), så ta hand om det.

Om du använder en extern tjänst på flera ställen inom din ansökan (flera modeller till exempel), exponerar du stora delar av din ansökan till hela landskapet av fel som kan produceras. Det här är inte en bra situation. Det vi vill göra är att begränsa vår exponering och det enda sättet vi kan göra är att få all tillgång till våra externa tjänster bakom en fasad, rädda alla fel där och återuppta ett semantiskt lämpligt fel (höja det TwitterError att vi pratade om fel uppstår när vi försöker slå Twitter API). Vi kan då enkelt använda tekniker som rescue_from att hantera dessa fel och vi exponerar inte stora delar av vår ansökan på ett okänt antal fel från externa källor.

En ännu bättre idé kan vara att göra din fasad till ett felfritt API. Återgå alla framgångsrika svaren som är och returnera nils eller null objekt när du räddar någon typ av fel (vi behöver fortfarande logga in / meddela oss om felen via några av de metoder som vi diskuterade ovan). På så sätt behöver vi inte blanda olika typer av kontrollflöde (undantagskontrollflöde vs om ... annars) som kan få oss betydligt renare kod. Till exempel, låt oss paketera vår Twitter API-åtkomst i en TwitterClient objekt:

klass TwitterClient attr_reader: klient def initiera @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 slutänden def latest_tweets (handle) client.user_timeline (handtag). karta | tweet | tweet.text rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" nil änden

Vi kan nu göra det här: TwitterClient.new.latest_tweets ( 'yukihiro_matz'), var som helst i vår kod och vi vet att det aldrig kommer att ge ett fel, eller snarare det kommer aldrig att sprida felet bortom TwitterClient. Vi har isolerat ett externt system för att se till att glitches i det systemet inte tar ner vår huvudapplikation.


Men vad händer om jag har utmärkt testdäckning?

Om du har en välprövad kod, berömmer jag dig på din flitighet, det tar dig en lång väg mot en mer robust applikation. Men en bra testpaket kan ofta ge en falsk känsla av säkerhet. Goda tester kan hjälpa dig refactor med självförtroende och skydda dig mot regression. Men du kan bara skriva test för saker du förväntar dig att hända. Buggar är av sin natur oväntade. Att använda vårt tweets-exempel tills vi väljer att skriva ett test för vårt fetch_tweets metod där client.user_timeline (handtag) väcker ett fel och tvingar oss att sätta in en rädda blockera koden, alla våra test har blivit gröna och vår kod skulle ha varit felaktig.

Skrivartester, befriar oss inte från ansvaret för att kasta ett kritiskt öga över vår kod för att ta reda på hur denna kod kan bryta. Å andra sidan kan en sådan utvärdering definitivt hjälpa oss att skriva bättre och mer kompletta testpaket.


Slutsats

Motståndskraftiga system strömmar inte helt ut från en helghacksession. Att göra en ansökan robust är en pågående process. Du upptäcker fel, fixar dem och skriv test för att se till att de inte kommer tillbaka. När din ansökan går ner på grund av ett extern systemfel, isolerar du det här systemet för att säkerställa att felet inte kan snöboll igen. Undantagshantering är din bästa vän när det gäller att göra detta. Även den mest felaktiga applikationen kan omvandlas till en robust om du tillämpar bra exceptionella hanteringsmetoder konsekvent över tiden.

Självklart är undantagshantering inte det enda verktyget i din arsenal när det gäller att göra ansökningar mer eftergivliga. I efterföljande artiklar kommer vi att prata om asynkron bearbetning, hur och när du ska tillämpa det och vad det kan göra när det gäller att göra ditt program fel tolerant. Vi kommer också att titta på några implementerings- och infrastrukturtips som kan få en betydande inverkan utan att bryta banken när det gäller både pengar och tid - håll dig stillad.