Skrive Robust Webapplikasjoner Den Lost Art Of Exception Handling

Som utviklere ønsker vi at applikasjonene vi bygger, er motstandsdyktige når det gjelder feil, men hvordan oppnår du dette målet? Hvis du tror sprøytenarkomanen, mikrotjenestene og en smart kommunikasjonsprotokoll er svaret på alle dine problemer, eller kanskje automatisk DNS-failover. Mens den typen ting har sin plass og gir en interessant konferansepresentasjon, er den litt mindre glamorøse sannheten at det å lage en robust søknad begynner med koden din. Men selv godt utformede og godt testede applikasjoner mangler ofte en viktig komponent i elastisk kode - unntakshåndtering.

Sponset innhold

Dette innholdet ble bestilt av Engine Yard og ble skrevet og / eller redigert av Tuts + -laget. Vårt mål med sponset innhold er å publisere relevante og objektive opplæringsprogrammer, casestudier og inspirerende intervjuer som gir ekte pedagogisk verdi til våre lesere og gjør det mulig for oss å finansiere etableringen av mer nyttig innhold.

Jeg unnlater aldri å bli overrasket over bare hvordan undervunnet unntakshåndtering pleier å være selv innenfor modne kodebaser. La oss se på et eksempel.


Hva kan muligens gå galt?

Si at vi har en Rails-app, og en av tingene vi kan gjøre ved hjelp av denne appen, er å hente en liste over de siste tweets for en bruker, gitt sitt håndtak. Våre TweetsController kan se slik ut:

klassen 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

Og Person modell som vi brukte kan være lik følgende:

klasse 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

Denne koden virker helt fornuftig, det er dusinvis av apper som har kode akkurat som dette sitter i produksjon, men la oss se litt nærmere ut.

  • find_or_create_by er en Rails metode, det er ikke en "bang" -metode, så det bør ikke kaste unntak, men hvis vi ser på dokumentasjonen, kan vi se at på grunn av måten denne metoden fungerer, kan den øke en Active :: RecordNotUnique feil. Dette skjer ikke ofte, men hvis søknaden vår har en anstendig mengde trafikk, forekommer det mer sannsynlig enn du kanskje regner med (jeg har sett det skje mange ganger).
  • Mens vi er på emnet, kan ethvert bibliotek du bruker, kaste uventede feil på grunn av feil i biblioteket selv og Rails er intet unntak. Avhengig av nivået på paranoia kan vi forvente at vår find_or_create_by å kaste enhver form for uventet feil når som helst (et sunt nivå av paranoia er en god ting når det kommer til å bygge robust programvare). Hvis vi ikke har noen global måte å håndtere uventede feil på (vi skal diskutere dette nedenfor), kan det hende vi vil håndtere disse individuelt.
  • Så er det person.fetch_tweets som instantiates en Twitter klient og prøver å hente noen tweets. Dette vil være en nettverksanrop og er utsatt for all slags feil. Vi vil kanskje lese dokumentasjonen for å finne ut hva de mulige feilene vi kan forvente er, men vi vet at feil ikke bare er mulig her, men ganske sannsynlig (for eksempel kan Twitter-API-en være nede, en person med det håndtaket kan kanskje ikke eksistert etc.). Ikke å sette noen unntakshåndteringslogikk rundt nettverkssamtaler, ber om problemer.

Vår lille mengde kode har noen alvorlige problemer, la oss prøve å gjøre det bedre.


Den riktige mengden unntakshåndtering

Vi pakker inn vår find_or_create_by og skyv den ned i Person modell:

klasse 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 "Oppdag en feil ved forsøk på å finne eller opprette Person for: # handle, # e.message # e.backtrace.join (" \ n ")" null ende slutten

Vi har håndtert Active :: RecordNotUnique ifølge dokumentasjonen og nå vet vi for et faktum at vi enten får en Person objekt eller nil hvis noe går galt Denne koden er nå solid, men hva med å hente våre tweets:

klasse person < ActiveRecord::Base def fetch_tweets client.user_timeline(handle).map|tweet| tweet.text rescue => e Rails.logger.error "Feil mens du henter tweets for: # handle, # e.message # e.backtrace.join (" \ n ")" nil end private 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 sluttendens ende

Vi presser instantiating Twitter klienten ned i sin egen private metode og siden vi ikke visste hva som kunne gå galt når vi henter tweets, redder vi alt.

Du har kanskje hørt et sted at du alltid skal ta feil. Dette er et lovverdig mål, men folk fortolker ofte det som, "hvis jeg ikke kan få noe spesifikt, vil jeg ikke fange noe". I virkeligheten, hvis du ikke kan fange noe spesifikt, bør du fange alt! På denne måten har du i det minste mulighet til å gjøre noe selv om det bare er å logge og gjenopprette feilen.

En bortsett fra OO Design

For å gjøre koden mer robust, ble vi tvunget til å refactor og nå er vår kode uten tvil bedre enn den var før. Du kan bruke ditt ønske om mer robust kode for å informere dine designbeslutninger.

En bortsett fra testing

Hver gang du legger til noen unntakshåndteringslogikk for en metode, er det også en ekstra bane gjennom denne metoden, og den må prøves. Det er viktig at du tester den eksepsjonelle banen, kanskje mer enn å teste den gode banen. Hvis noe går galt på den lykkelige stien, har du nå den ekstra forsikringen av redde blokkere for å forhindre at appen din faller over. Imidlertid har ingen logikk inne i redningsblokken selv ingen slik forsikring. Test din eksepsjonelle sti bra, så dumme ting som å mistype et variabelt navn inne i redde blokkere ikke føre til at søknaden din blåses opp (dette har skjedd med meg så mange ganger - seriøst, bare test ditt redde blokker).


Hva å gjøre med feilene vi fanger

Jeg har sett denne typen kode utallige ganger gjennom årene:

start widgetron.create rescue # trenger ikke å gjøre noe slutt

Vi redder et unntak og gjør ikke noe med det. Dette er nesten alltid en dårlig ide. Når du feilsøker et produksjonsproblem seks måneder fra nå, og prøver å finne ut hvorfor din "widgetron" ikke vises i databasen, vil du ikke huske at uskyldig kommentar og timer med frustrasjon vil følge.

Ikke svelg unntak! I det minste bør du logge noe unntak som du fanger, for eksempel:

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

På denne måten kan vi tråle loggene, og vi får årsaken og stakk spor av feilen for å se på.

Bedre ennå, kan du bruke en feilsøkingstjeneste, for eksempel Rollbar som er ganske fin. Det er mange fordeler med dette:

  • Feilmeldingene dine ignoreres ikke med andre loggmeldinger
  • Du får statistikk over hvor ofte den samme feilen har skjedd (slik at du kan finne ut om det er et alvorlig problem eller ikke)
  • Du kan sende ekstra informasjon sammen med feilen for å hjelpe deg med å diagnostisere problemet
  • Du kan få varsler (via e-post, pagerduty etc.) når det oppstår feil i appen din
  • Du kan spore deployer for å se når bestemte feil ble introdusert eller fikset
  • etc.
start foo.bar rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" Rollbar.report_exception (e) avslutte

Du kan selvfølgelig logge og bruke en overvåkingstjeneste som ovenfor.

Hvis din redde blokk er den siste i en metode, jeg anbefaler å ha en eksplisitt retur:

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

Du vil kanskje ikke alltid komme tilbake nil, Noen ganger kan du være bedre med et null objekt eller hva som helst annet gir mening i forbindelse med søknaden din. Ved konsekvent bruk av eksplisitte returverdier sparer alle en masse forvirring.

Du kan også øke den samme feilen eller heve en annen i din redde blokkere. Et mønster som jeg ofte finner nyttig er å pakke inn det eksisterende unntaket i en ny og heve det slik at du ikke mister den opprinnelige stakkesporen (jeg skrev til og med en perle for dette siden Ruby ikke gir denne funksjonaliteten ut av esken ). Senere i artikkelen når vi snakker om eksterne tjenester, vil jeg vise deg hvorfor dette kan være nyttig.


Håndtering av feil globalt

Rails lar deg spesifisere hvordan du håndterer forespørsler om ressurser av et bestemt format (HTML, XML, JSON) ved å bruke svare til og respond_with. Jeg ser sjelden apper som korrekt bruker denne funksjonaliteten, tross alt hvis du ikke bruker en svare til blokkere alt fungerer fint og Rails gjør malen riktig. Vi slo vår tweets kontroller via / Tweets / yukihiro_matz og få en HTML-side full av Matzs siste tweets. Hva folk ofte glemmer er at det er veldig enkelt å prøve og be om et annet format av samme ressurs, for eksempel. /tweets/yukihiro_matz.json. På dette punktet vil Rails trygt prøve å returnere en JSON-representasjon av Matzs tweets, men det vil ikke gå bra siden visningen for det ikke eksisterer. en ActionView :: MissingTemplate Feil vil bli hevet og vår app blåser opp på en spektakulær måte. Og JSON er et legitimt format, i et høyt trafikkprogram du er like sannsynlig å få en forespørsel om /tweets/yukihiro_matz.foobar. Tuts + får slike forespørsler hele tiden (sannsynligvis fra bots prøver å være smart).

Leksjonen er dette, hvis du ikke planlegger å returnere en legitim respons for et bestemt format, begrenser du kontrollerne fra å forsøke å oppfylle forespørsler om disse formatene. I tilfelle av vår TweetsController:

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

Nå når vi mottar forespørsler om falske formater, blir vi mer relevante ActionController :: UnknownFormat feil. Våre kontrollere føler seg noe strammere, noe som er en god ting når det gjelder å gjøre dem mer robuste.

Håndtering av feil på veiene

Problemet vi har nå er at til tross for vår semantisk tiltalende feil, er applikasjonen vår fortsatt i brukernes ansikt. Dette er hvor global unntakshåndtering kommer inn. Noen ganger vil applikasjonen vår gi feil som vi vil svare på konsekvent, uansett hvor de kommer fra (som vår ActionController :: UnknownFormat). Det er også feil som kan bli hevet av rammen før noen av kodene våre kommer til spill. Et perfekt eksempel på dette er ActionController :: RoutingError. Når noen ber om en nettadresse som ikke eksisterer, liker / Tweets2 / yukihiro_matz, Det er ingen steder for oss å koble inn for å redde denne feilen, ved hjelp av tradisjonell unntakshåndtering. Det er her Rails ' exceptions_app kommer inn.

Du kan konfigurere en Rack-app i application.rb å bli kalt når en feil vi ikke har håndtert er produsert (som vår ActionController :: RoutingError eller ActionController :: UnknownFormat). Måten du normalt vil se dette brukes til, er å konfigurere ruten app som exceptions_app, Definer deretter de ulike rutene for feilene du vil håndtere, og rute dem til en spesiell feilkontroller som du oppretter. Så vår application.rb ville se slik ut:

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

Våre routes.rb vil da inneholde følgende:

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

I dette tilfellet vår ActionController :: RoutingError ville bli plukket opp av 404 rute og ActionController :: UnknownFormat vil bli plukket opp av 406 rute. Det er mange mulige feil som kan kaste opp. Men så lenge du håndterer de vanlige (404, 500, 422 etc.) til å begynne med, kan du legge til andre hvis og når de skjer.

Innenfor vår feilkontroller kan vi nå gjengi de aktuelle maler for hver type feil sammen med vårt layout (hvis det ikke er en 500) for å opprettholde merkingen. Vi kan også logge feilene og sende dem til vår overvåkningstjeneste, selv om de fleste overvåkings tjenester vil koble seg til denne prosessen automatisk, slik at du ikke trenger å sende feilene selv. Nå når applikasjonen vår blåser opp, gjør det så forsiktig, med riktig statuskode, avhengig av feilen og en side der vi kan gi brukeren en ide om hva som skjedde og hva de kan gjøre (kontakt support) - en uendelig bedre opplevelse. Enda viktigere, vår app vil virke (og vil faktisk være) mye mer solid.

Flere feil av samme type i en kontroller

I hvilken som helst Rails controller kan vi definere spesifikke feil som skal håndteres globalt innenfor den kontrolleren (uansett hvilken handling de blir produsert i) - vi gjør dette via redning_from. Spørsmålet er når du skal bruke rescue_from? Jeg finner vanligvis at et godt mønster er å bruke det for feil som kan oppstå i flere handlinger (for eksempel den samme feilen i mer enn en handling). Hvis en feil kun blir produsert av en handling, håndter den den tradisjonelle begynn ... redning ... ende mekanisme, men hvis vi sannsynligvis vil få samme feil på flere steder, og vi ønsker å håndtere det på samme måte - det er en god kandidat for en rescue_from. La oss si vår TweetsController har også a skape handling:

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

La oss også si at begge disse handlingene kan støte på en TwitterError og hvis de gjør det, vil vi fortelle brukeren at noe er galt med Twitter. Dette er hvor rescue_from kan være veldig nyttig:

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

Nå trenger vi ikke å bekymre oss for å håndtere dette i våre handlinger, og de vil se mye renere ut, og vi kan / burde - selvfølgelig - logge på vår feil og / eller varsle vår feilovervåkningstjeneste innenfor twitter_error metode. Hvis du bruker rescue_from På riktig måte kan det ikke bare hjelpe deg med å gjøre applikasjonen din mer robust, men kan også gjøre din kontrollerkode renere. Dette vil gjøre det enklere å vedlikeholde og teste koden som gjør applikasjonen din litt mer motstandsdyktig igjen.


Bruk av eksterne tjenester i programmet

Det er vanskelig å skrive en betydelig applikasjon i disse dager uten å bruke en rekke eksterne tjenester / APIer. I tilfelle av vår TweetsController, Twitter kom inn i spill via en Ruby perle som bryter Twitter API. Ideelt sett ville vi gjøre alle våre eksterne API-anrop asynkront, men vi dekker ikke asynkron behandling i denne artikkelen, og det er mange programmer der ute som gjør minst noen API / nettverkssamtaler i prosessen..

Å foreta nettverkssamtaler er en ekstremt feilaktig oppgave, og godt unntakshåndtering er et must. Du kan få godkjenningsfeil, konfigurasjonsproblemer og tilkoblingsfeil. Biblioteket du bruker kan produsere et hvilket som helst antall kodefeil, og så er det et spørsmål om sakte forbindelser. Jeg glosser over dette punktet, men det er jo så viktig siden du ikke kan takle langsomme forbindelser via unntakshåndtering. Du må konfigurere tidsavbrudd på riktig måte i nettverksbiblioteket ditt, eller hvis du bruker en API-wrapper, må du sørge for at kroker konfigurerer timeouts. Det er ingen verre opplevelse for en bruker enn å måtte sitte der og vente uten at søknaden din gir noen indikasjon på hva som skjer. Omtrent alle glemmer å konfigurere timeouts på riktig måte (jeg vet jeg har), så vær oppmerksom.

Hvis du bruker en ekstern tjeneste på flere steder i applikasjonen din (for eksempel flere modeller), avslører du store deler av søknaden din til hele landskapet av feil som kan produseres. Dette er ikke en god situasjon. Det vi vil gjøre er å begrense vår eksponering, og en måte vi kan gjøre på dette, er å sette all tilgang til våre eksterne tjenester bak en fasade, redde alle feilene der og re-raise en semantisk passende feil (heve det TwitterError at vi snakket om eventuelle feil oppstår når vi prøver å treffe Twitter API). Vi kan så enkelt bruke teknikker som rescue_from å håndtere disse feilene, og vi utsettes ikke for store deler av vår søknad til et ukjent antall feil fra eksterne kilder.

En enda bedre ide kan være å gjøre fasaden din feilfri API. Returner alle vellykkede svar som det er og returner nuller eller nullobjekter når du redder noen form for feil (vi trenger fortsatt å logge / varsle oss om feilene via noen av metodene vi diskuterte ovenfor). På denne måten trenger vi ikke å blande ulike typer kontrollflyt (unntakskontrollstrøm vs hvis ... annet) som kan gi oss betydelig renere kode. For eksempel, la oss pakke inn vår Twitter API-tilgang i en TwitterClient gjenstand:

klasse TwitterClient attr_reader: klient def initialiser @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 sluttenden def latest_tweets (handle) client.user_timeline (handle). kartet | tweet | tweet.text rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" null ende

Vi kan nå gjøre dette: TwitterClient.new.latest_tweets ( 'yukihiro_matz'), hvor som helst i koden vår, og vi vet at det aldri vil gi en feil, eller heller vil den aldri forplante feilen utover TwitterClient. Vi har isolert et eksternt system for å sikre at feil i det systemet ikke kommer ned vår hovedapplikasjon.


Men hva om jeg har utmerket testdekning?

Hvis du har en velprøvd kode, anbefaler jeg deg på din flid, det vil ta deg en lang vei mot å ha en mer robust søknad. Men en god testpakke kan ofte gi en falsk følelse av sikkerhet. Gode ​​tester kan hjelpe deg med reflektor med selvtillit og beskytte deg mot regresjon. Men du kan bare skrive tester for ting du forventer å skje. Bugs er av sin natur uventet. For å bruke vårt tweets eksempel, til vi velger å skrive en test for vår fetch_tweets metode hvor client.user_timeline (håndtak) reiser en feil og tvinger oss til å pakke inn en redde blokkere rundt koden, alle testene våre har blitt grønne og koden vår ville ha vært feilaktig.

Skriftstester, frigjør oss ikke av ansvaret for å gi et kritisk øye over koden vår for å finne ut hvordan denne koden kan bryte. På den annen side kan dette med en slik evaluering definitivt hjelpe oss med å skrive bedre, mer komplette testpakker.


Konklusjon

Motstandsdyktige systemer sprer seg ikke helt ut fra en weekend hack-økt. Å gjøre en applikasjon robust, er en pågående prosess. Du oppdager feil, fikser dem og skriver tester for å sikre at de ikke kommer tilbake. Når søknaden din går ned på grunn av en ekstern systemfeil, isolerer du systemet for å sikre at feilen ikke kan snøball igjen. Unntakshåndtering er din beste venn når det gjelder å gjøre dette. Selv den mest feiltakede applikasjonen kan omdannes til en robust en hvis du bruker god eksepsjonell håndtering praksis konsekvent over tid.

Unntakshåndtering er selvsagt ikke det eneste verktøyet i arsenalet når det gjelder å gjøre applikasjoner mer motstandsdyktige. I etterfølgende artikler vil vi snakke om asynkron behandling, hvordan og når du skal bruke det og hva det kan gjøre når det gjelder å gjøre søknaden din feiltolerant. Vi vil også se på noen distribusjons- og infrastrukturtips som kan ha betydelig innvirkning uten å bryte banken både når det gjelder penger og tid - hold deg innstilt.