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.
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.
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