动机
在[BootrAils],我们试图尽可能地保持堆栈的简单。
然而,当你为商业目的创建一个新的Rails应用程序时,对后台作业存在的需求就会很快出现。想想 "忘记密码?"的功能吧。需要发送一封电子邮件。deliver_later ,而这种方法需要 "后台作业 "来正常工作。
而在Ruby或Ruby-on-Rails中,你都无法轻易做到这一点。
Rails问:在处理一个请求时,是否有一个好的工具或成语来异步运行一个代码块,这样它就不会阻塞响应,但也不会产生作业/队列系统的开销?
do_later { fire_and_forget() }
只是有一个一次性的调用&不希望用户等待。
理论上,ActiveJob(负责后台工作,顾名思义)已经包含在每个默认的Rails应用程序中。
但是......设置、依赖性以及生产、测试和开发的设置完全由开发者自己决定。
在这个领域有两个巨头:delayed_jobs (有时被称为 "DJ"),以及Sidekiq 。Sidekiq的知名度更高,维护和记录也更多,在此我建议任何新的Rails应用都选择Sidekiq--YMMV。
本教程是为了消除人们对在一个主要目的是 "仅仅是MCV "的应用程序中安装后台作业的恐惧。
前提条件
以下是本教程中我们将使用的工具。
$> ruby -v
ruby 3.1.0p0 // you need at least version 3 here
$> bundle -v
Bundler version 2.2.11
$> npm -v
8.3.0 // you need at least version 7.1 here
$> yarn -v
1.22.10
$> psql --version
psql (PostgreSQL) 13.1 // let's use a production-ready database locally
$> redis-cli ping // redis is a dependency of Sidekiq
PONG
$> foreman -v
0.87.2
创建一个新的Rails应用--首先不使用Sidekiq
mkdir sidekiqrails && cd sidekiqrails
echo "source 'https://rubygems.org'" > Gemfile
echo "gem 'rails', '7.0.1'" >> Gemfile
bundle install
bundle exec rails new . --force -d=postgresql --minimal
# Create a default controller
echo "class WelcomeController < ApplicationController" > app/controllers/welcome_controller.rb
echo "end" >> app/controllers/welcome_controller.rb
# Create a default route
echo "Rails.application.routes.draw do" > config/routes.rb
echo ' get "welcome/index"' >> config/routes.rb
echo ' root to: "welcome#index"' >> config/routes.rb
echo 'end' >> config/routes.rb
# Create a default view
mkdir app/views/welcome
echo '<h1>This is h1 title</h1>' > app/views/welcome/index.html.erb
# Create database and schema.rb
bin/rails db:create
bin/rails db:migrate
然后打开application.rb并取消对第6行的注释,如下所示。
# inside config/application.rb
require_relative "boot"
require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie" # <== Uncomment
# ... everything else remains the same
然后创建所有作业的父类。
# inside app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
end
题外话我们在创建Rails应用时使用了--minimal 标志,这样你就可以清楚地发现运行一个作业需要什么。在默认安装中,active_job/railtie 已经被取消了注释,而且所有作业的父类已经存在。
在你的Rails应用中添加redis和sidekiq gem
打开你的Gemfile,并在最下面添加
gem 'redis'
gem 'sidekiq'
在你的终端中,运行
bundle install
然后在application.rb中添加以下一行
# inside config/application.rb
# ...
class Application < Rails::Application
config.active_job.queue_adapter = :sidekiq
# ...
创建一个hello world job
在app/jobs/hello_world_job.rb下创建一个新文件。
# inside app/jobs/hello_world_job.rb
class HelloWorldJob < ApplicationJob
queue_as :default
def perform(*args)
# Simulates a long, time-consuming task
sleep 5
# Will display current time, milliseconds included
p "hello from HelloWorldJob #{Time.now().strftime('%F - %H:%M:%S.%L')}"
end
end
没有什么特别的。这个作业的目的是在被调用时以异步方式运行。而经典的HTTP请求/响应将在唯一可用的线程内处理。然而,任何HelloWorldJob最初都可以在HTTP请求中被触发。让我们来看看如何。
从视图中调用Hello World工作
修改routes.rb如下。
# inside config/routes.rb
Rails.application.routes.draw do
get "welcome/index"
# route where any visitor require the helloWorldJob to be triggered
post "welcome/trigger_job"
# where visitor are redirected once job has been called
get "other/job_done"
root to: "welcome#index"
end
创建app/controllers/other_controller.rb
# inside app/controllers/other_controller.rb
class OtherController < ApplicationController
def job_done
end
end
创建app/views/other/job_done.html.erb
<!-- inside app/views/other/job_done.html.erb -->
<h1>Job was called</h1>
现在我们的工作已经准备好从初始视图中调用了。
<%# inside app/views/welcome/index.html.erb %>
<h1>This is h1 title</h1>
<%= form_with url: welcome_trigger_job_path do |f| %>
<%= f.submit 'Launch job' %>
<% end %>
调用将发生在控制器内,像这样。
# inside app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
def trigger_job
HelloWorldJob.perform_later
redirect_to other_job_done_path
end
end
添加一个Procfile.dev
你可能想现在就尝试一切,但等一下:我们的工作必须从另一个线程发送。这可以通过在我们项目的根部添加一个Procfile.dev 文件来实现。
web: bin/rails s
worker: bundle exec sidekiq -C config/sidekiq.yml
添加config/sidekiq.yml
# inside config/sidekiq.yml
development:
:concurrency: 5
production:
:concurrency: 10
:max_retries: 1
:queues:
- default
最后在这里添加sidekiq初始化器 : config/initializers/sidekiq.rb
# inside config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') }
end
Sidekiq.configure_client do |config|
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') }
end
redis://localhost:6379/1 是本地安装的redis数据库的默认URL。如果这里没有,REDIS_URL是环境变量,它将会来帮忙。
启动本地应用程序
现在,是时候看看一切是否顺利了。
像这样启动你的本地服务器。
foreman start -f Procfile.dev
让我们看看本地显示的是什么。

localhost
点击 "启动作业 "按钮,在你的终端中立即显示日志。
18:43:31 web.1 | Started POST "/welcome/trigger_job" for ::1 at 2022-01-22 18:43:31 +0100
18:43:31 web.1 | Processing by WelcomeController#trigger_job as HTML
18:43:31 web.1 | [ActiveJob] Enqueued HelloWorldJob (Job ID: 9b9aa325-d118-45ff-a74b-99cad73ecda8) to Sidekiq(default)
18:43:31 web.1 | Redirected to http://localhost:5000/other/job_done
18:43:31 web.1 |
18:43:31 web.1 | Started GET "/other/job_done" for ::1 at 2022-01-22 18:43:31 +0100
18:43:31 web.1 | Completed 200 OK in 4ms (Views: 3.3ms | ActiveRecord: 0.0ms | Allocations: 2644)
18:43:31 web.1 |
18:43:31 worker.1 | 2022-01-22T17:43:31.776Z pid=3002 tid=3ze class=HelloWorldJob jid=166d945e2b7cd15e37addc7c INFO: Performing HelloWorldJob (Job ID: 9b9aa325-d118-45ff-a74b-99cad73ecda8) from Sidekiq(default) enqueued at 2022-01-22T17:43:31Z
18:43:36 worker.1 | "hello from HelloWorldJob 2022-01-22 - 18:43:36.784"
18:43:36 worker.1 | 2022-01-22T17:43:36.785Z pid=3002 tid=3ze class=HelloWorldJob jid=166d945e2b7cd15e37addc7c INFO: Performed HelloWorldJob (Job ID: 9b9aa325-d118-45ff-a74b-99cad73ecda8) from Sidekiq(default) in 5008.15ms
18:43:36 worker.1 | 2022-01-22T17:43:36.786Z pid=3002 tid=3ze class=HelloWorldJob jid=166d945e2b7cd15e37addc7c elapsed=5.47 INFO: done
我们在这里复制/粘贴了一个简化版的日志,这样你就可以看到实际发生的情况。GET "/other/job_done" 被立即触发,在18:43:31 ;而作业实际上在5秒后被调用,在18:43:36 。这就是所谓的 "后台作业"。
将 Rails 和 Sidekiq 推送到生产中
按照以下步骤进行。
# heroku is connected
heroku login
heroku create
# This will modify local files
echo "web: bundle exec puma -C config/puma.rb" > Procfile
echo "worker: bundle exec sidekiq -e production -C config/sidekiq.yml" >> Procfile
bundle lock --add-platform x86_64-linux
# This will modify you heroku app
heroku addons:create heroku-postgresql:hobby-dev
heroku addons:create heroku-redis:hobby-dev
heroku buildpacks:add heroku/ruby
git add . && git commit -m 'ready for prod'
git push heroku main
# app works, but worker (for background jobs) is missing
heroku ps:scale worker=1
到目前为止,一切都应该正常工作。
等待1分钟(最多2分钟),以便正确设置每个Heroku的依赖关系。
在浏览器中打开应用程序(URL应包含heroku.com,所以我们这次没有尝试localhost)。
再次点击 "启动 "按钮,并通过输入以下内容检查你的日志
heroku logs
你能看到正确打印出 "Hello world "的工作者吗?Sidekiq成功了
最后一句话(注意)
后台工作往往会毫无怨言地吞下异常。连接Redis数据库失败了?它不会告诉你。出现了错误?可能不会被显示。所以我们的建议是,通过保持HelloWorldJob的可用性(快速手动检查Sidekiq是否工作)来确保一切正常工作,并从一开始就安装Sidekiq的Web UI(本教程中未涉及)。