1. Place the problem
Suppose you want to send an email to the users after registering for an account to confirm your account on your Sample app page (the website that most of the Rails classmates will do through). If there are thousands of users registering at the same time, you will have to simultaneously send thousands of emails with account authentication content, this will lead to a response between the server and the client being affected. enjoy significantly.
=> We need to find a solution how to handle the tasks separately, the Background Job
will help us in performing the tasks in a separate thread and not affect the experience of the user whose jobs will be processed. => In this article, I will handle sending mail to the Background Job
2. What is Background Job?
Background job – ” background jobs “: are jobs or tasks that are processed out of the usual request – response flow in web applications.
Normally, the website receives a request from the user and returns a response, but the Background job
is a little different => Still from a request to the Website, but requires longer execution time than usual (can be thought It seems that these requests cannot be processed immediately), we will need to use the Background job
, move that complicated processing into the Job, it will process asynchronously on a separate thread, return response to the user.
For example
We have a request from the user to update their new home town information:
As you can see, the time from the time the user submits the form until the server returns the user page and the notification is successful will take 3.7 seconds.
When we use the Background job: the process we mentioned above only takes 0.3 seconds, processes like refresh cache
, send maild
, send noti
, recommend new friends
will be processed by Background job in a separate thread, the results will still be returned as desired by the user
3. How does Rails handle Backgound Job?
There are several ways to handle Background Job
for your Rails application:
- Use Active job
- Use Gem Sidekiq
- Use a Gem Delayed job
- Use Gem Resque
By default, Rails provides us Active job
(A framework for handling Background job
that we don’t need to install anything more about built-in queue process (built-in queue).
Unfortunately, the queue processes are stored in RAM, so if the server is shut down, the jobs that are not in progress or in progress will be lost, which can negatively affect our users and services. It is only suitable for small applications or unimportant jobs.
However, most large applications didn’t want this to happen, so they used 3rd-party adapter
like Sidekiq, Delayed_Job, Resque (which uses Redis to store queues, and has its own mechanisms for handling jobs when Server crashes and reboots will not lose jobs)
In this article, I mainly learn about Active jobs to handle Background Job, the other 3 ways you can learn more in the articles or on its doc to read.
I have also used Sidekiq (the other 2 have not): in addition to the perfect job handling, it also has some differences with other gems: Sidekiq uses multithreading and Redis to handle many jobs. also .Sidekiq and Rescue have a dashboard for managing jobs, while Active job and Delayed_job don’t (Delayed_job can install a gem to use the dashboard). In addition, passing parameters of Sidekiq will be different from Active job, I will show below
To handle jobs using adapters such as Sidekiq, Delayed Job, Rescue, we can configure as follows:
- Change queue_adapter in the application.rb file
1 2 3 4 5 6 7 | module SampleApp class Application < Rails::Application ... config.active_job.queue_adapter = :sidekiq end end |
- Configure it in the file …_ job.rb
1 2 3 4 5 6 7 8 | class SendMailJob < ApplicationJob self.queue_adapter = sidekiq def perform user ... end end |
- In addition, if you want to more explicitly manage the queue that the job will run, you can use the #set method as follows:
1 2 | SendMailJob.set(queue: :async).perform_later @user |
4. Active Job – Handling Background Job in Rails
First, I will handle the problem of putting the email to activate user accounts in the job
First we must create a job that you want using the rails g job SendMail
command
1 2 3 4 5 | rails g job SendMail invoke test_unit create test/jobs/send_mail_job_test.rb create app/jobs/send_mail_job.rb |
The send_mail_job.rb file will look like this:
1 2 3 4 5 6 7 8 | class SendMailJob < ApplicationJob queue_as :default def perform(*args) # Do something later end end |
By default, Active job
uses queue_as :default
which means the adapter uses :async
to save the job, in addition there is another adapter :inline
, to use it, we have to configure at application.rb as follows:
1 2 | config.active_job.queue_adapter = :inline |
=> When using adapter :inline
, the job will be executed immediately in the main thread, it is no different from not using the Background job
, so I think you should not use it.
=> So what about the default Active job adapter :async
: Job will execute in another thread group, suitable for dev / test environment because it does not need external infrastructure, but it is not suitable in environment production field, as it removes any pending jobs on reboot. If the jobs share the same thread group, the jobs behind have to wait for the jobs to finish running (so if jobs are waiting and the server is reset, it will be lost.
Note : To overcome the above disadvantages, you can configure
queue_adapter = :sidekiq
, of course you must installgem sidekiq
, turn on sidekiq’s server and use it, sure your job will not be lost when restarting the server, I have tried it.
We can configure the number of threads: async works like this:
1 2 3 4 5 6 | config.active_job.queue_adapter = ActiveJob::QueueAdapters::AsyncAdapter.new min_threads: 1, max_threads: 2 * Concurrent.processor_count, idletime: 600.seconds end |
With Active job, you can pass as many parameters as you want, maybe an entire object, namely:
1 2 3 | Basic types (NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass) / Symbol / Array / Date / Time / DateTime ActiveSupport::TimeWithZone / ActiveSupport::Duration /Hash (Keys should be of String or Symbol type) / ActiveSupport::HashWithIndifferentAccess |
For Sidekiq: the arguments passed must be simple JSON types such as string, integer, boolean, float, null (nil), hash, array.
After a bit of rambling, solve the problem of sending mail), just do the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #file send_mail_job.rb class SendMailJob < ApplicationJob queue_as :default def perform user @user = user user.send_activation_email end end #file users_controller.rb chỉ cần thay @user.send_activation_email bằng cách gọi job SendMailJob.perform_later và truyền đối tượng @user class UsersController < ApplicationController def create @user = User.new user_params if @user.save # @user.send_activation_email SendMailJob.perform_later @user ... end end #instance method send_activation_email trong model user đây nhé def send_activation_email self.activation_token = User.new_token update :activation_digest, User.digest(activation_token) UserMailer.account_activation(self).deliver_now end > Mình mới code ROR được mấy tháng nên code tù lắm, mỏi người thông cảm. hihi |
That’s it, here’s my extra bonus
4.1. Job calls
Execute the job as soon as the queue is empty:
1 2 | SendMailJob.perform_later @user |
Do it after a period of time:
1 2 | SendMailJob.set(wait: 2.minutes).perform_later @user |
Do the job at noon tomorrow:
1 2 | SendMailJob.set(wait_until: Date.tomorrow.noon).perform_later @user |
Note: Besides
perform_later
also haveperform_now
, when you useperform_now
, the time you set to perform the job becomes meaningless.
4.2. Callbacks of Active job
Active Job provides callbacks
to trigger logic in the life cycle of a job. Works similarly to other callbacks
in the Rails model as follows:
1 2 3 4 | before_enqueue around_enqueue after_enqueue |
=> is called before, during, and after queuing the job
1 2 3 4 | before_perform around_perform after_perform |
=> is called before, during, and after the job is executed
4.3. Handling exceptions
During the job execution it is not possible to always be smooth, Active Job provides us with ways to catch exceptions raised during job execution:
- Handle exception in
perform
function body
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Class SendMailJob < ApplicationJob queue_as :default def perform user ActiveRecord::Base.transaction do .... raise ActiveRecord::RecordInvalid end rescue ActiveRecord::RecordInvalid puts "loi roi do" end end |
- discard_on (* exceptions): Eliminate the job that you do not want it to retry when raising exceptions, which is useful when the record has been deleted, for example.
1 2 3 4 5 6 7 8 9 10 11 | class SendMailJob < ApplicationJob queue_as :default discard_on ActiveRecord::RecordInvalid def perform user .... raise ActiveRecord::RecordInvalid end end |
- retry_on (* exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)Schedule the job to re-run when the job raises the exception, if the exception increases to exceed the number of attempts you set => can have your own retry mechanism for exceptions or put it in queue to check
- : wait – re-queues the job for a pending period
- : attempts – to re-enter the queue to be processed many times
- : queue – Put the job in another queue
- : priority – Re-arrange the job according to the priority
1 2 3 4 5 6 7 8 9 10 11 12 | class SendMailJob < ApplicationJob queue_as :default retry_on ActiveRecord::RecordInvalid # retry_on ActiveRecord::RecordInvalid, wait: 5.seconds, attempts: 3 def perform user ... raise ActiveRecord::RecordInvalid end end |
5. References
The article is quite long, but that is what I have learned about Active Job
and hope that some part helps you have an overview of the Background job
and the operating mechanism of Active Job
. Thank you for reading here
Guide Active job:
https://guides.rubyonrails.org/active_job_basics.html
https://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html