60 phút
Background Jobs và Action Mailer
Active Job
Tạo Job
rails g job SendWelcomeEmail
# app/jobs/send_welcome_email_job.rb
class SendWelcomeEmailJob < ApplicationJob
queue_as :default
retry_on StandardError, wait: :exponentially_longer, attempts: 3
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome(user).deliver_now
end
end
Enqueue Job
SendWelcomeEmailJob.perform_later(user.id)
SendWelcomeEmailJob.set(wait: 1.hour).perform_later(user.id)
SendWelcomeEmailJob.set(wait_until: Date.tomorrow.noon).perform_later(user.id)
SendWelcomeEmailJob.set(queue: :high_priority).perform_later(user.id)
Job với arguments
class ProcessOrderJob < ApplicationJob
queue_as :orders
discard_on ActiveRecord::RecordNotFound
def perform(order)
# ActiveRecord objects được serialize tự động
order.process!
end
end
ProcessOrderJob.perform_later(order)
Action Mailer
Generate Mailer
rails g mailer UserMailer welcome password_reset
Mailer Class
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
default from: 'noreply@myapp.com'
def welcome(user)
@user = user
@login_url = login_url
mail(
to: @user.email,
subject: "Welcome to MyApp!"
)
end
def password_reset(user)
@user = user
@reset_url = edit_password_reset_url(user.reset_token)
mail(to: @user.email, subject: "Reset your password")
end
end
Views
<!-- app/views/user_mailer/welcome.html.erb -->
<h1>Welcome, <%= @user.name %>!</h1>
<p>Thanks for signing up. We're excited to have you.</p>
<p><%= link_to "Log in here", @login_url %></p>
<p>Best regards,<br>The MyApp Team</p>
<!-- app/views/user_mailer/welcome.text.erb -->
Welcome, <%= @user.name %>!
Thanks for signing up. Log in here: <%= @login_url %>
Best regards,
The MyApp Team
Application Mailer Layout
<!-- app/views/layouts/mailer.html.erb -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
body { font-family: Arial, sans-serif; }
.header { background: #333; color: white; padding: 20px; }
.content { padding: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>MyApp</h1>
</div>
<div class="content">
<%= yield %>
</div>
</body>
</html>
Configuration
SMTP cho development (Mailcatcher/Mailhog)
# config/environments/development.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'localhost',
port: 1025
}
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
Production (Postmark, SendGrid, SES)
# config/environments/production.rb
config.action_mailer.delivery_method = :postmark
config.action_mailer.postmark_settings = {
api_token: Rails.application.credentials.postmark_api_token
}
config.action_mailer.default_url_options = { host: 'myapp.com' }
Job Backends
Sidekiq (Redis)
# Gemfile
gem 'sidekiq'
gem 'sidekiq-cron'
# config/application.rb
config.active_job.queue_adapter = :sidekiq
# config/sidekiq.yml
:concurrency: 5
:queues:
- [critical, 3]
- [default, 2]
- [low, 1]
Solid Queue (Rails 8 default)
config.active_job.queue_adapter = :solid_queue
bin/rails solid_queue:start
Recurring Jobs
sidekiq-cron
# config/initializers/sidekiq.rb
schedule = {
'daily_digest' => {
'cron' => '0 8 * * *',
'class' => 'DailyDigestJob',
'queue' => 'default'
},
'cleanup_job' => {
'cron' => '0 2 * * 0',
'class' => 'CleanupJob',
'queue' => 'low'
}
}
Sidekiq::Cron::Job.load_from_hash(schedule)
Solid Queue recurring
# config/recurring.yml
production:
daily_digest:
class: DailyDigestJob
schedule: every day at 8am
cleanup:
class: CleanupJob
schedule: every sunday at 2am
Job Patterns
Batch processing
class ProcessBatchJob < ApplicationJob
def perform(user_ids)
User.where(id: user_ids).find_each do |user|
ProcessUserJob.perform_later(user.id)
end
end
end
# Enqueue
user_ids = User.pluck(:id)
ProcessBatchJob.perform_later(user_ids)
Job chaining
class ImportDataJob < ApplicationJob
def perform(file_path)
result = import_file(file_path)
NotifyImportCompleteJob.perform_later(result.id)
end
end
Idempotent jobs
class ChargeCustomerJob < ApplicationJob
def perform(order_id)
order = Order.find(order_id)
return if order.paid? # idempotency
PaymentGateway.charge(order)
order.mark_as_paid!
end
end
Monitoring
Sidekiq Web UI
# config/routes.rb
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'
# Với basic auth
authenticate :user, ->(u) { u.admin? } do
mount Sidekiq::Web => '/sidekiq'
end
Mailer Preview
# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome
UserMailer.welcome(User.first)
end
def password_reset
UserMailer.password_reset(User.first)
end
end
Truy cập /rails/mailers trong development.
Test Jobs và Mailers
# spec/jobs/send_welcome_email_job_spec.rb
require 'rails_helper'
RSpec.describe SendWelcomeEmailJob, type: :job do
let(:user) { create(:user) }
it 'queues the job' do
expect {
described_class.perform_later(user.id)
}.to have_enqueued_job(described_class).with(user.id)
end
it 'sends welcome email' do
expect {
described_class.perform_now(user.id)
}.to change { ActionMailer::Base.deliveries.count }.by(1)
end
it 'retries on error' do
allow(UserMailer).to receive(:welcome).and_raise(StandardError)
expect {
described_class.perform_now(user.id)
}.to have_enqueued_job(described_class)
end
end
# spec/mailers/user_mailer_spec.rb
require 'rails_helper'
RSpec.describe UserMailer, type: :mailer do
let(:user) { create(:user) }
describe '#welcome' do
let(:mail) { described_class.welcome(user) }
it 'renders the subject' do
expect(mail.subject).to eq('Welcome to MyApp!')
end
it 'sends to user email' do
expect(mail.to).to eq([user.email])
end
it 'sends from noreply' do
expect(mail.from).to eq(['noreply@myapp.com'])
end
it 'includes user name in body' do
expect(mail.body.encoded).to include(user.name)
end
end
end
Bài tập thực hành
Hãy tạo welcome email job và mailer!