Como desarrolladores, queremos que las aplicaciones que construimos sean resistentes cuando se trata de fallas, pero ¿cómo lograr este objetivo? Si cree que la exageración, los microservicios y un protocolo de comunicación inteligente son la respuesta a todos sus problemas, o tal vez la conmutación por error automática de DNS. Si bien ese tipo de cosas tiene su lugar y lo convierte en una presentación de conferencia interesante, la verdad un poco menos atractiva es que hacer una aplicación robusta comienza con su código. Pero, incluso las aplicaciones bien diseñadas y probadas a menudo carecen de un componente vital de código resistente: manejo de excepciones.
Contenido PatrocinadoEste contenido fue encargado por Engine Yard y fue escrito y / o editado por el equipo de Tuts +. Nuestro objetivo con el contenido patrocinado es publicar tutoriales relevantes y objetivos, estudios de casos y entrevistas inspiradoras que ofrezcan un valor educativo genuino a nuestros lectores y nos permitan financiar la creación de contenido más útil..
Nunca dejé de sorprenderme por cómo el manejo de excepciones subutilizado tiende a ser incluso dentro de las bases de código maduras. Veamos un ejemplo.
Supongamos que tenemos una aplicación Rails, y una de las cosas que podemos hacer con esta aplicación es obtener una lista de los últimos tweets para un usuario, dado su manejo. Nuestro TweetsController
podría verse así:
clase 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
Y el Persona
El modelo que utilizamos podría ser similar al siguiente:
clase persona < 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
Este código parece perfectamente razonable, hay docenas de aplicaciones que tienen un código como este en producción, pero veamos un poco más de cerca.
find_or_create_by
es un método de Rails, no es un método 'bang', por lo que no debería lanzar excepciones, pero si miramos la documentación podemos ver que debido a la forma en que funciona este método, puede generar una ActiveRecord :: RecordNotUnique
error. Esto no sucederá a menudo, pero si nuestra aplicación tiene una cantidad de tráfico decente, es más probable de lo que podría esperar (lo he visto muchas veces).find_or_create_by
lanzar cualquier tipo de error inesperado en cualquier momento (un buen nivel de paranoia es algo bueno cuando se trata de construir software robusto). Si no tenemos una forma global de manejar errores inesperados (veremos esto más adelante), es posible que queramos manejarlos individualmente..person.fetch_tweets
que crea una instancia de un cliente de Twitter y trata de obtener algunos tweets. Esta será una llamada de red y es propensa a todo tipo de fallas. Es posible que queramos leer la documentación para averiguar cuáles son los posibles errores que podemos esperar, pero sabemos que los errores no solo son posibles aquí, sino que también son bastante probables (por ejemplo, la API de Twitter puede estar inactiva, una persona con ese control podría no existe etc.). No poner alguna lógica de manejo de excepciones en las llamadas de red es un problema.Nuestra pequeña cantidad de código tiene algunos problemas serios, intentemos mejorarlo.
Envolveremos nuestro find_or_create_by
y empujarlo hacia abajo en el Persona
modelo:
clase persona < 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 "Se encontró un error al intentar encontrar o crear una Persona para: # handle, # e.message # e.backtrace.join (" \ n ")" nil end end final fin
Hemos manejado el ActiveRecord :: RecordNotUnique
de acuerdo con la documentación y ahora sabemos a ciencia cierta que obtendremos una Persona
objeto o nulo
Si algo va mal. Este código ahora es sólido, pero ¿qué hay de recuperar nuestros tweets?
clase persona < ActiveRecord::Base def fetch_tweets client.user_timeline(handle).map|tweet| tweet.text rescue => e Rails.logger.error "Error al obtener tweets para: # handle, # e.message # e.backtrace.join (" \ n ")" nil end private def client @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_secret end.
Impulsamos la creación de instancias del cliente de Twitter en su propio método privado y, como no sabíamos qué podía salir mal cuando recuperábamos los tweets, rescatábamos todo..
Es posible que hayas escuchado en algún lugar que siempre debes detectar errores específicos. Este es un objetivo loable, pero la gente a menudo lo malinterpreta como "si no puedo captar algo específico, no captaré nada". En realidad, si no puedes capturar algo específico, ¡debes atrapar todo! De esta manera, al menos, tiene la oportunidad de hacer algo, incluso si solo es para registrar y volver a generar el error.
Para hacer nuestro código más robusto, nos vimos obligados a refactorizar y ahora nuestro código es posiblemente mejor que antes. Puede utilizar su deseo de un código más resistente para informar sus decisiones de diseño.
Cada vez que agrega alguna lógica de manejo de excepciones a un método, también es una ruta adicional a través de ese método y debe ser probada. Es vital que pruebes el camino excepcional, tal vez más que probar el camino feliz. Si algo sale mal en el camino feliz, ahora tiene el seguro adicional del rescate
bloque para evitar que su aplicación se caiga. Sin embargo, cualquier lógica dentro del bloque de rescate en sí no tiene tal seguro. Prueba tu ruta excepcional bien, para que cosas tontas como escribir mal el nombre de una variable dentro de la rescate
el bloqueo no hace que su aplicación explote (esto me ha sucedido tantas veces, en serio, solo pruebe su rescate
bloques).
He visto este tipo de código innumerables veces a través de los años:
comenzar widgetron.create rescate # no es necesario que haga nada al final
Rescatamos una excepción y no hacemos nada con ella. Esto es casi siempre una mala idea. Cuando esté depurando un problema de producción dentro de seis meses, tratando de averiguar por qué su 'widgetron' no se muestra en la base de datos, no recordará ese inocente comentario y horas de frustración..
¡No trague excepciones! Como mínimo, debe registrar cualquier excepción que detecte, por ejemplo:
begin foo.bar rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" end
De esta manera podemos rastrear los registros y tendremos la causa y el seguimiento de la pila del error para analizar.
Mejor aún, puedes usar un servicio de monitoreo de errores, como Rollbar, que es bastante bueno. Hay muchas ventajas para esto:
comenzar foo.bar rescate => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" Rollbar.report_exception (e) end
Por supuesto, puede registrar y usar un servicio de monitoreo como se indica arriba.
Si tu rescate
bloque es lo último en un método, recomiendo tener un retorno explícito:
def my_method begin foo.bar rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" Rollbar.report_exception (e) nil end end
Puede que no siempre quieras volver nulo
, a veces puede que esté mejor con un objeto nulo o cualquier otra cosa que tenga sentido en el contexto de su aplicación. El uso constante de valores de retorno explícitos ahorrará a todos mucha confusión.
También puede volver a elevar el mismo error o generar otro diferente dentro de su rescate
bloquear. Un patrón que a menudo encuentro útil es envolver la excepción existente en una nueva y elevarla para no perder el rastro de pila original (incluso escribí una joya para esto ya que Ruby no proporciona esta funcionalidad de forma inmediata). ). Más adelante en el artículo, cuando hablemos de servicios externos, le mostraré por qué esto puede ser útil..
Rails le permite especificar cómo manejar las solicitudes de recursos de un determinado formato (HTML, XML, JSON) usando responder a
y responder con
. Raramente veo aplicaciones que usan correctamente esta funcionalidad, después de todo si no usas un responder a
bloquear todo funciona bien y Rails renderiza tu plantilla correctamente. Golpeamos nuestro controlador de tweets a través de. / tweets / yukihiro_matz
y obtén una página HTML llena de los últimos tweets de Matzs. Lo que la gente suele olvidar es que es muy fácil intentar y solicitar un formato diferente del mismo recurso, por ejemplo,. /tweets/yukihiro_matz.json
. En este punto, Rails intentará con valentía devolver una representación JSON de los tweets de Matzs, pero no irá bien ya que la vista no existe. Un ActionView :: MissingTemplate
el error se levantará y nuestra aplicación explotará de una manera espectacular. Y JSON es un formato legítimo, en una aplicación de alto tráfico es probable que obtenga una solicitud de /tweets/yukihiro_matz.foobar
. Tuts + recibe este tipo de solicitudes todo el tiempo (probablemente de los robots que intentan ser inteligentes).
La lección es esta: si no planea devolver una respuesta legítima para un formato particular, evite que sus controladores intenten cumplir con las solicitudes de esos formatos. En el caso de nuestro TweetsController
:
clase TweetsController < ApplicationController respond_to :html def show… respond_to do |format| format.html end end end
Ahora cuando recibamos solicitudes de formatos espurios obtendremos una más relevante. ActionController :: UnknownFormat
error. Nuestros controladores se sienten un poco más apretados, lo cual es una gran cosa cuando se trata de hacerlos más robustos..
El problema que tenemos ahora es que, a pesar de nuestro error semánticamente agradable, nuestra aplicación todavía está explotando en la cara de nuestros usuarios. Aquí es donde entra en juego el manejo de excepciones globales. A veces nuestra aplicación producirá errores a los que queremos responder de manera constante, sin importar de dónde provengan (como nuestro ActionController :: UnknownFormat
). También hay errores que el marco puede generar antes de que cualquiera de nuestros códigos entre en juego. Un ejemplo perfecto de esto es ActionController :: RoutingError
. Cuando alguien solicita una URL que no existe, como / tweets2 / yukihiro_matz
, No hay ningún lugar donde podamos conectarnos para rescatar este error, usando el manejo tradicional de excepciones. Aquí es donde Rails ' excepciones_app
viene en.
Puede configurar una aplicación Rack en aplicacion.rb
para ser llamado cuando se produce un error que no hemos manejado (como nuestro ActionController :: RoutingError
o ActionController :: UnknownFormat
). La forma en que normalmente verá este uso es configurar su aplicación de rutas como excepciones_app
, luego, defina las distintas rutas para los errores que desea manejar y diríjalos a un controlador de errores especiales que cree. Entonces nuestro aplicacion.rb
se vería así:
... config.exceptions_app = self.routes ...
Nuestro rutas.rb
entonces contendrá lo siguiente:
… Coincide '/ 404' => 'errores # not_found', a través de:: todos coinciden '/ 406' => 'errores # not_acceptable', vía:: todos coinciden '/ 500' => 'errores # internal_server_error', vía: :todos…
En este caso nuestro ActionController :: RoutingError
sería recogido por el 404
ruta y el ActionController :: UnknownFormat
será recogido por el 406
ruta. Hay muchos errores posibles que pueden surgir. Pero mientras manejas los comunes (404
, 500
, 422
Para comenzar, puedes agregar otros si y cuando suceden..
Dentro de nuestro controlador de errores, ahora podemos representar las plantillas relevantes para cada tipo de error junto con nuestro diseño (si no es un 500) para mantener la marca. También podemos registrar los errores y enviarlos a nuestro servicio de monitoreo, aunque la mayoría de los servicios de monitoreo se conectarán a este proceso automáticamente para que usted no tenga que enviar los errores usted mismo. Ahora, cuando nuestra aplicación explota, lo hace con suavidad, con el código de estado correcto en función del error y una página en la que podemos darle al usuario una idea de lo que sucedió y lo que puede hacer (soporte de contacto): una experiencia infinitamente mejor. Más importante aún, nuestra aplicación parecerá (y en realidad será) mucho más sólida.
En cualquier controlador de Rails podemos definir errores específicos para ser manejados globalmente dentro de ese controlador (sin importar en qué acción se produzcan), lo hacemos a través de rescue_from. La pregunta es cuándo usar. rescatado de
? Por lo general, me parece que un buen patrón es usarlo para los errores que pueden ocurrir en varias acciones (por ejemplo, el mismo error en más de una acción). Si un error solo será producido por una acción, manéjelo a través de la comenzar ... rescatar ... terminar
mecanismo, pero si es probable que obtengamos el mismo error en varios lugares y queramos manejarlo de la misma manera, es un buen candidato para un rescatado de
. Digamos nuestro TweetsController
también tiene un crear
acción:
clase TweetsController < ApplicationController respond_to :html def show… respond_to do |format| format.html end end def create… end end
Digamos también que ambas acciones pueden encontrar un TwitterError
y si lo hacen, queremos decirle al usuario que algo anda mal con Twitter. Aquí es donde rescatado de
puede ser realmente útil:
clase TweetsController < ApplicationController respond_to :html rescue_from TwitterError, with: twitter_error private def twitter_error render :twitter_error end end
Ahora no tenemos que preocuparnos por manejar esto en nuestras acciones, se verán mucho más limpias y podemos / debemos - por supuesto - registrar nuestro error y / o notificar a nuestro servicio de monitoreo de errores dentro de twitter_error
método. Si utiliza rescatado de
correctamente, no solo puede ayudarlo a hacer su aplicación más robusta, sino que también puede hacer que su código de controlador sea más limpio. Esto hará que sea más fácil mantener y probar su código, haciendo que su aplicación sea un poco más resistente una vez más.
Es difícil escribir una aplicación importante en estos días sin usar una cantidad de servicios / API externos. En el caso de nuestro TweetsController
, Twitter entró en juego a través de una gema de Ruby que envuelve la API de Twitter. Lo ideal sería que hiciéramos todas nuestras llamadas API externas de forma asíncrona, pero no estamos cubriendo el procesamiento asíncrono en este artículo y hay muchas aplicaciones que hacen al menos algunas llamadas de API / red en proceso.
Hacer llamadas de red es una tarea extremadamente propensa a errores y un buen manejo de excepciones es una necesidad. Puede obtener errores de autenticación, problemas de configuración y errores de conectividad. La biblioteca que utiliza puede producir cualquier número de errores de código y luego hay una cuestión de conexiones lentas. Me estoy refiriendo a este punto, pero es tan crucial, ya que no puede manejar conexiones lentas a través del manejo de excepciones. Debe configurar adecuadamente los tiempos de espera en la biblioteca de su red, o si está utilizando un envoltorio de API, asegúrese de que proporciona enlaces para configurar los tiempos de espera. No hay peor experiencia para un usuario que tener que estar sentado allí esperando sin que su aplicación le dé alguna indicación de lo que está sucediendo. Casi todos se olvidan de configurar los tiempos de espera de forma adecuada (lo sé), así que presta atención.
Si está utilizando un servicio externo en múltiples lugares dentro de su aplicación (por ejemplo, múltiples modelos), expone grandes partes de su aplicación a todo el panorama de errores que pueden producirse. Esta no es una buena situacion. Lo que queremos hacer es limitar nuestra exposición y una forma en que podemos hacerlo es poner todo el acceso a nuestros servicios externos detrás de una fachada, rescatar todos los errores allí y volver a generar un error semánticamente apropiado (elevar eso TwitterError
de los que hablamos si se producen errores cuando intentamos acceder a la API de Twitter). Entonces podemos usar fácilmente técnicas como rescatado de
para hacer frente a estos errores y no exponemos grandes partes de nuestra aplicación a un número desconocido de errores de fuentes externas.
Una idea aún mejor sería hacer de su fachada una API libre de errores. Devuelva todas las respuestas exitosas tal como están y devuelva objetos nulos o nulos cuando recupere cualquier tipo de error (todavía necesitamos registrar / notificarnos a nosotros mismos de los errores a través de algunos de los métodos que analizamos anteriormente). De esta manera, no necesitamos mezclar diferentes tipos de flujo de control (flujo de control de excepción frente a ... más), lo que puede hacer que el código sea mucho más limpio. Por ejemplo, vamos a envolver nuestro acceso a la API de Twitter en un TwitterClient
objeto:
clase TwitterClient attr_reader: cliente 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_acceso_acceso_acceso_acceso_acceso_es_es_es_es_es_es_configuración_asken_acceso_acceso_acceso_es_es_es_confensa_acceso_acceso_acceso_acceso_es_es_es_configuración_asken_acceso_acceso_acceso_acceso_acceso_es_es_es_es_configuración_acceso_acceso_acceso_acceso_acceso_acceso_acceso_acceso_es_es_es_es_es_configuración_acceso_acceso_contratos mapa | tweet | tweet.text rescue => e Rails.logger.error "# e.message # e.backtrace.join (" \ n ")" nil end end
Ahora podemos hacer esto: TwitterClient.new.latest_tweets ('yukihiro_matz')
, En cualquier parte de nuestro código, sabemos que nunca producirá un error o, más bien, nunca propagará el error más allá de TwitterClient
. Hemos aislado un sistema externo para asegurarnos de que las fallas en ese sistema no afecten nuestra aplicación principal.
Si tiene un código bien probado, lo felicito por su diligencia, le llevará un largo camino para tener una aplicación más robusta. Pero un buen conjunto de pruebas a menudo puede proporcionar una falsa sensación de seguridad. Las buenas pruebas pueden ayudarlo a refactorizar con confianza y protegerlo contra la regresión. Pero, solo puedes escribir pruebas para las cosas que esperas que ocurran. Los insectos son, por su propia naturaleza, inesperados. Para usar nuestro ejemplo de tweets, hasta que decidamos escribir una prueba para nuestro fetch_tweets
método donde client.user_timeline (handle)
plantea un error que nos obliga a envolver una rescate
bloque alrededor del código, todas nuestras pruebas habrán sido verdes y nuestro código habría permanecido propenso a fallas.
Escribir pruebas, no nos exime de la responsabilidad de echar un vistazo crítico sobre nuestro código para descubrir cómo este código puede potencialmente romperse. Por otro lado, hacer este tipo de evaluación definitivamente puede ayudarnos a escribir conjuntos de pruebas mejores y más completos..
Los sistemas resistentes no surgen completamente formados de una sesión de hackeo de fin de semana. Hacer una aplicación robusta, es un proceso continuo. Descubres errores, los arreglas y escribes pruebas para asegurarte de que no vuelvan. Cuando su aplicación falla debido a una falla del sistema externo, usted aísla ese sistema para asegurarse de que la falla no pueda volver a aparecer. El manejo de excepciones es tu mejor amigo cuando se trata de hacer esto. Incluso la aplicación más propensa a fallar puede convertirse en una robusta si aplica buenas prácticas de manejo de excepciones de manera consistente, a lo largo del tiempo.
Por supuesto, el manejo de excepciones no es la única herramienta en su arsenal cuando se trata de hacer que las aplicaciones sean más resistentes. En artículos posteriores hablaremos sobre el procesamiento asíncrono, cómo y cuándo aplicarlo y lo que puede hacer para que su aplicación sea tolerante a fallos. También veremos algunos consejos de implementación e infraestructura que pueden tener un impacto significativo sin romper el banco en términos de dinero y tiempo. Estén atentos.