Tips for working with Delayed Job
DelayedJob has always been a great “hit the ground running” background task runner for ruby. It is simple to setup, easy to use and can more than carry its own weight in tasks.
Don’t know what a backround runner does or why you would use one?
I will not delve into why you need one but you can check out a summary
here: Background Jobs in Ruby on Rails.
You can sometimes run into some serious trouble trying to debug DelayedJobs. Not only do they run the task behind the scenes but frequently hide the errors and exceptions back there as well. You will often not see exceptions in your logs and most exception notifiers will not catch issues that occur in a background task. So how can we debug issues with delayed job?
If you are using SQL you probably had to setup or run a migration for a delayed job table. Just like any of your other ActiveRecord models DelayedJob has a model you can access. This is your link to the job that gets stored in the queue. This makes it easy for you to access the job from the console and inspect its attributes.
The model is Delayed::Job
.
In the console you can browse the currently queued jobs:
Delayed::Job.count
=> 1
job = Delayed::Job.new
=> #<Delayed::Job id: 1, queue: nil, last_error: nil, failed_at: nil, priority: 0, attempts: 0, updated_at: nil, locked_by: nil, run_at: nil, handler: nil, _type: nil, locked_at: nil, created_at: nil>
Finding Failed Jobs
This is a good start but the default in DelayedJob is for failed jobs to
be removed from the database. Let us change that option. Create the
following file:
config/initializers/delayed_job.rb
.
Delayed::Worker.destroy_failed_jobs = false
We set this option so failed jobs will not be purged from the database.
Now that we have a record of our failed jobs we can access those jobs in
the console. The jobs have a last_error
attribute which contains a
stacktrace from the error.
job = Delayed::Job.where('last_eror IS NOT NULL')
=> #<Delayed::Job id: 51ea79408d53620bd200026c, failed_at: Thu Sep 05 19:50:16 UTC 2013, locked_by: nil, created_at: Sat Jul 20 11:49:20 UTC 2013, handler: handler_ommited, updated_at: Thu Sep 05 19:50:16 UTC 2013, priority: 0, _type: nil, run_at: Fri Aug 09 21:35:56 UTC 2013, queue: nil, locked_at: nil, attempts: 25, last_error: "undefined method `notifiable' for Stacktrace…">
job.last_error
=> "undefined method `notifiable' for
Stacktrace…"
Excellent! Being able to get a stacktrace from the last error can help point you in the right direction.
Loading the Handler
The “Handler” in DelayedJob is the object that #perform
was called on
to execute the job. At the time the job is entered into the queue the
handler is serialized to be reconstructed later. While debugging you may
want access to this handler. You can convert the serailized state of the
object back to its original form by calling YAML::load
and passing in
the serialized object.
job = Delayed::Job.last
job.handler
=> "--- !ruby/object:NotificationObserver::Notifier
\nresource_class: !ruby/class Comment\nresource_id:
!ruby/object:BSON::ObjectId \n data: \n - 82\n - 67\n - 70\n - 67\n
- 183\n - 112\n - 32\n - 225\n - 252\n - 0\n - 0\n - 1\n"
notifier = YAML.load(job.handler)
=> #<NotificationObserver::Notifier:0x1363a7230 @resource_class=Comment, @resource_id=BSON::ObjectId('52434643b77020e1fc000001')>
notifier.class
=> NotificationObserver::Notifier
Working with jobs locally
I find while working with DelayedJob locally my needs can vary. For example:
Sometimes I would prefer the task just not run in the background. That way I can see the task perform and get an immediate response. Other times I might need the task to run in the background as expected so I can develop and test polling methods or execution of code that runs upfront while the background task is still processing.
If your case is the latter then you can run delayed jobs locally and require no additional changes. If your case is the prior and you want to run the code upfront then DelayedJob has a configurable option that will help.
In an initializer you can define the parameter Delayed::Worker#delay_jobs
to tell DelayedJob not
to run a task in the background and to execute the job code immediately.
This will allow you to use any of your regular debugging methods whether
it is using the ruby debugger, pry, or a series or puts statements.
Delayed::Worker.delay_jobs = false
Common problem
The most common problem I see and am asked to help diagnose are issues serializing the handler. Passing complex objects to DelayedJob is not recommended. YAML does its best to serialize all ruby objects but sometimes fails. The simplest solution is not to pass an entire complex object. If it is a persisted, record pass in only the id and load the record from within the job. If you need to save state it could be a good place to create a middle man with the stated attributes and simply pass the middle man into the job.
Have other tips for debugging DelayedJob? I would enjoy hearing about them. Send me an email or a note to @brian_pearce on twitter.