ruby on rails - rails - sondeo con delayed_job
sidekiq perform now (6)
Comencemos con la API. Me gustaría tener algo como lo siguiente.
@available.working? # => true or false, so we know it''s running
@available.finished? # => true or false, so we know it''s finished (already ran)
Ahora vamos a escribir el trabajo.
class AwesomeJob < Struct.new(:options)
def perform
do_something_with(options[:var])
end
end
Hasta aquí todo bien. Tenemos un trabajo. Ahora vamos a escribir la lógica que lo enquista. Dado que Available es el modelo responsable de este trabajo, vamos a enseñarle cómo comenzar este trabajo.
class Available < ActiveRecord::Base
def start_working!
Delayed::Job.enqueue(AwesomeJob.new(options))
end
def working?
# not sure what to put here yet
end
def finished?
# not sure what to put here yet
end
end
Entonces, ¿cómo sabemos si el trabajo está funcionando o no? Hay algunas maneras, pero en los rieles parece correcto que cuando mi modelo crea algo, generalmente se asocia con ese algo. ¿Cómo nos asociamos? Usando identificadores en la base de datos. job_id
un job_id
en el modelo Disponible.
Mientras estamos en eso, ¿cómo sabemos que el trabajo no está funcionando porque ya terminó, o porque aún no comenzó? Una forma es verificar realmente qué hizo realmente el trabajo. Si creó un archivo, verifique si el archivo existe. Si calculó un valor, verifique que el resultado esté escrito. Sin embargo, algunos trabajos no son tan fáciles de verificar, ya que es posible que no haya un resultado claro verificable de su trabajo. Para tal caso, puede usar una bandera o una marca de tiempo en su modelo. Suponiendo que este sea nuestro caso, agreguemos una marca de tiempo job_finished_at
para distinguir un trabajo que aún no se ejecutó de uno ya terminado .
class AddJobIdToAvailable < ActiveRecord::Migration
def self.up
add_column :available, :job_id, :integer
add_column :available, :job_finished_at, :datetime
end
def self.down
remove_column :available, :job_id
remove_column :available, :job_finished_at
end
end
Bien. Así que, en realidad, start_working!
Available
con su trabajo tan pronto como pongamos en cola el trabajo, modificando el start_working!
método.
def start_working!
job = Delayed::Job.enqueue(AwesomeJob.new(options))
update_attribute(:job_id, job.id)
end
Estupendo. En este punto, podría haber escrito belongs_to :job
, pero realmente no necesitamos eso.
¿Entonces ahora sabemos cómo escribir el working?
método, tan fácil.
def working?
job_id.present?
end
¿Pero cómo marcamos el trabajo terminado? Nadie sabe que un trabajo ha terminado mejor que el trabajo en sí. Pasemos, por lo tanto, available_id
al trabajo (como una de las opciones) y úselo en el trabajo. ¡Para eso tenemos que modificar el start_working!
método para pasar la identificación.
def start_working!
job = Delayed::Job.enqueue(AwesomeJob.new(options.merge(:available_id => id))
update_attribute(:job_id, job.id)
end
Y deberíamos agregar la lógica en el trabajo para actualizar nuestra job_finished_at
tiempo job_finished_at
cuando esté lista.
class AwesomeJob < Struct.new(:options)
def perform
available = Available.find(options[:available_id])
do_something_with(options[:var])
# Depending on whether you consider an error''ed job to be finished
# you may want to put this under an ensure. This way the job
# will be deemed finished even if it error''ed out.
available.update_attribute(:job_finished_at, Time.current)
end
end
Con este código en su lugar, ¿sabemos cómo escribir nuestro finished?
método.
def finished?
job_finished_at.present?
end
Y terminamos. Ahora podemos simplemente sondear contra @available.working?
y @available.finished?
Además, obtiene la conveniencia de saber qué trabajo exacto se creó para su Disponible al marcar @available.job_id
. Puede convertirlo fácilmente en una asociación real diciendo belongs_to :job
.
Tengo un proceso que generalmente tarda unos segundos en completarse, así que estoy tratando de usar el método de retraso para manejarlo de forma asincrónica. El trabajo en sí funciona bien, mi pregunta es cómo hacer una encuesta sobre el trabajo para saber si está hecho.
Puedo obtener una identificación de delayyed_job simplemente asignándola a una variable:
job = Available.delay.dosomething (: var => 1234)
+------+----------+----------+------------+------------+-------------+-----------+-----------+-----------+------------+-------------+
| id | priority | attempts | handler | last_error | run_at | locked_at | failed_at | locked_by | created_at | updated_at |
+------+----------+----------+------------+------------+-------------+-----------+-----------+-----------+------------+-------------+
| 4037 | 0 | 0 | --- !ru... | | 2011-04-... | | | | 2011-04... | 2011-04-... |
+------+----------+----------+------------+------------+-------------+-----------+-----------+-----------+------------+-------------+
Pero tan pronto como completa el trabajo, lo elimina y la búsqueda del registro completado devuelve un error:
@job=Delayed::Job.find(4037)
ActiveRecord::RecordNotFound: Couldn''t find Delayed::Backend::ActiveRecord::Job with ID=4037
@job= Delayed::Job.exists?(params[:id])
¿Debo molestarme en cambiar esto y quizás posponer la eliminación de registros completos? No estoy seguro de qué otra forma puedo recibir una notificación de su estado. O está sondeando un registro muerto como prueba de que está bien? ¿Alguien más enfrenta algo similar?
Creo que la mejor manera sería usar las devoluciones de llamada disponibles en el trabajo demorado. Estos son:: éxito,: error y: después. para que pueda poner un código en su modelo con el después:
class ToBeDelayed
def perform
# do something
end
def after(job)
# do something
end
end
Porque si insiste en utilizar el método obj.delayed.thethod, tendrá que aplicar el parche monkey Delayed :: PerformableMethod y agregar allí el método after
. En mi humilde opinión, es mucho mejor que la votación por algún valor que incluso podría ser específico para el back-end (ActiveRecord vs. Mongoid, por ejemplo).
El método más simple de lograr esto es cambiar su acción de sondeo para que sea similar a lo siguiente:
def poll
@job = Delayed::Job.find_by_id(params[:job_id])
if @job.nil?
# The job has completed and is no longer in the database.
else
if @job.last_error.nil?
# The job is still in the queue and has not been run.
else
# The job has encountered an error.
end
end
end
¿Por qué funciona esto? Cuando Delayed::Job
ejecuta un trabajo desde la cola, lo elimina de la base de datos si tiene éxito . Si el trabajo falla, el registro permanece en la cola para volver a ejecutarse más tarde y el atributo last_error
se establece en el error encontrado. Con las dos funciones anteriores, puede verificar los registros eliminados para ver si tuvieron éxito.
Los beneficios del método anterior son:
- Obtienes el efecto de encuesta que estabas buscando en tu publicación original
- Usando una rama de lógica simple, puede proporcionar comentarios al usuario si hay un error al procesar el trabajo
Puede encapsular esta funcionalidad en un método modelo haciendo algo como lo siguiente:
# Include this in your initializers somewhere
class Queue < Delayed::Job
def self.status(id)
self.find_by_id(id).nil? ? "success" : (job.last_error.nil? ? "queued" : "failure")
end
end
# Use this method in your poll method like so:
def poll
status = Queue.status(params[:id])
if status == "success"
# Success, notify the user!
elsif status == "failure"
# Failure, notify the user!
end
end
La tabla delayed_jobs en su aplicación está destinada a proporcionar el estado de trabajos en ejecución y puestos en cola solamente. No es una tabla persistente, y realmente debe ser lo más pequeña posible por motivos de rendimiento. Es por eso que los trabajos se eliminan inmediatamente después de la finalización.
En su lugar, debe agregar campo a su modelo Available
que indique que el trabajo está hecho. Como normalmente estoy interesado en cuánto tarda el trabajo en procesarse, agrego los campos start_time y end_time. Entonces mi método de dosomething
sería algo como esto:
def self.dosomething(model_id)
model = Model.find(model_id)
begin
model.start!
# do some long work ...
rescue Exception => e
# ...
ensure
model.finish!
end
end
¡El comienzo! ¡y acaba! los métodos simplemente registran la hora actual y guardan el modelo. Entonces, ¿tendría una completed?
método que su AJAX puede sondear para ver si el trabajo está terminado.
def completed?
return true if start_time and end_time
return false
end
Hay muchas formas de hacerlo, pero considero que este método es simple y funciona bien para mí.
Sugeriría que, si es importante recibir una notificación de que el trabajo se ha completado, escriba un objeto de trabajo personalizado y haga cola en lugar de confiar en el trabajo predeterminado que se pone en cola cuando llama a Available.delay.dosomething
. Crea un objeto como:
class DoSomethingAvailableJob
attr_accessor options
def initialize(options = {})
@options = options
end
def perform
Available.dosomething(@options)
# Do some sort of notification here
# ...
end
end
y enquéue con:
Delayed::Job.enqueue DoSomethingAvailableJob.new(:var => 1234)
Terminé usando una combinación de Delayed_Job con una devolución de llamada posterior a (trabajo) que rellena un objeto memcached con el mismo ID que el trabajo creado. De esta forma, minimizo el número de veces que llegué a la base de datos para conocer el estado del trabajo, en lugar de eso, sondeé el objeto que se ha archivado. Y contiene el objeto completo que necesito del trabajo completo, por lo que ni siquiera tengo una solicitud de ida y vuelta. Obtuve la idea de un artículo de los chicos de Github que hicieron más o menos lo mismo.
https://github.com/blog/467-smart-js-polling
y usó un plugin jquery para el sondeo, que sondea con menos frecuencia, y se da por vencido después de un cierto número de intentos
https://github.com/jeremyw/jquery-smart-poll
Parece que funciona bien.
def after(job)
prices = Room.prices.where("space_id = ? AND bookdate BETWEEN ? AND ?", space_id.to_i, date_from, date_to).to_a
Rails.cache.fetch(job.id) do
bed = Bed.new(:space_id => space_id, :date_from => date_from, :date_to => date_to, :prices => prices)
end
end