Rails, Sidekiq的完整教程

597 阅读3分钟

动机

在[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

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(本教程中未涉及)。