I'm using Rails 6 in my app with Sidekiq on board. I've got FetchAllProductsWorker like below:
module Imports
class FetchAllProductsWorker
include Sidekiq::Worker
sidekiq_options queue: 'imports_fetch_all', retry: 0
def perform
(...)
end
end
end
I want to check when FetchAllProductsWorker was finished successfully last time and display this info in my front-end. This job will be fired sporadically but the user must have feedback when the last database sync (FetchAllProductsWorker is responsible for that) succeeded.
I want to have such info only for this one worker. I saw a lot of useful things inside the sidekiq API docs but none of them relate to the history of completed jobs.
You could use the Sidekiq Batches API that provides an on_success callback but that is mostly used for tracking batch work which is an overkill for your problem. I suggest writing your code at the end of the perform function.
def perform
(...) # Run the already implemented code
(notify/log for success) # If it is successful notify/log.
end
The simplified default lifecycle of a Sidekiq looks like this:
If there is an error then the job will be retried a couple of times (read about Retries in the Sidekiq docs). During that time you can see the failing job and the error on the Sidekiq Web UI if configured.
– If the job is finished successfully the job is removed from Redis and there is no information about this specific job available to the application.
That means Sidekiq does not really support running queries about jobs that run successfully in the past. If you need information about past jobs then you need to build this on its own. I basically see three options to allow monitoring Sidekiq Jobs:
Write useful information to your application's log. Most logging tools support monitoring for specific messages, sending messages, or creating views for specific events. This might be enough if you just need this information for debugging reasons.
def perform
Rails.logger.info("#{self.class.name}" started)
begin
# job code
rescue => exception
Rails.logger.error("#{self.class.name} failed: #{exception.message}")
raise # re-raise the exception to trigger Sidekiq's default retry behavior
else
Rails.logger.info("#{self.class.name}" was finished successfully)
end
end
If you are mostly interested in getting informed when there is a problem then I suggest looking at a tool like Dead Man's Snitch. how those tools are working is that you ping their API as the last step of a job which will only reach when there was no error. Then configure that tool to notify you if its API hasn't been pinged in the expected timeframe, for example, if you have a daily import job, then Dead Man's Snitch would send you a message only if there wasn't a successful import Job in the last 24 hours. If the job was successful it will not spam you every single day.
require 'open-uri'
def perform
# job code
open("https://nosnch.in/#{TOKEN}")
end
If you want to allow your application's users to see job return statuses on a dashboard in the application. Then it makes sense to store that information in the database. You could, for example, just create a JobStatus ActiveRecord model with columns like job_name, status, payload, and a created_at and then create records in that table whenever it feels useful. Once the data is in the database you can present that data like every other model's data to the user.
def perform
begin
# job code
rescue => exception
JobStatus.create(job_name: self.class.name, status: 'failed', payload: exception.to_json)
raise # re-raise the exception to trigger Sidekiq's default retry behavior
else
JobStatus.create(job_name: self.class.name, status: 'success')
end
end
And, of course, you can combine all those technics and tools for different use-cases. 1. for history and statistics, 2. for admins and people being on-call, 3. for users of your application.
Related
I have a worker that downloads a JSON from s3, then a streaming JSON parser (Oj Saj) that parses the file into my db. I can update the worker status from the worker's class, but once I am inside the parser class it is outside the scope of the worker (or so it appears to me)
class Worker
include Sidekiq::Worker
include Sidekiq::Status::Worker
class SajParser << Oj::Saj
at 5 #this doesn't update the status of the worker
end
def perform()
at 5 #this does update the status of the worker
end
end
I would like a solution that allows me to update the status of the worker as the parsers goes over the JSON and inserts it into the db.
If you can make Sidekiq::Status #at method take the jid of the job to update and it's current status, then yes it is possible. Maybe you could follow up on this issue it seems the same as the one you have.
If I may suggest to actually use Sidekiq's power to parallelize the parser's job, into more workers instead of having one worker doing the job since you have the json in memory you can spin a job for each part of the json and whenever one of them is done, it should store its status somewhere accessible between all jobs like the DB for example.
I have a use case where user schedules a 'command' from the web interface. The user also specifies the date and time the command needs to be triggred.
This is sequence of steps:
1.User schedules a command 'Restart Device' at May 31, 3pm.
2.This is saved in a database table called Command.
3.Now there needs to be a background job that needs to be triggered at this specified time to do something (make an api call, send email etc.)
4.Once job is executed, It is removed or marked done, until a new command is issued.
There could be multpile users concurrently performing the above sequence of steps.
Is delayed_job a good choice for above? I couldnt find an example as how to implement above using delayed job.
EDIT: the reason I was looking at delayed_job is because eventually I would need to leverage existing relational database
I would advise to use Sidekiq. With it you can use scheduled jobs to tell sidekiq when to perform the jobs.
Example :
MyWorker.perform_at(3.hours.from_now, 'mike', 1)
EDIT : worker example
#app/workers/restart_device_worker.rb
class RestartDeviceWorker
include Sidekiq::Worker
def perform(params)
# Do the job
# ...
# update in DB
end
end
see doc: https://blog.codeship.com/how-to-use-rails-active-job/
https://guides.rubyonrails.org/active_job_basics.html
If you are using Rails 5 then you have best option of ActiveJob(inbuilt feature)
Use ActiveJob
"Active Job – Make work happen later. Active Job is a framework for declaring jobs and making them run on a variety of queuing backends. These jobs can be everything from regularly scheduled clean-ups, to billing charges, to mailings. Anything that can be chopped up into small units of work and run in parallel, really."
Active Job has built-in adapters for multiple queuing backends (Sidekiq, Resque, Delayed Job and others). You just need to tell them.
Scenario: I want to delete my story after 24 hours(1 day). Then we do create a job named "StoriesCleanupJob". Call this job at the time of the creation of story like below
StoriesCleanupJob.set(wait: 1.day).perform_later(story)
It will call the Job after 1 day.
class StoriesCleanupJob < ApplicationJob
queue_as :default
def perform(story)
if story.destroy
#put your own conditions like update the status and all, whatever you want to perform.
end
end
end
I have what I think is a working setup to send emails via Delayed_Job. However, I haven't received my test email and it isn't practical to wait for more to happen with a delay of days. I need to figure out:
What's wrong that's causing the email not to send.
How to test it without waiting days at a time.
I'm new to Delayed_Job, so pardon the newbie mistakes.
Here's the model that includes the send_reminder_emails method. They were fully functional without the .delay(run_at: self.mail_date) bit, so at least I know that much works:
class Reminder < ActiveRecord::Base
belongs_to :user
before_save :create_mail_date
after_save :send_reminder_emails
extend FriendlyId
friendly_id :name, use: [:slugged, :finders]
def create_mail_date
#schedule = IceCube::Schedule.new(self.date)
case self.repeating
when "Weekly"
#schedule.add_recurrence_rule(
IceCube::Rule.weekly
)
when "Monthly"
#schedule.add_recurrence_rule(
IceCube::Rule.monthly.day_of_month(self.date.mon)
)
when "Yearly"
#schedule.add_recurrence_rule(
IceCube::Rule.yearly.day_of_year(self.date.yday)
)
end
if self.repeating
self.date = #schedule.next_occurrence(Time.now)
end
self.mail_date = self.date - 7.days
end
private
def send_reminder_emails
if self.reminder
ReminderMailer.delay(run_at: self.mail_date).reminder_send(self.user, self).deliver_now
self.create_mail_date
end
end
handle_asynchronously :send_reminder_emails
end
The references to schedule are via the Ice_Cube gem and all of the date stuff has been tested via my console and is working. Here is my reminder_mailer.rb:
class ReminderMailer < ApplicationMailer
default from: "man#manlyartofbbq.com"
def reminder_send(user, reminder)
#user = user
#reminder = reminder
mail(to: user.email, subject: "Reminder! #{reminder.name} is fast approaching!")
end
end
I installed Delayed_Job step by step from their readme for Rails 4. Any help getting the delayed part of this mailer ironed out is appreciated!
You can use Active Job for sending mails by scheduling the job at a particular time.
Active Job is a framework for declaring jobs and making them run on a variety of queuing backends. These jobs can be everything from regularly scheduled clean-ups, to billing charges, to mailings. Anything that can be chopped up into small units of work and run in parallel, really.
For enqueuing and executing jobs in production you need to set up a queuing backend, that is to say you need to decide for a 3rd-party queuing library that Rails should use. Rails itself only provides an in-process queuing system, which only keeps the jobs in RAM. If the process crashes or the machine is reset, then all outstanding jobs are lost with the default async back-end. This may be fine for smaller apps or non-critical jobs, but most production apps will need to pick a persistent backend.
Active Job has built-in adapters for multiple queuing backends (Sidekiq, Resque, Delayed Job and others). To get an up-to-date list of the adapters see the API Documentation for ActiveJob::QueueAdapters.
Once after configuring the queuing adapters, then create Job for sending mails
class RemainderMailerJob < ApplicationJob
queue_as :default
def perform(user, remainder)
ReminderMailer.reminder_send(user, reminder).deliver
end
end
For sending the mail on a specific time you need to enqueue the job and make it perform at a specific time. If you need to send mail 7 days from now then call this piece of code where ever you need.
RemainderMailerJob.set(wait: 1.week).perform_later(user, remainder)
Now this Job will execute after 1 week, which in turn calls the mailer at that time, so your mails will be sent on the specific day.
IF you knew the specific Date Time then you can use
RemainderMailerJob.set(wait_untill: <Specific date>).perform_later(user, remainder)
If you have any doubts regarding the active job kindly refer Active Job Basics
When you send mails using delayed job, there is no need to add 'deliver' or 'deliver_now' when initializing the delayed_job for mailer. So
ReminderMailer.delay(run_at: self.mail_date).reminder_send(self.user, self)
will itself work.
Convert your 'self.mail_date' to also include the time at which you want to send the mail. Passing only date will not work. Basically the run_at should specify both date and time.
Please check the 'delayed_job' github link under the heading "Rails 3 Mailers"
https://github.com/collectiveidea/delayed_job
I have seen this happen when attribute protection is misconfigured. (Usually on legacy apps that left some kludge in when upgrading rails version.)
See if you're able to set the field from the console: Delayed::Job.new(run_at: 1.year.from_now). If not, that's the culprit and you'll need to track down the cause in your app.
If it works, then we still need to verify whether this is an app problem or a DJ bug. Make a new minimal rails app with just a delayed_job table and try to reproduce this. If you can't then there's something else odd in your app.
I have a rails application where I want to run a job in the background, but I need to run the job 2 hours from the original event.
The use case might be something like this:
User posts a product listing.
Background job is queued to syndicate listing to 3rd party api's, but even after original request, the response could take a while and the 3rd party's solution is to poll them every 2 hours to see if we can get a success acknowledgement.
So is there a way to queue a job, so that a worker daemon knows to ignore it or only listen to it at the scheduled time?
I don't want to use cron because it will load up a whole application stack and may be executed twice on overlapping long running jobs.
Can a priority queue be used for this? What solutions are there to implement this?
try delayed job - https://github.com/collectiveidea/delayed_job
something along these lines?
class ProductCheckSyndicateResponseJob < Struct.new(:product_id)
def perform
product = Product.find(product_id)
if product.still_needs_syndicate_response
# do it ...
# still no response, check again in two hours
Delayed::Job.enqueue(ProductCheckSyndicateResponseJob.new(product.id), :run_at => 2.hours.from_now)
else
# nothing to do ...
end
end
end
initialize job first time in controller or maybe before_create callback on model?
Delayed::Job.enqueue(ProductCheckSyndicateResponseJob.new(#product.id), :run_at => 2.hours.from_now)
Use the Rufus Scheduler gem. It runs as a background thread, so you don't have to load the entire application stack again. Add it to your Gemfile, and then your code is as simple as:
# in an initializer,
SCHEDULER = Rufus::Scheduler.start_new
# then wherever you want in your Rails app,
SCHEDULER.in('2h') do
# whatever code you want to run in 2 hours
end
The github page has tons of more examples.
I have a background job that does a map/reduce job on MongoDB. When the user sends in more data to the document, it kicks of the background job that runs on the document. If the user sends in multiple requests, it will kick off multiple background jobs for the same document, but only one really needs to run. Is there a way I can prevent multiple duplicate instances? I was thinking of creating a queue for each document and making sure it is empty before I submit a new job. Or perhaps I can set a job id somehow that is the same as my document id, and check that none exists before submitting it?
Also, I just found a sidekiq-unique-jobs gem. But the documentation is non-existent. Does this do what I want?
My initial suggestion would be a mutex for this specific job. But as there's a chance that you may have multiple application servers working the sidekiq jobs, I would suggest something at the redis level.
For instance, use redis-semaphore within your sidekiq worker definition. An untested example:
def perform
s = Redis::Semaphore.new(:map_reduce_semaphore, connection: "localhost")
# verify that this sidekiq worker is the first to reach this semaphore.
unless s.locked?
# auto-unlocks in 90 seconds. set to what is reasonable for your worker.
s.lock(90)
your_map_reduce()
s.unlock
end
end
def your_map_reduce
# ...
end
https://github.com/krasnoukhov/sidekiq-middleware
UniqueJobs
Provides uniqueness for jobs.
Usage
Example worker:
class UniqueWorker
include Sidekiq::Worker
sidekiq_options({
# Should be set to true (enables uniqueness for async jobs)
# or :all (enables uniqueness for both async and scheduled jobs)
unique: :all,
# Unique expiration (optional, default is 30 minutes)
# For scheduled jobs calculates automatically based on schedule time and expiration period
expiration: 24 * 60 * 60
})
def perform
# Your code goes here
end
end
There also is https://github.com/mhenrixon/sidekiq-unique-jobs (SidekiqUniqueJobs).
You can do this, assuming you have all the jobs are getting added to Enqueued bucket.
class SidekiqUniqChecker
def self.perform_unique_async(action, model_name, id)
key = "#{action}:#{model_name}:#{id}"
queue = Sidekiq::Queue.new('elasticsearch')
queue.each { |q| return if q.args.join(':') == key }
Indexer.perform_async(action, model_name, id)
end
end
The above code is just a sample, but you may tweak it to your needs.
Source