Rails Rails 4.2 中 ActiveJob 的使用

jicheng1014 · 2015年04月20日 · 最后由 ThxFly 回复于 2018年11月30日 · 9372 次阅读
本帖已被管理员设置为精华贴

初探 ActiveJob

简介

ActiveJob 是 Rails 4.2 新加入的功能。这个东西在 beta 阶段 rubyChina 就已经有很多高手关注了,无奈自己的项目使用的是 4.1.5,升级到 4.2 的时候其他 gem 又有很多依赖有问题,所以没在第一时间使用。今天补个课。

ActiveJob 是 Rails 自己开发运行后台程序的模块,常用于执行运行时间可能很长的工作(比如发送注册邮件)。

当然这种需求实际上非常普遍,所以 rails 也有相应的第三方 gem 来解决这个需求,比如著名的 Sidekiq 和 Resque 等。ActiveJob 的出现不是为了代替他们,而是统一了原来 Resque、Sidekiq 等其他 gem 对后台运行程序的各种千奇百怪的写法。

定义

ActiveJob 的使用官方文档已经给出了示例 首先在命令行中使用 rails g job JOBNAME 来新建一个任务

比如这里

➜  my_rails42 git:(master) ✗ rails g job add_lots_of_users
      invoke  test_unit
      create    test/jobs/add_lots_of_users_job_test.rb
      create  app/jobs/add_lots_of_users_job.rb
➜  my_rails42 git:(master) ✗

我们打开 add_lots_of_users_job.rb,可以看到下列内容

class AddLotsOfUsersJob < ActiveJob::Base
  queue_as :default

  def perform(*args)
    # Do something later
  end
end

我们可以将耗时的内容写入perform

def perform(*args)
  # Do something later
  sleep 10
  1000.times do |index|
    user = User.new
    user.name = "atpking#{index}"
    user.save
  end
end

我们可以看到,我在 job 里建立了一个作业,先睡 10 秒,再插入 1000 条数据到数据库中

至此,我们就成功建立了一个 job 了,接下来,就是使用了

使用

官方 demo 讲的非常简单,就是在你使用的地方用这个句子:XXJob.perform_later PARAMS

比如我这里

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # GET /users
  # GET /users.json
  def index
    AddLotsOfUsersJob.perform_later
    @users = User.all
  end
end

我们运行一下试试

运行过程中发现不像我们预想的那样,而是访问 index 的时候,活生生的等待了 10 多秒,在获得@users

[ActiveJob] [AddLotsOfUsersJob] [e191c62d-68a9-425f-8a94-b9fe080c141c]    (0.8ms)  commit transaction
[ActiveJob] [AddLotsOfUsersJob] [e191c62d-68a9-425f-8a94-b9fe080c141c]    (0.0ms)  begin transaction
[ActiveJob] [AddLotsOfUsersJob] [e191c62d-68a9-425f-8a94-b9fe080c141c]   SQL (0.2ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "atpking999"], ["created_at", "2015-04-20 04:30:29.320703"], ["updated_at", "2015-04-20 04:30:29.320703"]]
[ActiveJob] [AddLotsOfUsersJob] [e191c62d-68a9-425f-8a94-b9fe080c141c]    (0.6ms)  commit transaction
[ActiveJob] [AddLotsOfUsersJob] [e191c62d-68a9-425f-8a94-b9fe080c141c] Performed AddLotsOfUsersJob from Inline(default) in 12789.38ms
  User Load (2.1ms)  SELECT "users".* FROM "users"
  Rendered users/index.html.erb within layouts/application (186.7ms)
Completed 200 OK in 13210ms (Views: 392.6ms | ActiveRecord: 1166.0ms)

注意看上面,很多歌 ActiveJob 完毕了之后,输出Performed AddLotsOfUsersJob from Inline(default) in 12789.38ms,再执行的select * from users

也就是说,默认情况下的 ActiveJob 跟我们使用的方法没什么区别,是阻塞的,实际上官方文档也说明了

4 Job Execution If no adapter is set, the job is immediately executedIf no adapter is set, the job is immediately executed.

那么我们需要给 ActiveJob 指定一个 Adapter 了。官方有支持以下的 adapter,功能有所不同,需要注意。如果没设定,则是默认的 Active Job Inline,可以看到一个悲剧,不支持异步(Async),这也是为何我们刚刚等了很长时间

function Async Queues Delayed Priorities Timeout Retries
Backburner Yes Yes Yes Yes Job Global
Delayed Job Yes Yes Yes Job Global Global
Qu Yes Yes No No No Global
Que Yes Yes Yes Job No Job
queue_classic Yes Yes No* No No No
Resque Yes Yes Yes (Gem) Queue Global Yes
Sidekiq Yes Yes Yes Queue No Job
Sneakers Yes Yes No Queue Queue No
Sucker Punch Yes Yes No No No No
Active Job Inline(默认的) No Yes N/A N/A N/A N/A
Active Job Yes Yes Yes No No No

我选用了 sidekiq 作为 Adapter,注意这里,你必须要安装过 sidekiq, 没安装自然需要你 Gemfile 里加一句咯 同时还要安装 redis,之后在命令行使用 redis-server & 启用 redis 同时也得在命令行里启用 sidekiq,直接输入 sidekiq & 即可

之后我们需要指定 sidekiq 为我们的 adapter,我们需要在 application.rb 里加入一句话

config.active_job.queue_adapter = :sidekiq

再次运行 index 页面,我们看 rails 的日志,就变成了先执行 Select * from Users,返回页面结果,再继续执行 jobs 的内容了。

Started GET "/users" for ::1 at 2015-04-20 12:16:20 +0800
Processing by UsersController#index as HTML
[ActiveJob] Enqueued AddLotsOfUsersJob (Job ID: 50372234-6cf7-4ad2-b886-fd3029f4ea3d) to Sidekiq(default)
2015-04-20T04:16:20Z 79432 TID-ouzlum9ig ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper JID-eb96be6e314f6926583a1267 INFO: start
  User Load (0.3ms)  SELECT "users".* FROM "users"
  Rendered users/index.html.erb within layouts/application (4.1ms)
Completed 200 OK in 72ms (Views: 57.8ms | ActiveRecord: 0.5ms)


Started GET "/assets/jquery_ujs.self-8e98a7a072a6cee1372d19fff9ff3e6aa1e39a37d89d6f06861637d061113ee7.js?body=1" for ::1 at 2015-04-20 12:16:21 +0800


Started GET "/assets/users.self-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.css?body=1" for ::1 at 2015-04-20 12:16:21 +0800


Started GET "/assets/jquery.self-d03a5518f45df77341bdbe6201ba3bfa547ebba8ed64f0ea56bfa5f96ea7c074.js?body=1" for ::1 at 2015-04-20 12:16:21 +0800


Started GET "/assets/application.self-e80e8f2318043e8af94dddc2adad5a4f09739a8ebb323b3ab31cd71d45fd9113.css?body=1" for ::1 at 2015-04-20 12:16:21 +0800


Started GET "/assets/scaffolds.self-a98ac27100e3e5ca7065dbd7c898e5afa02690ec2ef84ccc02f65c4c20057b83.css?body=1" for ::1 at 2015-04-20 12:16:21 +0800


Started GET "/assets/turbolinks.self-c37727e9bd6b2735da5c311aa83fead54ed0be6cc8bd9a65309e9c5abe2cbfff.js?body=1" for ::1 at 2015-04-20 12:16:21 +0800


Started GET "/assets/users.self-877aef30ae1b040ab8a3aba4e3e309a11d7f2612f44dde450b5c157aa5f95c05.js?body=1" for ::1 at 2015-04-20 12:16:21 +0800


Started GET "/assets/application.self-3a3c8b61bda630ee689740ce7cbd0dd8ea6fdd45e2c42eef4661ab38cf268afe.js?body=1" for ::1 at 2015-04-20 12:16:21 +0800
2015-04-20T04:16:33Z 79432 TID-ouzlum9ig ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper JID-eb96be6e314f6926583a1267 INFO: done: 12.828 sec

请注意上面的这两句话

2015-04-20T04:16:20Z 79432 TID-ouzlum9ig ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper JID-eb96be6e314f6926583a1267 INFO: start

2015-04-20T04:16:33Z 79432 TID-ouzlum9ig ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper JID-eb96be6e314f6926583a1267 INFO: done: 12.828 sec


所以实际上只是统一了接口,实际工作的执行还是交给接口背后的 gem 来完成的。挺好的。

学习了:- )

门面接口。。。

文档里没说怎么取消 job 啊

学习了😊😊😊😊

顶 纪老师

证明了三国演义的那句开场白:天下合久必分,分久必合。

我的总结: 1、它只是一个接口,perform 内写入异步的逻辑 2、我也用 sidekiq,需要单独启动它 3、还得单独启动 redis,如果给 redis 设置了访问密码,需要在 sidekiq 启动时设置下

config/initializers/redis.rb

require "redis"

redis_config = YAML.load_file("#{Rails.root}/config/redis.yml")[Rails.env]
sidekiq_url = redis_config['url']

Sidekiq.configure_server do |config|
  config.redis = { namespace: 'sidekiq', url: sidekiq_url }
end

Sidekiq.configure_client do |config|
  config.redis = { namespace: 'sidekiq', url: sidekiq_url }
end

config/redis.yml

development:
  url: "redis://127.0.0.1:6379"
production:
  url: "redis://username:[email protected]:6379"

启动

bundle exec sidekiq -C config/sidekiq.yml -d -e production
sudo redis-server /etc/redis.conf

请问,perform_now 可以立即把任务抛到后台,这样的话,就可以不用 sidekiq 了吗?

刚才眼睛花了,以为 perform_now 就可以异步😅,forgot it.

#5 楼 @hw676018683 怎么取消应该看你用的是哪个 gem,比如 sidekiq 的 https://github.com/mperham/sidekiq/wiki/API#scheduled。另外 ActiveJob 只是统一接口,很多后台任务的 gem 的其他高级功能,是没有在 ActiveJob 里边提供的,比如 Sidekiq 提到的: https://github.com/mperham/sidekiq/wiki/Active-Job#active-job-introduction

Note that more advanced Sidekiq features cannot be controlled or configured via ActiveJob, e.g. saving backtraces.

简单看了下 ActiveJob 里边的代码,发现 ActiveJobAdapter 里边都是主要只提供 .enqueue 这个接口,用于 ActiveJob 将任务塞进对应的队列,所以基本上断定 ActiveJob 并没有封装清理队列的相关逻辑,需要的直接用对应的 gem 提供的接口操作,比如这里提到的如何移除的方式:https://github.com/mperham/sidekiq/wiki/API#scheduled

sidekiq 已成为标配的今天,ActiveJob 的工作反倒是多此一举了。还是手动生成 sidekiq 的 worker 来的实在

需要 登录 后方可回复, 如果你还没有账号请 注册新账号