ruby-on-rails ruby exception open-uri

ruby on rails - ¿Cómo debe mi raspadura "pila" manejar 404 errores?



ruby-on-rails exception (8)

TL; DR

Utilice el manejo de errores fuera de banda y un modelo de raspado conceptual diferente para acelerar las operaciones.

Las excepciones no son para condiciones comunes

Hay una serie de otras respuestas que tratan cómo manejar las excepciones para su caso de uso. Estoy tomando un enfoque diferente diciendo que el manejo de las excepciones es fundamentalmente el enfoque equivocado aquí por varias razones.

  1. En su libro Exceptional Ruby , Avdi Grimm proporciona algunos puntos de referencia que muestran el rendimiento de las excepciones como un ~ 156% más lento que el uso de técnicas de codificación alternativas, como las devoluciones tempranas.

  2. En El programador pragmático: de Journeyman a Master , los autores declaran que "[x] las excepciones deben reservarse para eventos inesperados". En su caso, los errores 404 son indeseables, pero no son del todo inesperados. De hecho, ¡el manejo de los errores 404 es una consideración fundamental!

En resumen, necesitas un enfoque diferente. Preferiblemente, el enfoque alternativo debería proporcionar un manejo de errores fuera de banda y evitar que su proceso se bloquee en los reintentos.

Una alternativa: un proceso más rápido y atómico

Tiene muchas opciones aquí, pero la que recomendaré es manejar los códigos de estado 404 como un resultado normal. Esto le permite "fallar rápido", pero también le permite reintentar páginas o eliminar direcciones URL de su cola en un momento posterior.

Considera este esquema de ejemplo:

ActiveRecord::Schema.define(:version => 20120718124422) do create_table "webcrawls", :force => true do |t| t.text "raw_html" t.integer "retries" t.integer "status_code" t.text "parsed_data" t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end end

La idea aquí es que simplemente trataría todo el rasguño como un proceso atómico. Por ejemplo:

  • ¿Recibiste la página?

    Genial, almacene la página en bruto y el código de estado exitoso. Incluso puede analizar el HTML sin procesar más tarde, para completar sus rasguños lo más rápido posible.

  • ¿Recibiste un 404?

    Bien, almacene la página de error y el código de estado. ¡Muévete rápido!

Cuando su proceso haya terminado de rastrear las URL, puede usar una búsqueda de ActiveRecord para encontrar todas las URL que recientemente devolvieron un estado 404 para que pueda tomar la acción apropiada. Quizás desee volver a intentar la página, registrar un mensaje o simplemente eliminar la URL de su lista de URL para raspar: la "acción apropiada" depende de usted.

Al realizar un seguimiento de sus recuentos de reintentos, incluso podría diferenciar entre errores transitorios y errores más permanentes. Esto le permite establecer umbrales para diferentes acciones, dependiendo de la frecuencia de fallas de raspado para una URL determinada.

Este enfoque también tiene el beneficio adicional de aprovechar la base de datos para administrar escrituras concurrentes y compartir resultados entre procesos. Esto le permitiría repartir el trabajo (quizás con una cola de mensajes o archivos de datos fragmentados) entre múltiples sistemas o procesos.

Pensamientos finales: ampliar y mejorar

Dedicar menos tiempo a los reintentos o al manejo de errores durante el raspado inicial debería acelerar su proceso significativamente. Sin embargo, algunas tareas son demasiado grandes para un enfoque de una sola máquina o de un solo proceso. Si la aceleración del proceso sigue siendo insuficiente para sus necesidades, es posible que desee considerar un enfoque menos lineal utilizando uno o más de los siguientes:

  • Procesos de fondo de bifurcación.
  • Uso de dRuby para dividir el trabajo entre múltiples procesos o máquinas.
  • Maximizando el uso del núcleo al generar múltiples procesos externos utilizando GNU en paralelo .
  • Algo más que no sea un proceso monolítico, secuencial.

La optimización de la lógica de la aplicación debería ser suficiente para el caso común, pero si no, escalar a más procesos o a más servidores. Sin duda, la ampliación será más laboriosa, pero también ampliará las opciones de procesamiento disponibles para usted.

Tengo una tarea de rake que es responsable de hacer el procesamiento por lotes en millones de URL. Debido a que este proceso tarda tanto, a veces encuentro que las URL que intento procesar ya no son válidas: 404s, sitio inactivo, lo que sea.

Cuando escribí esto inicialmente, básicamente había un solo sitio que se iría apagando continuamente mientras se procesaba, por lo que mi solución fue usar open-uri , rescatar cualquier excepción producida, esperar un poco y luego volver a intentarlo.

Esto funcionó bien cuando el conjunto de datos era más pequeño, pero ahora pasa tanto tiempo que estoy encontrando que las URL ya no están allí y producen un 404.

Usando el caso de un 404, cuando esto sucede, mi script se queda ahí y hace un bucle infinito, obviamente malo.

¿Cómo debo manejar los casos en que una página no se carga correctamente y, lo que es más importante, cómo encaja esto en la "pila" que he construido?

Soy bastante nuevo en esto, y Rails, ¡por lo que cualquier opinión sobre dónde podría haber salido mal en este diseño es bienvenida!

Aquí hay un código anónimo que muestra lo que tengo:

La tarea de rake que realiza una llamada a MyHelperModule:

# lib/tasks/my_app_tasks.rake namespace :my_app do desc "Batch processes some stuff @ a later time." task :process_the_batch => :environment do # The dataset being processed # is millions of rows so this is a big job # and should be done in batches! MyModel.where(some_thing: nil).find_in_batches do |my_models| MyHelperModule.do_the_process my_models: my_models end end end end

MyHelperModule acepta my_models y hace más cosas con ActiveRecord. Se llama SomeClass :

# lib/my_helper_module.rb module MyHelperModule def self.do_the_process(args = {}) my_models = args[:my_models] # Parallel.each(my_models, :in_processes => 5) do |my_model| my_models.each do |my_model| # Reconnect to prevent errors with Postgres ActiveRecord::Base.connection.reconnect! # Do some active record stuff some_var = SomeClass.new(my_model.id) # Do something super interesting, # fun, # AND sexy with my_model end end end

SomeClass saldrá a la web a través de WebpageHelper y procesará una página:

# lib/some_class.rb require_relative ''webpage_helper'' class SomeClass attr_accessor :some_data def initialize(arg) doc = WebpageHelper.get_doc("http://somesite.com/#{arg}") # do more stuff end end

WebpageHelper es donde se captura la excepción y se inicia un bucle infinito en el caso de 404:

# lib/webpage_helper.rb require ''nokogiri'' require ''open-uri'' class WebpageHelper def self.get_doc(url) begin page_content = open(url).read # do more stuff rescue Exception => ex puts "Failed at #{Time.now}" puts "Error: #{ex}" puts "URL: " + url puts "Retrying... Attempt #: #{attempts.to_s}" attempts = attempts + 1 sleep(10) retry end end end


Con respecto al problema que está experimentando, puede hacer lo siguiente:

class WebpageHelper def self.get_doc(url) retried = false begin page_content = open(url).read # do more stuff rescue OpenURI::HTTPError => ex unless ex.io.status.first.to_i == 404 log_error ex.message sleep(10) unless retried retried = true retry end end # FIXME: needs some refactoring rescue Exception => ex puts "Failed at #{Time.now}" puts "Error: #{ex}" puts "URL: " + url puts "Retrying... Attempt #: #{attempts.to_s}" attempts = attempts + 1 sleep(10) retry end end end

Pero reescribí todo para hacer un procesamiento paralelo con Typhoeus:

https://github.com/typhoeus/typhoeus

donde asignaría un bloque de devolución de llamada que haría el manejo de los datos devueltos, así desacoplando la búsqueda de la página y el procesamiento.

Algo a lo largo de las líneas:

def on_complete(response) end def on_failure(response) end def run hydra = Typhoeus::Hydra.new reqs = urls.collect do |url| Typhoeus::Request.new(url).tap { |req| req.on_complete = method(:on_complete).to_proc } hydra.queue(req) } end hydra.run # do something with all requests after all requests were performed, if needed end


Creo que los comentarios de todos sobre esta pregunta son acertados y correctos. Hay mucha información buena en esta página. Aquí está mi intento de recoger esta generosa recompensa. Dicho esto +1 a todas las respuestas.

Si solo le preocupa 404 con OpenURI, puede manejar solo esos tipos de excepciones

# lib/webpage_helper.rb rescue OpenURI::HTTPError => ex # handle OpenURI HTTP Error! rescue Exception => e # similar to the original case e.message when /404/ then puts ''404!'' when /500/ then puts ''500!'' # etc ... end end

Si desea un poco más, puede hacer un manejo diferente de Execption por tipo de error.

# lib/webpage_helper.rb rescue OpenURI::HTTPError => ex # do OpenURI HTTP ERRORS rescue Exception::SyntaxError => ex # do Syntax Errors rescue Exception => ex # do what we were doing before

También me gusta lo que se dice en las otras publicaciones sobre el número de intentos. Se asegura de que no sea un bucle infinito.

Creo que lo que hay que hacer después de varios intentos sería registrar, poner en cola o enviar un correo electrónico.

Para iniciar sesión puedes usar

webpage_logger = Log4r::Logger.new("webpage_helper_logger") # somewhere later # ie 404 case e.message when /404/ then webpage_logger.debug "debug level error #{attempts.to_s}" webpage_logger.info "info level error #{attempts.to_s}" webpage_logger.fatal "fatal level error #{attempts.to_s}"

Hay muchas formas de hacer cola. Creo que algunos de los mejores son faye y resque. Aquí hay un enlace a ambos: http://faye.jcoglan.com/ https://github.com/defunkt/resque/

Las colas funcionan como una línea. Lo creas o no las líneas de llamada de los británicos, "colas" (cuanto más sabes). Por lo tanto, al usar un servidor de cola, puede alinear muchas solicitudes y cuando el servidor al que está intentando enviar la solicitud vuelve, puede agrupar ese servidor con sus solicitudes en la cola. Por lo tanto, obligando a su servidor a fallar nuevamente, pero con el tiempo, con el tiempo, actualizarán sus máquinas porque siguen fallando.

Y, finalmente, al correo electrónico, los rieles también al rescate (no resque) ... Aquí está el enlace a la guía de rieles en ActionMailer: http://guides.rubyonrails.org/action_mailer_basics.html

Usted podría tener un anuncio de correo como este

class SomeClassMailer < ActionMailer::Base default :from => "[email protected]" def self.mail(*args) ... # then later rescue Exception => e case e.message when /404/ && attempts == 3 SomeClassMailer.mail(:to => "[email protected]", :subject => "Failure ! #{attempts}")


De hecho, tengo una tarea de rake que hace algo muy similar. Aquí está la esencia de lo que hice para lidiar con los 404 y podría aplicarlo con bastante facilidad.

Básicamente, lo que desea hacer es usar el siguiente código como filtro y crear un archivo de registro para almacenar sus errores. Por lo tanto, antes de tomar el sitio web y procesarlo, primero debe hacer lo siguiente:

Así que crea / crea un archivo de registro en tu archivo:

@logfile = File.open("404_log_#{Time.now.strftime("%m/%d/%Y")}.txt","w") # #{Time.now.strftime("%m/%d/%Y")} Just includes the date into the log in case you want # to run diffs on your log files.

Luego cambia tu clase de WebpageHelper a algo como esto:

class WebpageHelper def self.get_doc(url) response = Net::HTTP.get_response(URI.parse(url)) if (response.code.to_i == 404) notify_me(url) else page_content = open(url).read # do more stuff end end end

Lo que está haciendo es hacer ping a la página para obtener un código de respuesta. La instrucción if que incluí está verificando si el código de respuesta es un 404 y si se ejecuta el método notify_me, de lo contrario, ejecute sus comandos como de costumbre. Acabo de crear arbitrariamente ese método notify_me como ejemplo. En mi sistema, lo tengo escrito en un archivo txt que me envía por correo electrónico una vez finalizado. Podría usar un método similar para ver otros códigos de respuesta.

Método de registro genérico:

def notify_me(url) puts "Failed at #{Time.now}" puts "URL: " + url @logfile.puts("There was a 404 error for the site #{url} at #{Time.now}.") end


En lugar de usar initialize, que siempre devuelve una nueva instancia de un objeto, al crear una SomeClass nueva a partir de un raspado, usaría un método de clase para crear la instancia . No estoy usando excepciones aquí más allá de lo que nokogiri está lanzando porque suena como que nada más debería aumentar aún más, ya que solo quieres que se registren, pero de lo contrario se ignorará. Usted mencionó el registro de las excepciones. ¿Está simplemente registrando lo que va a la salida estándar? Te responderé como si fueras ...

# lib/my_helper_module.rb module MyHelperModule def self.do_the_process(args = {}) my_models = args[:my_models] # Parallel.each(my_models, :in_processes => 5) do |my_model| my_models.each do |my_model| # Reconnect to prevent errors with Postgres ActiveRecord::Base.connection.reconnect! some_object = SomeClass.create_from_scrape(my_model.id) if some_object # Do something super interesting if you were able to get a scraping # otherwise nothing happens (except it is noted in our logging elsewhere) end end end

Su SomeClass:

# lib/some_class.rb require_relative ''webpage_helper'' class SomeClass attr_accessor :some_data def initialize(doc) @doc = doc end # could shorten this, but you get the idea... def self.create_from_scrape(arg) doc = WebpageHelper.get_doc("http://somesite.com/#{arg}") if doc return SomeClass.new(doc) else return nil end end end

Tu WebPageHelper:

# lib/webpage_helper.rb require ''nokogiri'' require ''open-uri'' class WebpageHelper def self.get_doc(url) attempts = 0 # define attempts first in non-block local scope before using it begin page_content = open(url).read # do more stuff rescue Exception => ex attempts += 1 puts "Failed at #{Time.now}" puts "Error: #{ex}" puts "URL: " + url if attempts < 3 puts "Retrying... Attempt #: #{attempts.to_s}" sleep(10) retry else return nil end end end end


Podrías subir los 404:

rescue Exception => ex raise ex if ex.message[''404''] # retry for non-404s end


Todo depende de lo que quieras hacer con los 404.

Asumamos que solo quieres tragarlos. Parte de la respuesta de pguardiario es un buen comienzo: puede generar un error y volver a intentarlo varias veces ...

# lib/webpage_helper.rb require ''nokogiri'' require ''open-uri'' class WebpageHelper def self.get_doc(url) attempt_number = 0 begin attempt_number = attempt_number + 1 page_content = open(url).read # do more stuff rescue Exception => ex puts "Failed at #{Time.now}" puts "Error: #{ex}" puts "URL: " + url puts "Retrying... Attempt #: #{attempts.to_s}" sleep(10) retry if attempt_number < 10 # Try ten times. end end end

Si siguieras este patrón, simplemente fallaría en silencio. Nada pasaría, y seguiría adelante después de diez intentos. En general, consideraría que un mal plan (tm). En lugar de simplemente fallar en silencio, buscaría algo como esto en la cláusula de rescate:

rescue Exception => ex if attempt_number < 10 # Try ten times. retry else raise "Unable to contact #{url} after ten tries." end end

y luego lance algo como esto en MyHelperModule # do_the_process (tendría que actualizar su base de datos para tener una columna de errores y mensaje de error):

my_models.each do |my_model| # ... cut ... begin some_var = SomeClass.new(my_model.id) rescue Exception => e my_model.update_attributes(errors: true, error_message: e.message) next end # ... cut ... end

Esa es probablemente la forma más fácil y elegante de hacerlo con lo que tienes actualmente. Dicho esto, si estás manejando tantas solicitudes en una tarea de rake masivo, eso no es muy elegante. No puede reiniciarlo si algo sale mal, está atando un solo proceso en su sistema durante mucho tiempo, etc. Si termina con cualquier pérdida de memoria (o bucles infinitos), se encontrará en un lugar donde no puedo simplemente decir ''seguir adelante''. Probablemente deberías usar algún tipo de sistema de colas como Resque o Sidekiq, o Trabajo demorado (aunque parece que tienes más elementos que terminarías poniendo en cola de lo que el Trabajo demorado manejaría con gusto). Recomiendo profundizar en ellos si buscas un enfoque más elocuente.


Curb tiene una forma más sencilla de hacerlo y puede ser una opción mejor (y más rápida) en lugar de open-uri .

Los informes de errores de acera (y que puedes rescatar y hacer algo)

http://curb.rubyforge.org/classes/Curl/Err.html

Joya de bordillo: https://github.com/taf2/curb

Código de muestra:

def browse(url) c = Curl::Easy.new(url) begin c.connect_timeout = 3 c.perform return c.body_str rescue Curl::Err::NotFoundError handle_not_found_error(url) end end def handle_not_found_error(url) puts "This is a 404!" end