├── CHANGELOG.md
├── .gitignore
├── test
├── support
│ ├── delayed_job
│ │ └── delayed
│ │ │ ├── serialization
│ │ │ └── test.rb
│ │ │ └── backend
│ │ │ └── test.rb
│ ├── integration
│ │ ├── adapters
│ │ │ ├── sucker_punch.rb
│ │ │ ├── inline.rb
│ │ │ ├── delayed_job.rb
│ │ │ ├── qu.rb
│ │ │ ├── backburner.rb
│ │ │ ├── que.rb
│ │ │ ├── resque.rb
│ │ │ ├── queue_classic.rb
│ │ │ ├── sidekiq.rb
│ │ │ └── sneakers.rb
│ │ ├── dummy_app_template.rb
│ │ ├── jobs_manager.rb
│ │ ├── test_case_helpers.rb
│ │ └── helper.rb
│ ├── backburner
│ │ └── inline.rb
│ ├── que
│ │ └── inline.rb
│ ├── sneakers
│ │ └── inline.rb
│ ├── job_buffer.rb
│ └── queue_classic
│ │ └── inline.rb
├── adapters
│ ├── inline.rb
│ ├── qu.rb
│ ├── resque.rb
│ ├── sidekiq.rb
│ ├── sneakers.rb
│ ├── backburner.rb
│ ├── que.rb
│ ├── sucker_punch.rb
│ ├── queue_classic.rb
│ └── delayed_job.rb
├── jobs
│ ├── gid_job.rb
│ ├── hello_job.rb
│ ├── nested_job.rb
│ ├── logging_job.rb
│ ├── rescue_job.rb
│ └── callback_job.rb
├── cases
│ ├── adapter_test.rb
│ ├── job_serialization_test.rb
│ ├── test_case_test.rb
│ ├── callbacks_test.rb
│ ├── queuing_test.rb
│ ├── rescue_test.rb
│ ├── queue_naming_test.rb
│ ├── argument_serialization_test.rb
│ ├── logging_test.rb
│ └── test_helper_test.rb
├── models
│ └── person.rb
├── gemfiles
│ └── Gemfile.activesupport-4.0.x
├── helper.rb
└── integration
│ └── queuing_test.rb
├── lib
├── activejob_backport.rb
├── global_id.rb
├── active_job
│ ├── test_case.rb
│ ├── version.rb
│ ├── gem_version.rb
│ ├── configured_job.rb
│ ├── queue_adapters
│ │ ├── inline_adapter.rb
│ │ ├── sucker_punch_adapter.rb
│ │ ├── que_adapter.rb
│ │ ├── delayed_job_adapter.rb
│ │ ├── backburner_adapter.rb
│ │ ├── qu_adapter.rb
│ │ ├── sneakers_adapter.rb
│ │ ├── sidekiq_adapter.rb
│ │ ├── resque_adapter.rb
│ │ ├── test_adapter.rb
│ │ └── queue_classic_adapter.rb
│ ├── queue_adapters.rb
│ ├── base.rb
│ ├── railtie.rb
│ ├── queue_adapter.rb
│ ├── execution.rb
│ ├── queue_name.rb
│ ├── enqueuing.rb
│ ├── arguments.rb
│ ├── core.rb
│ ├── logging.rb
│ ├── callbacks.rb
│ └── test_helper.rb
├── rails
│ └── generators
│ │ └── job
│ │ ├── templates
│ │ └── job.rb
│ │ └── job_generator.rb
├── global_id
│ ├── identification.rb
│ ├── railtie.rb
│ ├── locator.rb
│ └── global_id.rb
└── active_job.rb
├── .travis.yml
├── Gemfile
├── README.md
├── activejob_backport.gemspec
├── MIT-LICENSE
└── Rakefile
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | * Started project.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | test/dummy
2 | *.lock
3 | pkg/
4 |
--------------------------------------------------------------------------------
/test/support/delayed_job/delayed/serialization/test.rb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/adapters/inline.rb:
--------------------------------------------------------------------------------
1 | ActiveJob::Base.queue_adapter = :inline
--------------------------------------------------------------------------------
/lib/activejob_backport.rb:
--------------------------------------------------------------------------------
1 | require 'active_job' if !defined?(ActiveJob)
2 |
--------------------------------------------------------------------------------
/test/adapters/qu.rb:
--------------------------------------------------------------------------------
1 | require 'qu-immediate'
2 |
3 | ActiveJob::Base.queue_adapter = :qu
4 |
--------------------------------------------------------------------------------
/test/adapters/resque.rb:
--------------------------------------------------------------------------------
1 | ActiveJob::Base.queue_adapter = :resque
2 | Resque.inline = true
3 |
--------------------------------------------------------------------------------
/test/adapters/sidekiq.rb:
--------------------------------------------------------------------------------
1 | require 'sidekiq/testing/inline'
2 | ActiveJob::Base.queue_adapter = :sidekiq
3 |
--------------------------------------------------------------------------------
/test/adapters/sneakers.rb:
--------------------------------------------------------------------------------
1 | require 'support/sneakers/inline'
2 | ActiveJob::Base.queue_adapter = :sneakers
3 |
--------------------------------------------------------------------------------
/test/adapters/backburner.rb:
--------------------------------------------------------------------------------
1 | require 'support/backburner/inline'
2 |
3 | ActiveJob::Base.queue_adapter = :backburner
--------------------------------------------------------------------------------
/test/adapters/que.rb:
--------------------------------------------------------------------------------
1 | require 'support/que/inline'
2 |
3 | ActiveJob::Base.queue_adapter = :que
4 | Que.mode = :sync
5 |
--------------------------------------------------------------------------------
/test/adapters/sucker_punch.rb:
--------------------------------------------------------------------------------
1 | require 'sucker_punch/testing/inline'
2 | ActiveJob::Base.queue_adapter = :sucker_punch
3 |
--------------------------------------------------------------------------------
/test/adapters/queue_classic.rb:
--------------------------------------------------------------------------------
1 | require 'support/queue_classic/inline'
2 | ActiveJob::Base.queue_adapter = :queue_classic
3 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/sucker_punch.rb:
--------------------------------------------------------------------------------
1 | module SuckerPunchJobsManager
2 | def setup
3 | ActiveJob::Base.queue_adapter = :sucker_punch
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/global_id.rb:
--------------------------------------------------------------------------------
1 | require 'global_id/global_id'
2 |
3 | class GlobalID
4 | autoload :Locator, 'global_id/locator'
5 | autoload :Identification, 'global_id/identification'
6 | end
7 |
--------------------------------------------------------------------------------
/lib/active_job/test_case.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/test_case'
2 |
3 | module ActiveJob
4 | class TestCase < ActiveSupport::TestCase
5 | include ActiveJob::TestHelper
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/gid_job.rb:
--------------------------------------------------------------------------------
1 | require_relative '../support/job_buffer'
2 |
3 | class GidJob < ActiveJob::Base
4 | def perform(person)
5 | JobBuffer.add("Person with ID: #{person.id}")
6 | end
7 | end
8 |
9 |
--------------------------------------------------------------------------------
/test/jobs/hello_job.rb:
--------------------------------------------------------------------------------
1 | require_relative '../support/job_buffer'
2 |
3 | class HelloJob < ActiveJob::Base
4 | def perform(greeter = "David")
5 | JobBuffer.add("#{greeter} says hello")
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/nested_job.rb:
--------------------------------------------------------------------------------
1 | class NestedJob < ActiveJob::Base
2 | def perform
3 | LoggingJob.perform_later "NestedJob"
4 | end
5 |
6 | def job_id
7 | "NESTED-JOB-ID"
8 | end
9 | end
10 |
11 |
--------------------------------------------------------------------------------
/test/jobs/logging_job.rb:
--------------------------------------------------------------------------------
1 | class LoggingJob < ActiveJob::Base
2 | def perform(dummy)
3 | logger.info "Dummy, here is it: #{dummy}"
4 | end
5 |
6 | def job_id
7 | "LOGGING-JOB-ID"
8 | end
9 | end
10 |
11 |
--------------------------------------------------------------------------------
/lib/active_job/version.rb:
--------------------------------------------------------------------------------
1 | require_relative 'gem_version'
2 |
3 | module ActiveJob
4 | # Returns the version of the currently loaded Active Job as a Gem::Version
5 | def self.version
6 | gem_version
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/adapters/delayed_job.rb:
--------------------------------------------------------------------------------
1 | ActiveJob::Base.queue_adapter = :delayed_job
2 |
3 | $LOAD_PATH << File.dirname(__FILE__) + "/../support/delayed_job"
4 |
5 | Delayed::Worker.delay_jobs = false
6 | Delayed::Worker.backend = :test
7 |
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.1
4 | script: bundle exec rake test
5 | notifications:
6 | email:
7 | on_success: never
8 | on_failure: change
9 | gemfile:
10 | - Gemfile
11 | - test/gemfiles/Gemfile.activesupport-4.0.x
12 |
--------------------------------------------------------------------------------
/lib/rails/generators/job/templates/job.rb:
--------------------------------------------------------------------------------
1 | <% module_namespacing do -%>
2 | class <%= class_name %>Job < ActiveJob::Base
3 | queue_as :<%= options[:queue] %>
4 |
5 | def perform(*args)
6 | # Do something later
7 | end
8 | end
9 | <% end -%>
10 |
--------------------------------------------------------------------------------
/test/support/backburner/inline.rb:
--------------------------------------------------------------------------------
1 | require 'backburner'
2 |
3 | Backburner::Worker.class_eval do
4 | class << self; alias_method :original_enqueue, :enqueue; end
5 | def self.enqueue(job_class, args=[], opts={})
6 | job_class.perform(*args)
7 | end
8 | end
--------------------------------------------------------------------------------
/test/support/que/inline.rb:
--------------------------------------------------------------------------------
1 | require 'que'
2 |
3 | Que::Job.class_eval do
4 | class << self; alias_method :original_enqueue, :enqueue; end
5 | def self.enqueue(*args)
6 | args.pop if args.last.is_a?(Hash)
7 | self.run(*args)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/inline.rb:
--------------------------------------------------------------------------------
1 | module InlineJobsManager
2 | def setup
3 | ActiveJob::Base.queue_adapter = :inline
4 | end
5 |
6 | def clear_jobs
7 | end
8 |
9 | def start_workers
10 | end
11 |
12 | def stop_workers
13 | end
14 | end
15 |
16 |
--------------------------------------------------------------------------------
/test/support/sneakers/inline.rb:
--------------------------------------------------------------------------------
1 | require 'sneakers'
2 |
3 | module Sneakers
4 | module Worker
5 | module ClassMethods
6 | def enqueue(msg)
7 | worker = self.new(nil, nil, {})
8 | worker.work(*msg)
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/global_id/identification.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | class GlobalID
4 | module Identification
5 | extend ActiveSupport::Concern
6 |
7 | def to_global_id
8 | @global_id ||= GlobalID.create(self)
9 | end
10 | alias to_gid to_global_id
11 |
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/support/job_buffer.rb:
--------------------------------------------------------------------------------
1 | module JobBuffer
2 | class << self
3 | def clear
4 | values.clear
5 | end
6 |
7 | def add(value)
8 | values << value
9 | end
10 |
11 | def values
12 | @values ||= []
13 | end
14 |
15 | def last_value
16 | values.last
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/cases/adapter_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 |
3 | class AdapterTest < ActiveSupport::TestCase
4 | test "should load #{ENV['AJADAPTER']} adapter" do
5 | ActiveJob::Base.queue_adapter = ENV['AJADAPTER'].to_sym
6 | assert_equal ActiveJob::Base.queue_adapter, "active_job/queue_adapters/#{ENV['AJADAPTER']}_adapter".classify.constantize
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/active_job/gem_version.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | # Returns the version of the currently loaded Active Job as a Gem::Version
3 | def self.gem_version
4 | Gem::Version.new VERSION::STRING
5 | end
6 |
7 | module VERSION
8 | MAJOR = 0
9 | MINOR = 0
10 | TINY = 3
11 | PRE = nil
12 |
13 | STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/active_job/configured_job.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | class ConfiguredJob #:nodoc:
3 | def initialize(job_class, options={})
4 | @options = options
5 | @job_class = job_class
6 | end
7 |
8 | def perform_now(*args)
9 | @job_class.new(*args).perform_now
10 | end
11 |
12 | def perform_later(*args)
13 | @job_class.new(*args).enqueue @options
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/cases/job_serialization_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'jobs/gid_job'
3 | require 'models/person'
4 |
5 | class JobSerializationTest < ActiveSupport::TestCase
6 | setup do
7 | JobBuffer.clear
8 | @person = Person.find(5)
9 | end
10 |
11 | test 'serialize job with gid' do
12 | GidJob.perform_later @person
13 | assert_equal "Person with ID: 5", JobBuffer.last_value
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/cases/test_case_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'jobs/hello_job'
3 | require 'jobs/logging_job'
4 | require 'jobs/nested_job'
5 |
6 | class ActiveJobTestCaseTest < ActiveJob::TestCase
7 | def test_include_helper
8 | assert_includes self.class.ancestors, ActiveJob::TestHelper
9 | end
10 |
11 | def test_set_test_adapter
12 | assert_instance_of ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/models/person.rb:
--------------------------------------------------------------------------------
1 | class Person
2 | class RecordNotFound < StandardError; end
3 |
4 | include GlobalID::Identification
5 |
6 | attr_reader :id
7 |
8 | def self.find(id)
9 | raise RecordNotFound.new("Cannot find person with ID=404") if id.to_i==404
10 | new(id)
11 | end
12 |
13 | def initialize(id)
14 | @id = id
15 | end
16 |
17 | def ==(other_person)
18 | other_person.is_a?(Person) && id.to_s == other_person.id.to_s
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/inline_adapter.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | module QueueAdapters
3 | class InlineAdapter
4 | class << self
5 | def enqueue(job)
6 | Base.execute(job.serialize)
7 | end
8 |
9 | def enqueue_at(*)
10 | raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at https://github.com/rails/activejob")
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | module QueueAdapters
3 | extend ActiveSupport::Autoload
4 |
5 | autoload :InlineAdapter
6 | autoload :BackburnerAdapter
7 | autoload :DelayedJobAdapter
8 | autoload :QuAdapter
9 | autoload :QueAdapter
10 | autoload :QueueClassicAdapter
11 | autoload :ResqueAdapter
12 | autoload :SidekiqAdapter
13 | autoload :SneakersAdapter
14 | autoload :SuckerPunchAdapter
15 | autoload :TestAdapter
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/delayed_job.rb:
--------------------------------------------------------------------------------
1 | require 'delayed_job'
2 | require 'delayed_job_active_record'
3 |
4 | module DelayedJobJobsManager
5 | def setup
6 | ActiveJob::Base.queue_adapter = :delayed_job
7 | end
8 | def clear_jobs
9 | Delayed::Job.delete_all
10 | end
11 |
12 | def start_workers
13 | @worker = Delayed::Worker.new(quiet: false, sleep_delay: 0.5, queues: %w(integration_tests))
14 | @thread = Thread.new { @worker.start }
15 | end
16 |
17 | def stop_workers
18 | @worker.stop
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/active_job/base.rb:
--------------------------------------------------------------------------------
1 | require 'active_job/core'
2 | require 'active_job/queue_adapter'
3 | require 'active_job/queue_name'
4 | require 'active_job/enqueuing'
5 | require 'active_job/execution'
6 | require 'active_job/callbacks'
7 | require 'active_job/logging'
8 |
9 | module ActiveJob
10 | class Base
11 | include Core
12 | include QueueAdapter
13 | include QueueName
14 | include Enqueuing
15 | include Execution
16 | include Callbacks
17 | include Logging
18 |
19 | ActiveSupport.run_load_hooks(:active_job, self)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/support/integration/dummy_app_template.rb:
--------------------------------------------------------------------------------
1 | if ENV['AJADAPTER'] == 'delayed_job'
2 | generate "delayed_job:active_record"
3 | rake("db:migrate")
4 | end
5 |
6 | initializer 'activejob.rb', <<-CODE
7 | require "#{File.expand_path("../jobs_manager.rb", __FILE__)}"
8 | JobsManager.current_manager.setup
9 | CODE
10 |
11 | file 'app/jobs/test_job.rb', <<-CODE
12 | class TestJob < ActiveJob::Base
13 | queue_as :integration_tests
14 |
15 | def perform(x)
16 | File.open(Rails.root.join("tmp/\#{x}"), "w+") do |f|
17 | f.write x
18 | end
19 | end
20 | end
21 | CODE
22 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/sucker_punch_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'sucker_punch'
2 |
3 | module ActiveJob
4 | module QueueAdapters
5 | class SuckerPunchAdapter
6 | class << self
7 | def enqueue(job)
8 | JobWrapper.new.async.perform job.serialize
9 | end
10 |
11 | def enqueue_at(job, timestamp)
12 | raise NotImplementedError
13 | end
14 | end
15 |
16 | class JobWrapper
17 | include SuckerPunch::Job
18 |
19 | def perform(job_data)
20 | Base.execute job_data
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/que_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'que'
2 |
3 | module ActiveJob
4 | module QueueAdapters
5 | class QueAdapter
6 | class << self
7 | def enqueue(job)
8 | JobWrapper.enqueue job.serialize, queue: job.queue_name
9 | end
10 |
11 | def enqueue_at(job, timestamp)
12 | JobWrapper.enqueue job.serialize, queue: job.queue_name, run_at: Time.at(timestamp)
13 | end
14 | end
15 |
16 | class JobWrapper < Que::Job
17 | def run(job_data)
18 | Base.execute job_data
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/support/integration/jobs_manager.rb:
--------------------------------------------------------------------------------
1 | class JobsManager
2 | @@managers = {}
3 | attr :adapter_name
4 |
5 | def self.current_manager
6 | @@managers[ENV['AJADAPTER']] ||= new(ENV['AJADAPTER'])
7 | end
8 |
9 | def initialize(adapter_name)
10 | @adapter_name = adapter_name
11 | require_relative "adapters/#{adapter_name}"
12 | extend "#{adapter_name.camelize}JobsManager".constantize
13 | end
14 |
15 | def setup
16 | ActiveJob::Base.queue_adapter = nil
17 | end
18 |
19 | def clear_jobs
20 | end
21 |
22 | def start_workers
23 | end
24 |
25 | def stop_workers
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/delayed_job_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'delayed_job'
2 |
3 | module ActiveJob
4 | module QueueAdapters
5 | class DelayedJobAdapter
6 | class << self
7 | def enqueue(job)
8 | JobWrapper.new.delay(queue: job.queue_name).perform(job.serialize)
9 | end
10 |
11 | def enqueue_at(job, timestamp)
12 | JobWrapper.new.delay(queue: job.queue_name, run_at: Time.at(timestamp)).perform(job.serialize)
13 | end
14 | end
15 |
16 | class JobWrapper
17 | def perform(job_data)
18 | Base.execute(job_data)
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/active_job/railtie.rb:
--------------------------------------------------------------------------------
1 | # require 'global_id/railtie'
2 | require 'active_job'
3 |
4 | module ActiveJob
5 | # = Active Job Railtie
6 | class Railtie < Rails::Railtie # :nodoc:
7 | config.active_job = ActiveSupport::OrderedOptions.new
8 |
9 | initializer 'active_job.logger' do
10 | ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger }
11 | end
12 |
13 | initializer "active_job.set_configs" do |app|
14 | options = app.config.active_job
15 | options.queue_adapter ||= :inline
16 |
17 | ActiveSupport.on_load(:active_job) do
18 | options.each { |k,v| send("#{k}=", v) }
19 | end
20 | end
21 |
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | gem 'activesupport', '~> 4.1.0'
6 |
7 | gem 'resque', require: false
8 | gem 'resque-scheduler', require: false
9 | gem 'sidekiq', require: false
10 | gem 'sucker_punch', "< 2.0", require: false
11 | gem 'delayed_job', require: false
12 | gem 'queue_classic', "< 3.0.0", require: false, platforms: :ruby
13 | gem 'sneakers', '0.1.1.pre', require: false
14 | gem 'que', require: false
15 | gem 'backburner', require: false
16 | gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false
17 | gem 'qu-redis', require: false
18 | gem 'delayed_job_active_record', require: false
19 | gem 'sequel', require: false
20 | gem 'actionmailer'
21 | gem 'sqlite3'
22 |
--------------------------------------------------------------------------------
/test/support/queue_classic/inline.rb:
--------------------------------------------------------------------------------
1 | require 'queue_classic'
2 |
3 | module QC
4 | class Queue
5 | def enqueue(method, *args)
6 | receiver_str, _, message = method.rpartition('.')
7 | receiver = eval(receiver_str)
8 | receiver.send(message, *args)
9 | end
10 |
11 | def enqueue_in(seconds, method, *args)
12 | receiver_str, _, message = method.rpartition('.')
13 | receiver = eval(receiver_str)
14 | receiver.send(message, *args)
15 | end
16 |
17 | def enqueue_at(not_before, method, *args)
18 | receiver_str, _, message = method.rpartition('.')
19 | receiver = eval(receiver_str)
20 | receiver.send(message, *args)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/rails/generators/job/job_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators/named_base'
2 |
3 | module Rails
4 | module Generators # :nodoc:
5 | class JobGenerator < Rails::Generators::NamedBase # :nodoc:
6 | desc 'This generator creates an active job file at app/jobs'
7 |
8 | class_option :queue, type: :string, default: 'default', desc: 'The queue name for the generated job'
9 |
10 | check_class_collision suffix: 'Job'
11 |
12 | hook_for :test_framework
13 |
14 | def self.default_generator_root
15 | File.dirname(__FILE__)
16 | end
17 |
18 | def create_job_file
19 | template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb")
20 | end
21 |
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Active Job
2 |
3 | Active Job backported to Rails 4.0 and 4.1
4 |
5 | ```ruby
6 | gem 'activejob_backport'
7 | ```
8 |
9 | And create `config/initializers/active_job.rb` with:
10 |
11 | ```ruby
12 | ActiveJob::Base.queue_adapter = :inline # default queue adapter
13 | # Adapters currently supported: :backburner, :delayed_job, :qu, :que, :queue_classic,
14 | # :resque, :sidekiq, :sneakers, :sucker_punch
15 | ```
16 |
17 | See [how to use Active Job](http://edgeguides.rubyonrails.org/active_job_basics.html) and the [official repo](https://github.com/rails/rails/tree/master/activejob)
18 |
19 | [](https://travis-ci.org/ankane/activejob_backport)
20 |
--------------------------------------------------------------------------------
/test/gemfiles/Gemfile.activesupport-4.0.x:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec path: '../../'
4 |
5 | gem 'activesupport', '~> 4.0.0'
6 |
7 | gem 'resque', require: false
8 | gem 'resque-scheduler', require: false
9 | gem 'sidekiq', require: false
10 | gem 'sucker_punch', "< 2.0", require: false
11 | gem 'delayed_job', require: false
12 | gem 'queue_classic', "< 3.0.0", require: false, platforms: :ruby
13 | gem 'sneakers', '0.1.1.pre', require: false
14 | gem 'que', require: false
15 | gem 'backburner', require: false
16 | gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false
17 | gem 'qu-redis', require: false
18 | gem 'delayed_job_active_record', require: false
19 | gem 'sequel', require: false
20 | gem 'actionmailer'
21 | gem 'sqlite3'
22 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/backburner_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'backburner'
2 |
3 | module ActiveJob
4 | module QueueAdapters
5 | class BackburnerAdapter
6 | class << self
7 | def enqueue(job)
8 | Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name
9 | end
10 |
11 | def enqueue_at(job, timestamp)
12 | delay = timestamp - Time.current.to_f
13 | Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay
14 | end
15 | end
16 |
17 | class JobWrapper
18 | class << self
19 | def perform(job_data)
20 | Base.execute job_data
21 | end
22 | end
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/qu_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'qu'
2 |
3 | module ActiveJob
4 | module QueueAdapters
5 | class QuAdapter
6 | class << self
7 | def enqueue(job, *args)
8 | Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload|
9 | payload.instance_variable_set(:@queue, job.queue_name)
10 | end.push
11 | end
12 |
13 | def enqueue_at(job, timestamp, *args)
14 | raise NotImplementedError
15 | end
16 | end
17 |
18 | class JobWrapper < Qu::Job
19 | def initialize(job_data)
20 | @job_data = job_data
21 | end
22 |
23 | def perform
24 | Base.execute @job_data
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/jobs/rescue_job.rb:
--------------------------------------------------------------------------------
1 | require_relative '../support/job_buffer'
2 |
3 | class RescueJob < ActiveJob::Base
4 | class OtherError < StandardError; end
5 |
6 | rescue_from(ArgumentError) do
7 | JobBuffer.add('rescued from ArgumentError')
8 | arguments[0] = "DIFFERENT!"
9 | retry_job
10 | end
11 |
12 | rescue_from(ActiveJob::DeserializationError) do |e|
13 | JobBuffer.add('rescued from DeserializationError')
14 | JobBuffer.add("DeserializationError original exception was #{e.original_exception.class.name}")
15 | end
16 |
17 | def perform(person = "david")
18 | case person
19 | when "david"
20 | raise ArgumentError, "Hair too good"
21 | when "other"
22 | raise OtherError
23 | else
24 | JobBuffer.add('performed beautifully')
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/global_id/railtie.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require 'rails/railtie'
3 | rescue LoadError
4 | else
5 | require 'global_id'
6 | require 'active_support'
7 | require 'active_support/core_ext/string/inflections'
8 |
9 | class GlobalID
10 | # = GlobalID Railtie
11 | # Set up the signed GlobalID verifier and include Active Record support.
12 | class Railtie < Rails::Railtie # :nodoc:
13 | config.global_id = ActiveSupport::OrderedOptions.new
14 |
15 | initializer 'global_id' do |app|
16 | app.config.global_id.app ||= app.railtie_name.sub('_application', '').dasherize
17 | GlobalID.app = app.config.global_id.app
18 |
19 | ActiveSupport.on_load(:active_record) do
20 | require 'global_id/identification'
21 | send :include, GlobalID::Identification
22 | end
23 | end
24 | end
25 | end
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | # from load_paths.rb
2 | require 'bundler'
3 | Bundler.setup
4 |
5 | require 'active_job'
6 | require 'support/job_buffer'
7 |
8 | GlobalID.app = 'aj'
9 |
10 | @adapter = ENV['AJADAPTER'] || 'inline'
11 |
12 | def sidekiq?
13 | @adapter == 'sidekiq'
14 | end
15 |
16 | def ruby_193?
17 | RUBY_VERSION == '1.9.3' && RUBY_ENGINE != 'java'
18 | end
19 |
20 | # Sidekiq doesn't work with MRI 1.9.3
21 | exit if sidekiq? && ruby_193?
22 |
23 | if ENV['AJ_INTEGRATION_TESTS']
24 | require 'support/integration/helper'
25 | else
26 | require "adapters/#{@adapter}"
27 | end
28 |
29 | require 'active_support/testing/autorun'
30 |
31 | ActiveJob::Base.logger.level = Logger::DEBUG
32 |
33 | if !Date.respond_to?(:noon)
34 | class Date
35 | def noon
36 | in_time_zone.change(:hour => 12)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/sneakers_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'sneakers'
2 | require 'thread'
3 |
4 | module ActiveJob
5 | module QueueAdapters
6 | class SneakersAdapter
7 | @monitor = Monitor.new
8 |
9 | class << self
10 | def enqueue(job)
11 | @monitor.synchronize do
12 | JobWrapper.from_queue job.queue_name
13 | JobWrapper.enqueue ActiveSupport::JSON.encode(job.serialize)
14 | end
15 | end
16 |
17 | def enqueue_at(job, timestamp)
18 | raise NotImplementedError
19 | end
20 | end
21 |
22 | class JobWrapper
23 | include Sneakers::Worker
24 | from_queue 'default'
25 |
26 | def work(msg)
27 | job_data = ActiveSupport::JSON.decode(msg)
28 | Base.execute job_data
29 | ack!
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'active_job/queue_adapters/inline_adapter'
2 | require 'active_support/core_ext/string/inflections'
3 |
4 | module ActiveJob
5 | module QueueAdapter
6 | extend ActiveSupport::Concern
7 |
8 | included do
9 | self.queue_adapter = ActiveJob::QueueAdapters::InlineAdapter
10 | end
11 |
12 | module ClassMethods
13 | mattr_reader(:queue_adapter)
14 |
15 | def queue_adapter=(name_or_adapter)
16 | @@queue_adapter = \
17 | case name_or_adapter
18 | when :test
19 | ActiveJob::QueueAdapters::TestAdapter.new
20 | when Symbol, String
21 | load_adapter(name_or_adapter)
22 | when Class
23 | name_or_adapter
24 | end
25 | end
26 |
27 | private
28 | def load_adapter(name)
29 | "ActiveJob::QueueAdapters::#{name.to_s.camelize}Adapter".constantize
30 | end
31 | end
32 | end
33 | end
--------------------------------------------------------------------------------
/test/jobs/callback_job.rb:
--------------------------------------------------------------------------------
1 | class CallbackJob < ActiveJob::Base
2 | before_perform ->(job) { job.history << "CallbackJob ran before_perform" }
3 | after_perform ->(job) { job.history << "CallbackJob ran after_perform" }
4 |
5 | before_enqueue ->(job) { job.history << "CallbackJob ran before_enqueue" }
6 | after_enqueue ->(job) { job.history << "CallbackJob ran after_enqueue" }
7 |
8 | around_perform :around_perform
9 | around_enqueue :around_enqueue
10 |
11 |
12 | def perform(person = "david")
13 | # NOTHING!
14 | end
15 |
16 | def history
17 | @history ||= []
18 | end
19 |
20 | # FIXME: Not sure why these can't be declared inline like before/after
21 | def around_perform
22 | history << "CallbackJob ran around_perform_start"
23 | yield
24 | history << "CallbackJob ran around_perform_stop"
25 | end
26 |
27 | def around_enqueue
28 | history << "CallbackJob ran around_enqueue_start"
29 | yield
30 | history << "CallbackJob ran around_enqueue_stop"
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/activejob_backport.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'active_job/version'
5 |
6 | Gem::Specification.new do |s|
7 | s.platform = Gem::Platform::RUBY
8 | s.name = 'activejob_backport'
9 | s.version = ActiveJob::VERSION::STRING
10 | s.summary = 'Job framework with pluggable queues.'
11 | s.description = 'Declare job classes that can be run by a variety of queueing backends.'
12 |
13 | s.required_ruby_version = '>= 1.9.3'
14 |
15 | s.license = 'MIT'
16 |
17 | s.author = 'David Heinemeier Hansson'
18 | s.email = 'david@loudthinking.com'
19 | s.homepage = 'https://github.com/ankane/activejob_backport'
20 |
21 | s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.md', 'lib/**/*']
22 | s.require_path = 'lib'
23 |
24 | s.add_dependency 'activesupport', '>= 4.0.0'
25 |
26 | s.add_development_dependency 'bundler', '~> 1.6'
27 | s.add_development_dependency 'rake'
28 | end
29 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/qu.rb:
--------------------------------------------------------------------------------
1 | module QuJobsManager
2 | def setup
3 | require 'qu-rails'
4 | require 'qu-redis'
5 | ActiveJob::Base.queue_adapter = :qu
6 | ENV['REDISTOGO_URL'] = "tcp://127.0.0.1:6379/12"
7 | backend = Qu::Backend::Redis.new
8 | backend.namespace = "active_jobs_int_test"
9 | Qu.backend = backend
10 | Qu.logger = Rails.logger
11 | Qu.interval = 0.5
12 | unless can_run?
13 | puts "Cannot run integration tests for qu. To be able to run integration tests for qu you need to install and start redis.\n"
14 | exit
15 | end
16 | end
17 |
18 | def clear_jobs
19 | Qu.clear "integration_tests"
20 | end
21 |
22 | def start_workers
23 | @thread = Thread.new { Qu::Worker.new("integration_tests").start }
24 | end
25 |
26 | def stop_workers
27 | @thread.kill
28 | end
29 |
30 | def can_run?
31 | begin
32 | Qu.backend.connection.client.connect
33 | rescue => e
34 | return false
35 | end
36 | true
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/sidekiq_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'sidekiq'
2 |
3 | module ActiveJob
4 | module QueueAdapters
5 | class SidekiqAdapter
6 | class << self
7 | def enqueue(job)
8 | #Sidekiq::Client does not support symbols as keys
9 | Sidekiq::Client.push \
10 | 'class' => JobWrapper,
11 | 'wrapped' => job.class.to_s,
12 | 'queue' => job.queue_name,
13 | 'args' => [ job.serialize ],
14 | 'retry' => true
15 | end
16 |
17 | def enqueue_at(job, timestamp)
18 | Sidekiq::Client.push \
19 | 'class' => JobWrapper,
20 | 'wrapped' => job.class.to_s,
21 | 'queue' => job.queue_name,
22 | 'args' => [ job.serialize ],
23 | 'retry' => true,
24 | 'at' => timestamp
25 | end
26 | end
27 |
28 | class JobWrapper
29 | include Sidekiq::Worker
30 |
31 | def perform(job_data)
32 | Base.execute job_data
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/backburner.rb:
--------------------------------------------------------------------------------
1 | module BackburnerJobsManager
2 | def setup
3 | ActiveJob::Base.queue_adapter = :backburner
4 | Backburner.configure do |config|
5 | config.logger = Rails.logger
6 | end
7 | unless can_run?
8 | puts "Cannot run integration tests for backburner. To be able to run integration tests for backburner you need to install and start beanstalkd.\n"
9 | exit
10 | end
11 | end
12 |
13 | def clear_jobs
14 | tube.clear
15 | end
16 |
17 | def start_workers
18 | @thread = Thread.new { Backburner.work "integration-tests" } # backburner dasherizes the queue name
19 | end
20 |
21 | def stop_workers
22 | @thread.kill
23 | end
24 |
25 | def tube
26 | @tube ||= Beaneater::Tube.new(Backburner::Worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name
27 | end
28 |
29 | def can_run?
30 | begin
31 | Backburner::Worker.connection.send :connect!
32 | rescue => e
33 | return false
34 | end
35 | true
36 | end
37 | end
38 |
39 |
--------------------------------------------------------------------------------
/test/cases/callbacks_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'jobs/callback_job'
3 |
4 | require 'active_support/core_ext/object/inclusion'
5 |
6 | class CallbacksTest < ActiveSupport::TestCase
7 | test 'perform callbacks' do
8 | performed_callback_job = CallbackJob.new("A-JOB-ID")
9 | performed_callback_job.perform_now
10 | assert "CallbackJob ran before_perform".in? performed_callback_job.history
11 | assert "CallbackJob ran after_perform".in? performed_callback_job.history
12 | assert "CallbackJob ran around_perform_start".in? performed_callback_job.history
13 | assert "CallbackJob ran around_perform_stop".in? performed_callback_job.history
14 | end
15 |
16 | test 'enqueue callbacks' do
17 | enqueued_callback_job = CallbackJob.perform_later
18 | assert "CallbackJob ran before_enqueue".in? enqueued_callback_job.history
19 | assert "CallbackJob ran after_enqueue".in? enqueued_callback_job.history
20 | assert "CallbackJob ran around_enqueue_start".in? enqueued_callback_job.history
21 | assert "CallbackJob ran around_enqueue_stop".in? enqueued_callback_job.history
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 David Heinemeier Hansson
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/lib/active_job/execution.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/rescuable'
2 | require 'active_job/arguments'
3 |
4 | module ActiveJob
5 | module Execution
6 | extend ActiveSupport::Concern
7 | include ActiveSupport::Rescuable
8 |
9 | module ClassMethods
10 | # Performs the job immediately.
11 | #
12 | # MyJob.perform_now("mike")
13 | #
14 | def perform_now(*args)
15 | job_or_instantiate(*args).perform_now
16 | end
17 |
18 | def execute(job_data) #:nodoc:
19 | job = deserialize(job_data)
20 | job.perform_now
21 | end
22 | end
23 |
24 | # Performs the job immediately. The job is not sent to the queueing adapter
25 | # and will block the execution until it's finished.
26 | #
27 | # MyJob.new(*args).perform_now
28 | def perform_now
29 | deserialize_arguments_if_needed
30 | run_callbacks :perform do
31 | perform(*arguments)
32 | end
33 | rescue => exception
34 | rescue_with_handler(exception) || raise(exception)
35 | end
36 |
37 | def perform(*)
38 | fail NotImplementedError
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/support/integration/test_case_helpers.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 | require 'support/integration/jobs_manager'
3 |
4 | module TestCaseHelpers
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | self.use_transactional_fixtures = false
9 |
10 | setup do
11 | clear_jobs
12 | @id = "AJ-#{SecureRandom.uuid}"
13 | end
14 |
15 | teardown do
16 | clear_jobs
17 | end
18 | end
19 |
20 | protected
21 |
22 | def jobs_manager
23 | JobsManager.current_manager
24 | end
25 |
26 | def clear_jobs
27 | jobs_manager.clear_jobs
28 | end
29 |
30 | def adapter_is?(adapter)
31 | ActiveJob::Base.queue_adapter.name.split("::").last.gsub(/Adapter$/, '').underscore==adapter.to_s
32 | end
33 |
34 | def wait_for_jobs_to_finish_for(seconds=60)
35 | begin
36 | Timeout.timeout(seconds) do
37 | while !job_executed do
38 | sleep 0.25
39 | end
40 | end
41 | rescue Timeout::Error
42 | end
43 | end
44 |
45 | def job_executed
46 | Dummy::Application.root.join("tmp/#{@id}").exist?
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/active_job/queue_name.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | module QueueName
3 | extend ActiveSupport::Concern
4 |
5 | module ClassMethods
6 | mattr_accessor(:queue_name_prefix)
7 | mattr_accessor(:default_queue_name)
8 |
9 | def queue_as(part_name=nil, &block)
10 | if block_given?
11 | self.queue_name = block
12 | else
13 | self.queue_name = queue_name_from_part(part_name)
14 | end
15 | end
16 |
17 | def queue_name_from_part(part_name) #:nodoc:
18 | queue_name = part_name.to_s.presence || default_queue_name
19 | name_parts = [queue_name_prefix.presence, queue_name]
20 | name_parts.compact.join('_')
21 | end
22 | end
23 |
24 | included do
25 | class_attribute :queue_name, instance_accessor: false
26 | self.default_queue_name = "default"
27 | self.queue_name = default_queue_name
28 | end
29 |
30 | # Returns the name of the queue the job will be run on
31 | def queue_name
32 | if @queue_name.is_a?(Proc)
33 | @queue_name = self.class.queue_name_from_part(instance_exec(&@queue_name))
34 | end
35 | @queue_name
36 | end
37 |
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/que.rb:
--------------------------------------------------------------------------------
1 | module QueJobsManager
2 | def setup
3 | require 'sequel'
4 | ActiveJob::Base.queue_adapter = :que
5 | que_url = ENV['QUE_DATABASE_URL'] || 'postgres://localhost/active_jobs_que_int_test'
6 | uri = URI.parse(que_url)
7 | user = uri.user||ENV['USER']
8 | pass = uri.password
9 | db = uri.path[1..-1]
10 | %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database "#{db}"' -U #{user} -t template1}
11 | %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1}
12 | Que.connection = Sequel.connect(que_url)
13 | Que.migrate!
14 | Que.mode = :off
15 | Que.worker_count = 1
16 | rescue Sequel::DatabaseConnectionError
17 | puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n"
18 | exit
19 | end
20 |
21 | def clear_jobs
22 | Que.clear!
23 | end
24 |
25 | def start_workers
26 | @thread = Thread.new do
27 | loop do
28 | Que::Job.work("integration_tests")
29 | sleep 0.5
30 | end
31 | end
32 | end
33 |
34 | def stop_workers
35 | @thread.kill
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/resque_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'resque'
2 | require 'active_support/core_ext/enumerable'
3 | require 'active_support/core_ext/array/access'
4 |
5 | begin
6 | require 'resque-scheduler'
7 | rescue LoadError
8 | begin
9 | require 'resque_scheduler'
10 | rescue LoadError
11 | false
12 | end
13 | end
14 |
15 | module ActiveJob
16 | module QueueAdapters
17 | class ResqueAdapter
18 | class << self
19 | def enqueue(job)
20 | Resque.enqueue_to job.queue_name, JobWrapper, job.serialize
21 | end
22 |
23 | def enqueue_at(job, timestamp)
24 | unless Resque.respond_to?(:enqueue_at_with_queue)
25 | raise NotImplementedError, "To be able to schedule jobs with Resque you need the " \
26 | "resque-scheduler gem. Please add it to your Gemfile and run bundle install"
27 | end
28 | Resque.enqueue_at_with_queue job.queue_name, timestamp, JobWrapper, job.serialize
29 | end
30 | end
31 |
32 | class JobWrapper
33 | class << self
34 | def perform(job_data)
35 | Base.execute job_data
36 | end
37 | end
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/cases/queuing_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'jobs/hello_job'
3 | require 'active_support/core_ext/numeric/time'
4 |
5 |
6 | class QueuingTest < ActiveSupport::TestCase
7 | setup do
8 | JobBuffer.clear
9 | end
10 |
11 | test 'run queued job' do
12 | HelloJob.perform_later
13 | assert_equal "David says hello", JobBuffer.last_value
14 | end
15 |
16 | test 'run queued job with arguments' do
17 | HelloJob.perform_later "Jamie"
18 | assert_equal "Jamie says hello", JobBuffer.last_value
19 | end
20 |
21 | test 'run queued job later' do
22 | begin
23 | result = HelloJob.set(wait_until: 1.second.ago).perform_later "Jamie"
24 | assert result
25 | rescue NotImplementedError
26 | skip
27 | end
28 | end
29 |
30 | test 'job returned by enqueue has the arguments available' do
31 | job = HelloJob.perform_later "Jamie"
32 | assert_equal [ "Jamie" ], job.arguments
33 | end
34 |
35 |
36 | test 'job returned by perform_at has the timestamp available' do
37 | begin
38 | job = HelloJob.set(wait_until: Time.utc(2014, 1, 1)).perform_later
39 | assert_equal Time.utc(2014, 1, 1).to_f, job.scheduled_at
40 | rescue NotImplementedError
41 | skip
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/cases/rescue_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'jobs/rescue_job'
3 | require 'models/person'
4 |
5 | require 'active_support/core_ext/object/inclusion'
6 |
7 | class RescueTest < ActiveSupport::TestCase
8 | setup do
9 | JobBuffer.clear
10 | end
11 |
12 | test 'rescue perform exception with retry' do
13 | job = RescueJob.new("david")
14 | job.perform_now
15 | assert_equal [ "rescued from ArgumentError", "performed beautifully" ], JobBuffer.values
16 | end
17 |
18 | test 'let through unhandled perform exception' do
19 | job = RescueJob.new("other")
20 | assert_raises(RescueJob::OtherError) do
21 | job.perform_now
22 | end
23 | end
24 |
25 | test 'rescue from deserialization errors' do
26 | RescueJob.perform_later Person.new(404)
27 | assert_includes JobBuffer.values, 'rescued from DeserializationError'
28 | assert_includes JobBuffer.values, 'DeserializationError original exception was Person::RecordNotFound'
29 | assert_not_includes JobBuffer.values, 'performed beautifully'
30 | end
31 |
32 | test "should not wrap DeserializationError in DeserializationError" do
33 | RescueJob.perform_later [Person.new(404)]
34 | assert_includes JobBuffer.values, 'DeserializationError original exception was Person::RecordNotFound'
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/test_adapter.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | module QueueAdapters
3 | class TestAdapter
4 | delegate :name, to: :class
5 | attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs)
6 | attr_writer(:enqueued_jobs, :performed_jobs)
7 |
8 | # Provides a store of all the enqueued jobs with the TestAdapter so you can check them.
9 | def enqueued_jobs
10 | @enqueued_jobs ||= []
11 | end
12 |
13 | # Provides a store of all the performed jobs with the TestAdapter so you can check them.
14 | def performed_jobs
15 | @performed_jobs ||= []
16 | end
17 |
18 | def enqueue(job)
19 | if perform_enqueued_jobs
20 | performed_jobs << {job: job.class, args: job.arguments, queue: job.queue_name}
21 | job.perform_now
22 | else
23 | enqueued_jobs << {job: job.class, args: job.arguments, queue: job.queue_name}
24 | end
25 | end
26 |
27 | def enqueue_at(job, timestamp)
28 | if perform_enqueued_at_jobs
29 | performed_jobs << {job: job.class, args: job.arguments, queue: job.queue_name, at: timestamp}
30 | job.perform_now
31 | else
32 | enqueued_jobs << {job: job.class, args: job.arguments, queue: job.queue_name, at: timestamp}
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/integration/queuing_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'jobs/logging_job'
3 | require 'active_support/core_ext/numeric/time'
4 |
5 | class QueuingTest < ActiveSupport::TestCase
6 | test 'should run jobs enqueued on a listenting queue' do
7 | TestJob.perform_later @id
8 | wait_for_jobs_to_finish_for(5.seconds)
9 | assert job_executed
10 | end
11 |
12 | test 'should not run jobs queued on a non-listenting queue' do
13 | begin
14 | skip if adapter_is?(:inline) || adapter_is?(:sucker_punch)
15 | old_queue = TestJob.queue_name
16 | TestJob.queue_as :some_other_queue
17 | TestJob.perform_later @id
18 | wait_for_jobs_to_finish_for(2.seconds)
19 | assert_not job_executed
20 | ensure
21 | TestJob.queue_name = old_queue
22 | end
23 | end
24 |
25 | test 'should not run job enqueued in the future' do
26 | begin
27 | TestJob.set(wait: 10.minutes).perform_later @id
28 | wait_for_jobs_to_finish_for(5.seconds)
29 | assert_not job_executed
30 | rescue NotImplementedError
31 | skip
32 | end
33 | end
34 |
35 | test 'should run job enqueued in the future at the specified time' do
36 | begin
37 | TestJob.set(wait: 3.seconds).perform_later @id
38 | wait_for_jobs_to_finish_for(2.seconds)
39 | assert_not job_executed
40 | wait_for_jobs_to_finish_for(10.seconds)
41 | assert job_executed
42 | rescue NotImplementedError
43 | skip
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/resque.rb:
--------------------------------------------------------------------------------
1 | module ResqueJobsManager
2 | def setup
3 | ActiveJob::Base.queue_adapter = :resque
4 | Resque.redis = Redis::Namespace.new 'active_jobs_int_test', redis: Redis.connect(url: "tcp://127.0.0.1:6379/12", :thread_safe => true)
5 | Resque.logger = Rails.logger
6 | unless can_run?
7 | puts "Cannot run integration tests for resque. To be able to run integration tests for resque you need to install and start redis.\n"
8 | exit
9 | end
10 | end
11 |
12 | def clear_jobs
13 | Resque.queues.each { |queue_name| Resque.redis.del "queue:#{queue_name}" }
14 | Resque.redis.keys("delayed:*").each { |key| Resque.redis.del "#{key}" }
15 | Resque.redis.del "delayed_queue_schedule"
16 | end
17 |
18 | def start_workers
19 | @resque_thread = Thread.new do
20 | Resque::Worker.new("integration_tests").work(0.5)
21 | end
22 | @scheduler_thread = Thread.new do
23 | Resque::Scheduler.configure do |c|
24 | c.poll_sleep_amount = 0.5
25 | c.dynamic = true
26 | c.verbose = true
27 | c.logfile = nil
28 | end
29 | Resque::Scheduler.master_lock.release!
30 | Resque::Scheduler.run
31 | end
32 | end
33 |
34 | def stop_workers
35 | @resque_thread.kill
36 | @scheduler_thread.kill
37 | end
38 |
39 | def can_run?
40 | begin
41 | Resque.redis.client.connect
42 | rescue => e
43 | return false
44 | end
45 | true
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/active_job/queue_adapters/queue_classic_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'queue_classic'
2 |
3 | module ActiveJob
4 | module QueueAdapters
5 | class QueueClassicAdapter
6 | class << self
7 | def enqueue(job)
8 | build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize)
9 | end
10 |
11 | def enqueue_at(job, timestamp)
12 | queue = build_queue(job.queue_name)
13 | unless queue.respond_to?(:enqueue_at)
14 | raise NotImplementedError, 'To be able to schedule jobs with Queue Classic ' \
15 | 'the QC::Queue needs to respond to `enqueue_at(timestamp, method, *args)`. '
16 | 'You can implement this yourself or you can use the queue_classic-later gem.'
17 | end
18 | queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize)
19 | end
20 |
21 | # Builds a QC::Queue object to schedule jobs on.
22 | #
23 | # If you have a custom QC::Queue subclass you'll need to suclass
24 | # ActiveJob::QueueAdapters::QueueClassicAdapter and override the
25 | # build_queue method.
26 | def build_queue(queue_name)
27 | QC::Queue.new(queue_name)
28 | end
29 | end
30 |
31 | class JobWrapper
32 | class << self
33 | def perform(job_data)
34 | Base.execute job_data
35 | end
36 | end
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/queue_classic.rb:
--------------------------------------------------------------------------------
1 | module QueueClassicJobsManager
2 | def setup
3 | ENV['QC_DATABASE_URL'] ||= 'postgres://localhost/active_jobs_qc_int_test'
4 | ENV['QC_LISTEN_TIME'] = "0.5"
5 | uri = URI.parse(ENV['QC_DATABASE_URL'])
6 | user = uri.user||ENV['USER']
7 | pass = uri.password
8 | db = uri.path[1..-1]
9 | %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database "#{db}"' -U #{user} -t template1}
10 | %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1}
11 | ActiveJob::Base.queue_adapter = :queue_classic
12 | QC::Setup.create
13 | rescue PG::ConnectionBad
14 | puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n"
15 | exit
16 | end
17 |
18 | def clear_jobs
19 | QC::Queue.new("integration_tests").delete_all
20 | retried = false
21 | rescue => e
22 | puts "Got exception while trying to clear jobs: #{e.inspect}"
23 | if retried
24 | puts "Already retried. Raising exception"
25 | raise e
26 | else
27 | puts "Retrying"
28 | retried = true
29 | QC::Conn.connection = QC::Conn.connect
30 | retry
31 | end
32 | end
33 |
34 | def start_workers
35 | @pid = fork do
36 | QC::Conn.connection = QC::Conn.connect
37 | worker = QC::Worker.new(q_name: 'integration_tests')
38 | worker.start
39 | end
40 | end
41 |
42 | def stop_workers
43 | Process.kill 'HUP', @pid
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/active_job.rb:
--------------------------------------------------------------------------------
1 | #--
2 | # Copyright (c) 2014 David Heinemeier Hansson
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files (the
6 | # "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to
9 | # permit persons to whom the Software is furnished to do so, subject to
10 | # the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | #++
23 |
24 | require 'active_support'
25 | require 'active_support/rails'
26 | require 'active_job/version'
27 | require 'global_id' if !defined?(GlobalId)
28 | require 'global_id/railtie' if defined?(Rails)
29 | require 'active_job/railtie' if defined?(Rails)
30 | require 'active_support/core_ext/module/attribute_accessors'
31 |
32 | module ActiveJob
33 | extend ActiveSupport::Autoload
34 |
35 | autoload :Base
36 | autoload :QueueAdapters
37 | autoload :ConfiguredJob
38 | autoload :TestCase
39 | autoload :TestHelper
40 | end
41 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/sidekiq.rb:
--------------------------------------------------------------------------------
1 | require 'sidekiq/cli'
2 | require 'sidekiq/api'
3 |
4 | module SidekiqJobsManager
5 |
6 | def setup
7 | ActiveJob::Base.queue_adapter = :sidekiq
8 | unless can_run?
9 | puts "Cannot run integration tests for sidekiq. To be able to run integration tests for sidekiq you need to install and start redis.\n"
10 | exit
11 | end
12 | end
13 |
14 | def clear_jobs
15 | Sidekiq::ScheduledSet.new.clear
16 | Sidekiq::Queue.new("integration_tests").clear
17 | end
18 |
19 | def start_workers
20 | fork do
21 | sidekiq = Sidekiq::CLI.instance
22 | logfile = Rails.root.join("log/sidekiq.log").to_s
23 | pidfile = Rails.root.join("tmp/sidekiq.pid").to_s
24 | sidekiq.parse([ "--require", Rails.root.to_s,
25 | "--queue", "integration_tests",
26 | "--logfile", logfile,
27 | "--pidfile", pidfile,
28 | "--environment", "test",
29 | "--concurrency", "1",
30 | "--timeout", "1",
31 | "--daemon",
32 | "--verbose"
33 | ])
34 | require 'celluloid'
35 | require 'sidekiq/scheduled'
36 | Sidekiq.poll_interval = 0.5
37 | Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1
38 | sidekiq.run
39 | end
40 | sleep 1
41 | end
42 |
43 | def stop_workers
44 | pidfile = Rails.root.join("tmp/sidekiq.pid").to_s
45 | Process.kill 'TERM', File.open(pidfile).read.to_i
46 | FileUtils.rm_f pidfile
47 | rescue
48 | end
49 |
50 | def can_run?
51 | begin
52 | Sidekiq.redis { |conn| conn.connect }
53 | rescue => e
54 | return false
55 | end
56 | true
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/test/cases/queue_naming_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'jobs/hello_job'
3 | require 'jobs/logging_job'
4 | require 'jobs/nested_job'
5 |
6 | class QueueNamingTest < ActiveSupport::TestCase
7 | test 'name derived from base' do
8 | assert_equal "default", HelloJob.queue_name
9 | end
10 |
11 | test 'uses given queue name job' do
12 | begin
13 | original_queue_name = HelloJob.queue_name
14 | HelloJob.queue_as :greetings
15 | assert_equal "greetings", HelloJob.new.queue_name
16 | ensure
17 | HelloJob.queue_name = original_queue_name
18 | end
19 | end
20 |
21 | test 'evals block given to queue_as to determine queue' do
22 | begin
23 | original_queue_name = HelloJob.queue_name
24 | HelloJob.queue_as { :another }
25 | assert_equal "another", HelloJob.new.queue_name
26 | ensure
27 | HelloJob.queue_name = original_queue_name
28 | end
29 | end
30 |
31 | test 'can use arguments to determine queue_name in queue_as block' do
32 | begin
33 | original_queue_name = HelloJob.queue_name
34 | HelloJob.queue_as { self.arguments.first=='1' ? :one : :two }
35 | assert_equal "one", HelloJob.new('1').queue_name
36 | assert_equal "two", HelloJob.new('3').queue_name
37 | ensure
38 | HelloJob.queue_name = original_queue_name
39 | end
40 | end
41 |
42 | test 'queu_name_prefix prepended to the queue name' do
43 | begin
44 | original_queue_name_prefix = ActiveJob::Base.queue_name_prefix
45 | original_queue_name = HelloJob.queue_name
46 |
47 | ActiveJob::Base.queue_name_prefix = 'aj'
48 | HelloJob.queue_as :low
49 | assert_equal 'aj_low', HelloJob.queue_name
50 | ensure
51 | ActiveJob::Base.queue_name_prefix = original_queue_name_prefix
52 | HelloJob.queue_name = original_queue_name
53 | end
54 | end
55 |
56 | test 'uses queue passed to #set' do
57 | job = HelloJob.set(queue: :some_queue).perform_later
58 | assert_equal "some_queue", job.queue_name
59 | end
60 |
61 | end
62 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 | require 'rake/testtask'
3 |
4 | ACTIVEJOB_ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner)
5 | ACTIVEJOB_ADAPTERS -= %w(queue_classic) if defined?(JRUBY_VERSION)
6 |
7 | task default: :test
8 | task test: 'test:default'
9 |
10 | namespace :test do
11 | desc 'Run all adapter tests'
12 | task :default do
13 | run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:#{a}" }
14 | end
15 |
16 | desc 'Run all adapter tests in isolation'
17 | task :isolated do
18 | run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:isolated:#{a}" }
19 | end
20 |
21 | desc 'Run integration tests for all adapters'
22 | task :integration do
23 | run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:integration:#{a}" }
24 | end
25 |
26 | task 'env:integration' do
27 | ENV['AJ_INTEGRATION_TESTS'] = "1"
28 | end
29 |
30 | ACTIVEJOB_ADAPTERS.each do |adapter|
31 | task("env:#{adapter}") { ENV['AJADAPTER'] = adapter }
32 |
33 | Rake::TestTask.new(adapter => "test:env:#{adapter}") do |t|
34 | t.description = "Run adapter tests for #{adapter}"
35 | t.libs << 'test'
36 | t.test_files = FileList['test/cases/**/*_test.rb']
37 | t.verbose = true
38 | end
39 |
40 | namespace :isolated do
41 | task adapter => "test:env:#{adapter}" do
42 | dir = File.dirname(__FILE__)
43 | Dir.glob("#{dir}/test/cases/**/*_test.rb").all? do |file|
44 | sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
45 | end or raise 'Failures'
46 | end
47 | end
48 |
49 | namespace :integration do
50 | Rake::TestTask.new(adapter => ["test:env:#{adapter}", 'test:env:integration']) do |t|
51 | t.description = "Run integration tests for #{adapter}"
52 | t.libs << 'test'
53 | t.test_files = FileList['test/integration/**/*_test.rb']
54 | t.verbose = true
55 | end
56 | end
57 | end
58 | end
59 |
60 | def run_without_aborting(tasks)
61 | errors = []
62 |
63 | tasks.each do |task|
64 | begin
65 | Rake::Task[task].invoke
66 | rescue Exception
67 | errors << task
68 | end
69 | end
70 |
71 | abort "Errors running #{errors.join(', ')}" if errors.any?
72 | end
73 |
--------------------------------------------------------------------------------
/test/cases/argument_serialization_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'active_job/arguments'
3 | require 'models/person'
4 | require 'active_support/core_ext/hash/indifferent_access'
5 |
6 | class ArgumentSerializationTest < ActiveSupport::TestCase
7 | setup do
8 | @person = Person.find('5')
9 | end
10 |
11 | [ nil, 1, 1.0, 1_000_000_000_000_000_000_000,
12 | 'a', true, false,
13 | [ 1, 'a' ],
14 | { 'a' => 1 }
15 | ].each do |arg|
16 | test "serializes #{arg.class} verbatim" do
17 | assert_arguments_unchanged arg
18 | end
19 | end
20 |
21 | [ :a, Object.new, self, Person.find('5').to_gid ].each do |arg|
22 | test "does not serialize #{arg.class}" do
23 | assert_raises ActiveJob::SerializationError do
24 | ActiveJob::Arguments.serialize [ arg ]
25 | end
26 |
27 | assert_raises ActiveJob::DeserializationError do
28 | ActiveJob::Arguments.deserialize [ arg ]
29 | end
30 | end
31 | end
32 |
33 | test 'should convert records to Global IDs' do
34 | assert_arguments_roundtrip [@person], [@person.to_gid.to_s]
35 | end
36 |
37 | test 'should dive deep into arrays and hashes' do
38 | assert_arguments_roundtrip [3, [@person]], [3, [@person.to_gid.to_s]]
39 | assert_arguments_roundtrip [{ 'a' => @person }], [{ 'a' => @person.to_gid.to_s }.with_indifferent_access]
40 | end
41 |
42 | test 'should stringify symbol hash keys' do
43 | assert_equal [ 'a' => 1 ], ActiveJob::Arguments.serialize([ a: 1 ])
44 | end
45 |
46 | test 'should disallow non-string/symbol hash keys' do
47 | assert_raises ActiveJob::SerializationError do
48 | ActiveJob::Arguments.serialize [ { 1 => 2 } ]
49 | end
50 |
51 | assert_raises ActiveJob::SerializationError do
52 | ActiveJob::Arguments.serialize [ { :a => [{ 2 => 3 }] } ]
53 | end
54 | end
55 |
56 | test 'should not allow non-primitive objects' do
57 | assert_raises ActiveJob::SerializationError do
58 | ActiveJob::Arguments.serialize [Object.new]
59 | end
60 |
61 | assert_raises ActiveJob::SerializationError do
62 | ActiveJob::Arguments.serialize [1, [Object.new]]
63 | end
64 | end
65 |
66 | private
67 | def assert_arguments_unchanged(*args)
68 | assert_arguments_roundtrip args, args
69 | end
70 |
71 | def assert_arguments_roundtrip(args, expected_serialized_args)
72 | serialized = ActiveJob::Arguments.serialize(args)
73 | assert_equal expected_serialized_args, serialized
74 | assert_equal args, ActiveJob::Arguments.deserialize(serialized)
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/test/support/integration/adapters/sneakers.rb:
--------------------------------------------------------------------------------
1 | require 'sneakers/runner'
2 | require 'sneakers/publisher'
3 | require 'timeout'
4 |
5 | module Sneakers
6 | class Publisher
7 | def safe_ensure_connected
8 | @mutex.synchronize do
9 | ensure_connection! unless connected?
10 | end
11 | end
12 | end
13 | end
14 |
15 |
16 | module SneakersJobsManager
17 | def setup
18 | ActiveJob::Base.queue_adapter = :sneakers
19 | Sneakers.configure :heartbeat => 2,
20 | :amqp => 'amqp://guest:guest@localhost:5672',
21 | :vhost => '/',
22 | :exchange => 'active_jobs_sneakers_int_test',
23 | :exchange_type => :direct,
24 | :daemonize => true,
25 | :threads => 1,
26 | :workers => 1,
27 | :pid_path => Rails.root.join("tmp/sneakers.pid").to_s,
28 | :log => Rails.root.join("log/sneakers.log").to_s
29 | unless can_run?
30 | puts "Cannot run integration tests for sneakers. To be able to run integration tests for sneakers you need to install and start rabbitmq.\n"
31 | exit
32 | end
33 | end
34 |
35 | def clear_jobs
36 | bunny_queue.purge
37 | end
38 |
39 | def start_workers
40 | @pid = fork do
41 | queues = %w(integration_tests)
42 | workers = queues.map do |q|
43 | worker_klass = "ActiveJobWorker"+Digest::MD5.hexdigest(q)
44 | Sneakers.const_set(worker_klass, Class.new(ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper) do
45 | from_queue q
46 | end)
47 | end
48 | Sneakers::Runner.new(workers).run
49 | end
50 | begin
51 | Timeout.timeout(10) do
52 | while bunny_queue.status[:consumer_count] == 0
53 | sleep 0.5
54 | end
55 | end
56 | rescue Timeout::Error
57 | stop_workers
58 | raise "Failed to start sneakers worker"
59 | end
60 | end
61 |
62 | def stop_workers
63 | Process.kill 'TERM', @pid
64 | Process.kill 'TERM', File.open(Rails.root.join("tmp/sneakers.pid").to_s).read.to_i
65 | rescue
66 | end
67 |
68 | def can_run?
69 | begin
70 | bunny_publisher
71 | rescue => e
72 | return false
73 | end
74 | true
75 | end
76 |
77 | protected
78 | def bunny_publisher
79 | @bunny_publisher ||= begin
80 | p = ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper.send(:publisher)
81 | p.safe_ensure_connected
82 | p
83 | end
84 | end
85 |
86 | def bunny_queue
87 | @queue ||= bunny_publisher.exchange.channel.queue "integration_tests", durable: true
88 | end
89 |
90 | end
91 |
--------------------------------------------------------------------------------
/lib/global_id/locator.rb:
--------------------------------------------------------------------------------
1 | class GlobalID
2 | module Locator
3 | class << self
4 | # Takes either a GlobalID or a string that can be turned into a GlobalID
5 | #
6 | # Options:
7 | # * :only - A class, module or Array of classes and/or modules that are
8 | # allowed to be located. Passing one or more classes limits instances of returned
9 | # classes to those classes or their subclasses. Passing one or more modules in limits
10 | # instances of returned classes to those including that module. If no classes or
11 | # modules match, +nil+ is returned.
12 | def locate(gid, options = {})
13 | if gid = GlobalID.parse(gid)
14 | locator_for(gid).locate gid if find_allowed?(gid.model_class, options[:only])
15 | end
16 | end
17 |
18 | # Tie a locator to an app.
19 | # Useful when different apps collaborate and reference each others' Global IDs.
20 | #
21 | # The locator can be either a block or a class.
22 | #
23 | # Using a block:
24 | #
25 | # GlobalID::Locator.use :foo do |gid|
26 | # FooRemote.const_get(gid.model_name).find(gid.model_id)
27 | # end
28 | #
29 | # Using a class:
30 | #
31 | # GlobalID::Locator.use :bar, BarLocator.new
32 | #
33 | # class BarLocator
34 | # def locate(gid)
35 | # @search_client.search name: gid.model_name, id: gid.model_id
36 | # end
37 | # end
38 | def use(app, locator = nil, &locator_block)
39 | raise ArgumentError, 'No locator provided. Pass a block or an object that responds to #locate.' unless locator || block_given?
40 |
41 | GlobalID.validate_app(app)
42 |
43 | @locators[normalize_app(app)] = locator || BlockLocator.new(locator_block)
44 | end
45 |
46 | private
47 | def locator_for(gid)
48 | @locators.fetch(normalize_app(gid.app)) { default_locator }
49 | end
50 |
51 | def find_allowed?(model_class, only = nil)
52 | only ? Array(only).any? { |c| model_class <= c } : true
53 | end
54 |
55 | def normalize_app(app)
56 | app.to_s.downcase
57 | end
58 | end
59 |
60 | private
61 | @locators = {}
62 |
63 | class ActiveRecordFinder
64 | def locate(gid)
65 | gid.model_class.find gid.model_id
66 | end
67 | end
68 |
69 | def self.default_locator
70 | ActiveRecordFinder.new
71 | end
72 |
73 | class BlockLocator
74 | def initialize(block)
75 | @locator = block
76 | end
77 |
78 | def locate(gid)
79 | @locator.call(gid)
80 | end
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/active_job/enqueuing.rb:
--------------------------------------------------------------------------------
1 | require 'active_job/arguments'
2 |
3 | module ActiveJob
4 | module Enqueuing
5 | extend ActiveSupport::Concern
6 |
7 | module ClassMethods
8 | # Push a job onto the queue. The arguments must be legal JSON types
9 | # (string, int, float, nil, true, false, hash or array) or
10 | # GlobalID::Identification instances. Arbitrary Ruby objects
11 | # are not supported.
12 | #
13 | # Returns an instance of the job class queued with args available in
14 | # Job#arguments.
15 | def perform_later(*args)
16 | job_or_instantiate(*args).enqueue
17 | end
18 |
19 | protected
20 | def job_or_instantiate(*args)
21 | args.first.is_a?(self) ? args.first : new(*args)
22 | end
23 | end
24 |
25 | # Reschedule the job to be re-executed. This is usefull in combination
26 | # with the +rescue_from+ option. When you rescue an exception from your job
27 | # you can ask Active Job to retry performing your job.
28 | #
29 | # ==== Options
30 | # * :wait - Enqueues the job with the specified delay
31 | # * :wait_until - Enqueues the job at the time specified
32 | # * :queue - Enqueues the job on the specified queue
33 | #
34 | # ==== Examples
35 | #
36 | # class SiteScrapperJob < ActiveJob::Base
37 | # rescue_from(ErrorLoadingSite) do
38 | # retry_job queue: :low_priority
39 | # end
40 | # def perform(*args)
41 | # # raise ErrorLoadingSite if cannot scrape
42 | # end
43 | # end
44 | def retry_job(options={})
45 | enqueue options
46 | end
47 |
48 | # Equeue the job to be performed by the queue adapter.
49 | #
50 | # ==== Options
51 | # * :wait - Enqueues the job with the specified delay
52 | # * :wait_until - Enqueues the job at the time specified
53 | # * :queue - Enqueues the job on the specified queue
54 | #
55 | # ==== Examples
56 | #
57 | # my_job_instance.enqueue
58 | # my_job_instance.enqueue wait: 5.minutes
59 | # my_job_instance.enqueue queue: :important
60 | # my_job_instance.enqueue wait_until: Date.tomorrow.midnight
61 | def enqueue(options={})
62 | self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait]
63 | self.scheduled_at = options[:wait_until].to_f if options[:wait_until]
64 | self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue]
65 | run_callbacks :enqueue do
66 | if self.scheduled_at
67 | self.class.queue_adapter.enqueue_at self, self.scheduled_at
68 | else
69 | self.class.queue_adapter.enqueue self
70 | end
71 | end
72 | self
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/lib/active_job/arguments.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | # Raised when an exception is raised during job arguments deserialization.
3 | #
4 | # Wraps the original exception raised as +original_exception+.
5 | class DeserializationError < StandardError
6 | attr_reader :original_exception
7 |
8 | def initialize(e) #:nodoc:
9 | super("Error while trying to deserialize arguments: #{e.message}")
10 | @original_exception = e
11 | set_backtrace e.backtrace
12 | end
13 | end
14 |
15 | # Raised when an unsupported argument type is being set as job argument. We
16 | # currently support NilClass, Fixnum, Float, String, TrueClass, FalseClass,
17 | # Bignum and object that can be represented as GlobalIDs (ex: Active Record).
18 | # Also raised if you set the key for a Hash something else than a string or
19 | # a symbol.
20 | class SerializationError < ArgumentError
21 | end
22 |
23 | module Arguments
24 | extend self
25 | TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ]
26 |
27 | def serialize(arguments)
28 | arguments.map { |argument| serialize_argument(argument) }
29 | end
30 |
31 | def deserialize(arguments)
32 | arguments.map { |argument| deserialize_argument(argument) }
33 | rescue => e
34 | raise DeserializationError.new(e)
35 | end
36 |
37 | private
38 | def serialize_argument(argument)
39 | case argument
40 | when *TYPE_WHITELIST
41 | argument
42 | when GlobalID::Identification
43 | argument.to_global_id.to_s
44 | when Array
45 | argument.map { |arg| serialize_argument(arg) }
46 | when Hash
47 | argument.each_with_object({}) do |(key, value), hash|
48 | hash[serialize_hash_key(key)] = serialize_argument(value)
49 | end
50 | else
51 | raise SerializationError.new("Unsupported argument type: #{argument.class.name}")
52 | end
53 | end
54 |
55 | def deserialize_argument(argument)
56 | case argument
57 | when String
58 | GlobalID::Locator.locate(argument) || argument
59 | when *TYPE_WHITELIST
60 | argument
61 | when Array
62 | argument.map { |arg| deserialize_argument(arg) }
63 | when Hash
64 | argument.each_with_object({}.with_indifferent_access) do |(key, value), hash|
65 | hash[key] = deserialize_argument(value)
66 | end
67 | else
68 | raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}"
69 | end
70 | end
71 |
72 | def serialize_hash_key(key)
73 | case key
74 | when String, Symbol
75 | key.to_s
76 | else
77 | raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}")
78 | end
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/global_id/global_id.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require 'active_support/core_ext/string/inflections' # For #model_class constantize
3 | require 'active_support/core_ext/array/access'
4 | require 'active_support/core_ext/object/try' # For #find
5 | require 'uri'
6 |
7 | class GlobalID
8 | class << self
9 | attr_reader :app
10 |
11 | def create(model, options = {})
12 | app = options.fetch :app, GlobalID.app
13 | raise ArgumentError, "An app is required to create a GlobalID. Pass the :app option or set the default GlobalID.app." unless app
14 | new URI("gid://#{app}/#{model.class.name}/#{model.id}"), options
15 | end
16 |
17 | def find(gid, options = {})
18 | parse(gid, options).try(:find, options)
19 | end
20 |
21 | def parse(gid, options = {})
22 | gid.is_a?(self) ? gid : new(gid, options)
23 | rescue URI::Error
24 | parse_encoded_gid(gid, options)
25 | end
26 |
27 | def app=(app)
28 | @app = validate_app(app)
29 | end
30 |
31 | def validate_app(app)
32 | URI.parse('gid:///').hostname = app
33 | rescue URI::InvalidComponentError
34 | raise ArgumentError, 'Invalid app name. ' \
35 | 'App names must be valid URI hostnames: alphanumeric and hyphen characters only.'
36 | end
37 |
38 | private
39 | def parse_encoded_gid(gid, options)
40 | new(Base64.urlsafe_decode64(repad_gid(gid)), options) rescue nil
41 | end
42 |
43 | # We removed the base64 padding character = during #to_param, now we're adding it back so decoding will work
44 | def repad_gid(gid)
45 | padding_chars = gid.length.modulo(4).zero? ? 0 : (4 - gid.length.modulo(4))
46 | gid + ('=' * padding_chars)
47 | end
48 | end
49 |
50 | attr_reader :uri, :app, :model_name, :model_id
51 |
52 | def initialize(gid, options = {})
53 | extract_uri_components gid
54 | end
55 |
56 | def find(options = {})
57 | Locator.locate self, options
58 | end
59 |
60 | def model_class
61 | model_name.constantize
62 | end
63 |
64 | def ==(other)
65 | other.is_a?(GlobalID) && @uri == other.uri
66 | end
67 |
68 | def to_s
69 | @uri.to_s
70 | end
71 |
72 | def to_param
73 | # remove the = padding character for a prettier param -- it'll be added back in parse_encoded_gid
74 | Base64.urlsafe_encode64(to_s).sub(/=+$/, '')
75 | end
76 |
77 | private
78 | PATH_REGEXP = %r(\A/([^/]+)/([^/]+)\z)
79 |
80 | # Pending a URI::GID to handle validation
81 | def extract_uri_components(gid)
82 | @uri = gid.is_a?(URI) ? gid : URI.parse(gid)
83 | raise URI::BadURIError, "Not a gid:// URI scheme: #{@uri.inspect}" unless @uri.scheme == 'gid'
84 |
85 | if @uri.path =~ PATH_REGEXP
86 | @app = @uri.host
87 | @model_name = $1
88 | @model_id = $2
89 | else
90 | raise URI::InvalidURIError, "Expected a URI like gid://app/Person/1234: #{@uri.inspect}"
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/lib/active_job/core.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | module Core
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | # Job arguments
7 | attr_accessor :arguments
8 | attr_writer :serialized_arguments
9 |
10 | # Timestamp when the job should be performed
11 | attr_accessor :scheduled_at
12 |
13 | # Job Identifier
14 | attr_accessor :job_id
15 |
16 | # Queue on which the job should be run on.
17 | attr_writer :queue_name
18 | end
19 |
20 | module ClassMethods
21 | # Creates a new job instance from a hash created with +serialize+
22 | def deserialize(job_data)
23 | job = job_data['job_class'].constantize.new
24 | job.job_id = job_data['job_id']
25 | job.queue_name = job_data['queue_name']
26 | job.serialized_arguments = job_data['arguments']
27 | job
28 | end
29 |
30 | # Creates a job preconfigured with the given options. You can call
31 | # perform_later with the job arguments to enqueue the job with the
32 | # preconfigured options
33 | #
34 | # ==== Options
35 | # * :wait - Enqueues the job with the specified delay
36 | # * :wait_until - Enqueues the job at the time specified
37 | # * :queue - Enqueues the job on the specified queue
38 | #
39 | # ==== Examples
40 | #
41 | # VideoJob.set(queue: :some_queue).perform_later(Video.last)
42 | # VideoJob.set(wait: 5.minutes).perform_later(Video.last)
43 | # VideoJob.set(wait_until: Time.tomorroe).perform_later(Video.last)
44 | # VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last)
45 | # VideoJob.set(queue: :some_queue, wait_until: Time.tomorroe).perform_later(Video.last)
46 | def set(options={})
47 | ConfiguredJob.new(self, options)
48 | end
49 | end
50 |
51 | # Creates a new job instance. Takes as arguments the arguments that
52 | # will be passed to the perform method.
53 | def initialize(*arguments)
54 | @arguments = arguments
55 | @job_id = SecureRandom.uuid
56 | @queue_name = self.class.queue_name
57 | end
58 |
59 | # Returns a hash with the job data that can safely be passed to the
60 | # queueing adapter.
61 | def serialize
62 | {
63 | 'job_class' => self.class.name,
64 | 'job_id' => job_id,
65 | 'queue_name' => queue_name,
66 | 'arguments' => serialize_arguments(arguments)
67 | }
68 | end
69 |
70 | private
71 | def deserialize_arguments_if_needed
72 | if defined?(@serialized_arguments) && @serialized_arguments.present?
73 | @arguments = deserialize_arguments(@serialized_arguments)
74 | @serialized_arguments = nil
75 | end
76 | end
77 |
78 | def serialize_arguments(serialized_args)
79 | Arguments.serialize(serialized_args)
80 | end
81 |
82 | def deserialize_arguments(serialized_args)
83 | Arguments.deserialize(serialized_args)
84 | end
85 | end
86 | end
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/test/support/delayed_job/delayed/backend/test.rb:
--------------------------------------------------------------------------------
1 | #copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb
2 | require 'ostruct'
3 |
4 | # An in-memory backend suitable only for testing. Tries to behave as if it were an ORM.
5 | module Delayed
6 | module Backend
7 | module Test
8 | class Job
9 | attr_accessor :id
10 | attr_accessor :priority
11 | attr_accessor :attempts
12 | attr_accessor :handler
13 | attr_accessor :last_error
14 | attr_accessor :run_at
15 | attr_accessor :locked_at
16 | attr_accessor :locked_by
17 | attr_accessor :failed_at
18 | attr_accessor :queue
19 |
20 | include Delayed::Backend::Base
21 |
22 | cattr_accessor :id
23 | self.id = 0
24 |
25 | def initialize(hash = {})
26 | self.attempts = 0
27 | self.priority = 0
28 | self.id = (self.class.id += 1)
29 | hash.each{|k,v| send(:"#{k}=", v)}
30 | end
31 |
32 | @jobs = []
33 | def self.all
34 | @jobs
35 | end
36 |
37 | def self.count
38 | all.size
39 | end
40 |
41 | def self.delete_all
42 | all.clear
43 | end
44 |
45 | def self.create(attrs = {})
46 | new(attrs).tap do |o|
47 | o.save
48 | end
49 | end
50 |
51 | def self.create!(*args); create(*args); end
52 |
53 | def self.clear_locks!(worker_name)
54 | all.select{|j| j.locked_by == worker_name}.each {|j| j.locked_by = nil; j.locked_at = nil}
55 | end
56 |
57 | # Find a few candidate jobs to run (in case some immediately get locked by others).
58 | def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time)
59 | jobs = all.select do |j|
60 | j.run_at <= db_time_now &&
61 | (j.locked_at.nil? || j.locked_at < db_time_now - max_run_time || j.locked_by == worker_name) &&
62 | !j.failed?
63 | end
64 |
65 | jobs = jobs.select{|j| Worker.queues.include?(j.queue)} if Worker.queues.any?
66 | jobs = jobs.select{|j| j.priority >= Worker.min_priority} if Worker.min_priority
67 | jobs = jobs.select{|j| j.priority <= Worker.max_priority} if Worker.max_priority
68 | jobs.sort_by{|j| [j.priority, j.run_at]}[0..limit-1]
69 | end
70 |
71 | # Lock this job for this worker.
72 | # Returns true if we have the lock, false otherwise.
73 | def lock_exclusively!(max_run_time, worker)
74 | now = self.class.db_time_now
75 | if locked_by != worker
76 | # We don't own this job so we will update the locked_by name and the locked_at
77 | self.locked_at = now
78 | self.locked_by = worker
79 | end
80 |
81 | return true
82 | end
83 |
84 | def self.db_time_now
85 | Time.current
86 | end
87 |
88 | def update_attributes(attrs = {})
89 | attrs.each{|k,v| send(:"#{k}=", v)}
90 | save
91 | end
92 |
93 | def destroy
94 | self.class.all.delete(self)
95 | end
96 |
97 | def save
98 | self.run_at ||= Time.current
99 |
100 | self.class.all << self unless self.class.all.include?(self)
101 | true
102 | end
103 |
104 | def save!; save; end
105 |
106 | def reload
107 | reset
108 | self
109 | end
110 | end
111 | end
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/test/support/integration/helper.rb:
--------------------------------------------------------------------------------
1 | puts "\n\n"
2 | puts "*** Running integration tests for #{ENV['AJADAPTER']} ***"
3 | puts "\n\n"
4 |
5 | ENV["RAILS_ENV"] = "test"
6 | ActiveJob::Base.queue_name_prefix = nil
7 |
8 | require 'rails/generators'
9 | require 'rails/generators/rails/app/app_generator'
10 |
11 | if !defined?(Rails::Generators::ARGVScrubber)
12 | # This class handles preparation of the arguments before the AppGenerator is
13 | # called. The class provides version or help information if they were
14 | # requested, and also constructs the railsrc file (used for extra configuration
15 | # options).
16 | #
17 | # This class should be called before the AppGenerator is required and started
18 | # since it configures and mutates ARGV correctly.
19 | class Rails::Generators::ARGVScrubber
20 | def initialize(argv = ARGV)
21 | @argv = argv
22 | end
23 |
24 | def prepare!
25 | handle_version_request!(@argv.first)
26 | handle_invalid_command!(@argv.first, @argv) do
27 | handle_rails_rc!(@argv.drop(1))
28 | end
29 | end
30 |
31 | def self.default_rc_file
32 | File.expand_path('~/.railsrc')
33 | end
34 |
35 | private
36 |
37 | def handle_version_request!(argument)
38 | if ['--version', '-v'].include?(argument)
39 | require 'rails/version'
40 | puts "Rails #{Rails::VERSION::STRING}"
41 | exit(0)
42 | end
43 | end
44 |
45 | def handle_invalid_command!(argument, argv)
46 | if argument == "new"
47 | yield
48 | else
49 | ['--help'] + argv.drop(1)
50 | end
51 | end
52 |
53 | def handle_rails_rc!(argv)
54 | if argv.find { |arg| arg == '--no-rc' }
55 | argv.reject { |arg| arg == '--no-rc' }
56 | else
57 | railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
58 | end
59 | end
60 |
61 | def railsrc(argv)
62 | if (customrc = argv.index{ |x| x.include?("--rc=") })
63 | fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
64 | yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
65 | else
66 | yield argv, self.class.default_rc_file
67 | end
68 | end
69 |
70 | def read_rc_file(railsrc)
71 | extra_args = File.readlines(railsrc).flat_map(&:split)
72 | puts "Using #{extra_args.join(" ")} from #{railsrc}"
73 | extra_args
74 | end
75 |
76 | def insert_railsrc_into_argv!(argv, railsrc)
77 | return argv unless File.exist?(railsrc)
78 | extra_args = read_rc_file railsrc
79 | argv.take(1) + extra_args + argv.drop(1)
80 | end
81 | end
82 | end
83 |
84 | dummy_app_path = Dir.mktmpdir + "/dummy"
85 | dummy_app_template = File.expand_path("../dummy_app_template.rb", __FILE__)
86 | args = Rails::Generators::ARGVScrubber.new(["new", dummy_app_path, "--skip-gemfile", "--skip-bundle",
87 | "--skip-git", "--skip-spring", "-d", "sqlite3", "--skip-javascript", "--force", "--quite",
88 | "--template", dummy_app_template]).prepare!
89 | Rails::Generators::AppGenerator.start args
90 |
91 | require "#{dummy_app_path}/config/environment.rb"
92 |
93 | ActiveRecord::Migrator.migrations_paths = [ Rails.root.join('db/migrate').to_s ]
94 | require 'rails/test_help'
95 |
96 | Rails.backtrace_cleaner.remove_silencers!
97 |
98 | require_relative 'test_case_helpers'
99 | ActiveSupport::TestCase.send(:include, TestCaseHelpers)
100 |
101 | JobsManager.current_manager.start_workers
102 |
103 | if Minitest.respond_to?(:after_run)
104 | Minitest.after_run do
105 | JobsManager.current_manager.stop_workers
106 | JobsManager.current_manager.clear_jobs
107 | end
108 | else
109 | MiniTest::Unit.after_tests do
110 | JobsManager.current_manager.stop_workers
111 | JobsManager.current_manager.clear_jobs
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/test/cases/logging_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require "active_support/log_subscriber/test_helper"
3 | require 'active_support/core_ext/numeric/time'
4 | require 'jobs/hello_job'
5 | require 'jobs/logging_job'
6 | require 'jobs/nested_job'
7 |
8 | class AdapterTest < ActiveSupport::TestCase
9 | include ActiveSupport::LogSubscriber::TestHelper
10 | include ActiveSupport::Logger::Severity
11 |
12 | class TestLogger < ActiveSupport::Logger
13 | def initialize
14 | @file = StringIO.new
15 | super(@file)
16 | end
17 |
18 | def messages
19 | @file.rewind
20 | @file.read
21 | end
22 | end
23 |
24 | def setup
25 | super
26 | JobBuffer.clear
27 | @old_logger = ActiveJob::Base.logger
28 | @logger = ActiveSupport::TaggedLogging.new(TestLogger.new)
29 | set_logger @logger
30 | ActiveJob::Logging::LogSubscriber.attach_to :active_job
31 | end
32 |
33 | def teardown
34 | super
35 | ActiveJob::Logging::LogSubscriber.log_subscribers.pop
36 | ActiveJob::Base.logger = @old_logger
37 | end
38 |
39 | def set_logger(logger)
40 | ActiveJob::Base.logger = logger
41 | end
42 |
43 |
44 | def test_uses_active_job_as_tag
45 | HelloJob.perform_later "Cristian"
46 | assert_match(/\[ActiveJob\]/, @logger.messages)
47 | end
48 |
49 | def test_uses_job_name_as_tag
50 | LoggingJob.perform_later "Dummy"
51 | assert_match(/\[LoggingJob\]/, @logger.messages)
52 | end
53 |
54 | def test_uses_job_id_as_tag
55 | LoggingJob.perform_later "Dummy"
56 | assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages)
57 | end
58 |
59 | def test_logs_correct_queue_name
60 | original_queue_name = LoggingJob.queue_name
61 | LoggingJob.queue_as :php_jobs
62 | LoggingJob.perform_later("Dummy")
63 | assert_match(/to .*?\(php_jobs\).*/, @logger.messages)
64 | ensure
65 | LoggingJob.queue_name = original_queue_name
66 | end
67 |
68 | def test_enqueue_job_logging
69 | HelloJob.perform_later "Cristian"
70 | assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages)
71 | end
72 |
73 | def test_perform_job_logging
74 | LoggingJob.perform_later "Dummy"
75 | assert_match(/Performing LoggingJob from .*? with arguments:.*Dummy/, @logger.messages)
76 | assert_match(/Dummy, here is it: Dummy/, @logger.messages)
77 | assert_match(/Performed LoggingJob from .*? in .*ms/, @logger.messages)
78 | end
79 |
80 | def test_perform_nested_jobs_logging
81 | NestedJob.perform_later
82 | assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages)
83 | assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages)
84 | assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob from/, @logger.messages)
85 | assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages)
86 | assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob from .* with arguments: "NestedJob"/, @logger.messages)
87 | assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages)
88 | assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob from .* in/, @logger.messages)
89 | assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob from .* in/, @logger.messages)
90 | end
91 |
92 | def test_enqueue_at_job_logging
93 | HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian"
94 | assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
95 | rescue NotImplementedError
96 | skip
97 | end
98 |
99 | def test_enqueue_in_job_logging
100 | HelloJob.set(wait: 2.seconds).perform_later "Cristian"
101 | assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
102 | rescue NotImplementedError
103 | skip
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/lib/active_job/logging.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/string/filters'
2 | require 'active_support/tagged_logging'
3 | require 'active_support/logger'
4 |
5 | module ActiveJob
6 | module Logging
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) }
11 |
12 | if ActiveSupport::VERSION::MINOR > 0
13 | around_enqueue do |_, block, _|
14 | tag_logger do
15 | block.call
16 | end
17 | end
18 |
19 | around_perform do |job, block, _|
20 | tag_logger(job.class.name, job.job_id) do
21 | payload = {adapter: job.class.queue_adapter, job: job}
22 | ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
23 | ActiveSupport::Notifications.instrument("perform.active_job", payload) do
24 | block.call
25 | end
26 | end
27 | end
28 | else
29 | around_enqueue do |_, block|
30 | tag_logger do
31 | block.call
32 | end
33 | end
34 |
35 | around_perform do |job, block|
36 | tag_logger(job.class.name, job.job_id) do
37 | payload = {adapter: job.class.queue_adapter, job: job}
38 | ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
39 | ActiveSupport::Notifications.instrument("perform.active_job", payload) do
40 | block.call
41 | end
42 | end
43 | end
44 | end
45 |
46 | before_enqueue do |job|
47 | if job.scheduled_at
48 | ActiveSupport::Notifications.instrument "enqueue_at.active_job",
49 | adapter: job.class.queue_adapter, job: job
50 | else
51 | ActiveSupport::Notifications.instrument "enqueue.active_job",
52 | adapter: job.class.queue_adapter, job: job
53 | end
54 | end
55 | end
56 |
57 | private
58 | def tag_logger(*tags)
59 | if logger.respond_to?(:tagged)
60 | tags.unshift "ActiveJob" unless logger_tagged_by_active_job?
61 | ActiveJob::Base.logger.tagged(*tags){ yield }
62 | else
63 | yield
64 | end
65 | end
66 |
67 | def logger_tagged_by_active_job?
68 | logger.formatter.current_tags.include?("ActiveJob")
69 | end
70 |
71 | class LogSubscriber < ActiveSupport::LogSubscriber
72 | def enqueue(event)
73 | info do
74 | job = event.payload[:job]
75 | "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)}" + args_info(job)
76 | end
77 | end
78 |
79 | def enqueue_at(event)
80 | info do
81 | job = event.payload[:job]
82 | "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)} at #{scheduled_at(event)}" + args_info(job)
83 | end
84 | end
85 |
86 | def perform_start(event)
87 | info do
88 | job = event.payload[:job]
89 | "Performing #{job.class.name} from #{queue_name(event)}" + args_info(job)
90 | end
91 | end
92 |
93 | def perform(event)
94 | info do
95 | job = event.payload[:job]
96 | "Performed #{job.class.name} from #{queue_name(event)} in #{event.duration.round(2).to_s}ms"
97 | end
98 | end
99 |
100 | private
101 | def queue_name(event)
102 | event.payload[:adapter].name.demodulize.gsub('Adapter', '') + "(#{event.payload[:job].queue_name})"
103 | end
104 |
105 | def args_info(job)
106 | job.arguments.any? ? " with arguments: #{job.arguments.map(&:inspect).join(", ")}" : ""
107 | end
108 |
109 | def scheduled_at(event)
110 | Time.at(event.payload[:job].scheduled_at).utc
111 | end
112 |
113 | def logger
114 | ActiveJob::Base.logger
115 | end
116 | end
117 | end
118 | end
119 |
120 | ActiveJob::Logging::LogSubscriber.attach_to :active_job
121 |
--------------------------------------------------------------------------------
/lib/active_job/callbacks.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/callbacks'
2 |
3 | module ActiveJob
4 | # = Active Job Callbacks
5 | #
6 | # Active Job provides hooks during the lifecycle of a job. Callbacks allow you
7 | # to trigger logic during the lifecycle of a job. Available callbacks are:
8 | #
9 | # * before_enqueue
10 | # * around_enqueue
11 | # * after_enqueue
12 | # * before_perform
13 | # * around_perform
14 | # * after_perform
15 | #
16 | module Callbacks
17 | extend ActiveSupport::Concern
18 | include ActiveSupport::Callbacks
19 |
20 | included do
21 | define_callbacks :perform
22 | define_callbacks :enqueue
23 | end
24 |
25 | module ClassMethods
26 | # Defines a callback that will get called right before the
27 | # job's perform method is executed.
28 | #
29 | # class VideoProcessJob < ActiveJob::Base
30 | # queue_as :default
31 | #
32 | # before_perform do |job|
33 | # UserMailer.notify_video_started_processing(job.arguments.first)
34 | # end
35 | #
36 | # def perform(video_id)
37 | # Video.find(video_id).process
38 | # end
39 | # end
40 | #
41 | def before_perform(*filters, &blk)
42 | set_callback(:perform, :before, *filters, &blk)
43 | end
44 |
45 | # Defines a callback that will get called right after the
46 | # job's perform method has finished.
47 | #
48 | # class VideoProcessJob < ActiveJob::Base
49 | # queue_as :default
50 | #
51 | # after_perform do |job|
52 | # UserMailer.notify_video_processed(job.arguments.first)
53 | # end
54 | #
55 | # def perform(video_id)
56 | # Video.find(video_id).process
57 | # end
58 | # end
59 | #
60 | def after_perform(*filters, &blk)
61 | set_callback(:perform, :after, *filters, &blk)
62 | end
63 |
64 | # Defines a callback that will get called around the job's perform method.
65 | #
66 | # class VideoProcessJob < ActiveJob::Base
67 | # queue_as :default
68 | #
69 | # around_perform do |job, block|
70 | # UserMailer.notify_video_started_processing(job.arguments.first)
71 | # block.call
72 | # UserMailer.notify_video_processed(job.arguments.first)
73 | # end
74 | #
75 | # def perform(video_id)
76 | # Video.find(video_id).process
77 | # end
78 | # end
79 | #
80 | def around_perform(*filters, &blk)
81 | set_callback(:perform, :around, *filters, &blk)
82 | end
83 |
84 | # Defines a callback that will get called right before the
85 | # job is enqueued.
86 | #
87 | # class VideoProcessJob < ActiveJob::Base
88 | # queue_as :default
89 | #
90 | # before_enqueue do |job|
91 | # $statsd.increment "enqueue-video-job.try"
92 | # end
93 | #
94 | # def perform(video_id)
95 | # Video.find(video_id).process
96 | # end
97 | # end
98 | #
99 | def before_enqueue(*filters, &blk)
100 | set_callback(:enqueue, :before, *filters, &blk)
101 | end
102 |
103 | # Defines a callback that will get called right after the
104 | # job is enqueued.
105 | #
106 | # class VideoProcessJob < ActiveJob::Base
107 | # queue_as :default
108 | #
109 | # after_enqueue do |job|
110 | # $statsd.increment "enqueue-video-job.success"
111 | # end
112 | #
113 | # def perform(video_id)
114 | # Video.find(video_id).process
115 | # end
116 | # end
117 | #
118 | def after_enqueue(*filters, &blk)
119 | set_callback(:enqueue, :after, *filters, &blk)
120 | end
121 |
122 | # Defines a callback that will get called before and after the
123 | # job is enqueued.
124 | #
125 | # class VideoProcessJob < ActiveJob::Base
126 | # queue_as :default
127 | #
128 | # around_enqueue do |job, block|
129 | # $statsd.time "video-job.process" do
130 | # block.call
131 | # end
132 | # end
133 | #
134 | # def perform(video_id)
135 | # Video.find(video_id).process
136 | # end
137 | # end
138 | #
139 | def around_enqueue(*filters, &blk)
140 | set_callback(:enqueue, :around, *filters, &blk)
141 | end
142 | end
143 | end
144 | end
145 |
--------------------------------------------------------------------------------
/test/cases/test_helper_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'active_support/core_ext/time'
3 | require 'active_support/core_ext/date'
4 | require 'jobs/hello_job'
5 | require 'jobs/logging_job'
6 | require 'jobs/nested_job'
7 |
8 | class EnqueuedJobsTest < ActiveJob::TestCase
9 | setup { queue_adapter.perform_enqueued_at_jobs = true }
10 |
11 | def test_assert_enqueued_jobs
12 | assert_nothing_raised do
13 | assert_enqueued_jobs 1 do
14 | HelloJob.perform_later('david')
15 | end
16 | end
17 | end
18 |
19 | def test_repeated_enqueued_jobs_calls
20 | assert_nothing_raised do
21 | assert_enqueued_jobs 1 do
22 | HelloJob.perform_later('abdelkader')
23 | end
24 | end
25 |
26 | assert_nothing_raised do
27 | assert_enqueued_jobs 2 do
28 | HelloJob.perform_later('sean')
29 | HelloJob.perform_later('yves')
30 | end
31 | end
32 | end
33 |
34 | def test_assert_enqueued_jobs_with_no_block
35 | assert_nothing_raised do
36 | HelloJob.perform_later('rafael')
37 | assert_enqueued_jobs 1
38 | end
39 |
40 | assert_nothing_raised do
41 | HelloJob.perform_later('aaron')
42 | HelloJob.perform_later('matthew')
43 | assert_enqueued_jobs 3
44 | end
45 | end
46 |
47 | def test_assert_no_enqueued_jobs
48 | assert_nothing_raised do
49 | assert_no_enqueued_jobs do
50 | # Scheduled jobs are being performed in this context
51 | HelloJob.set(wait_until: Date.tomorrow.noon).perform_later('godfrey')
52 | end
53 | end
54 | end
55 |
56 | def test_assert_enqueued_jobs_too_few_sent
57 | error = assert_raise ActiveSupport::TestCase::Assertion do
58 | assert_enqueued_jobs 2 do
59 | HelloJob.perform_later('xavier')
60 | end
61 | end
62 |
63 | assert_match(/2 .* but 1/, error.message)
64 | end
65 |
66 | def test_assert_enqueued_jobs_too_many_sent
67 | error = assert_raise ActiveSupport::TestCase::Assertion do
68 | assert_enqueued_jobs 1 do
69 | HelloJob.perform_later('cristian')
70 | HelloJob.perform_later('guillermo')
71 | end
72 | end
73 |
74 | assert_match(/1 .* but 2/, error.message)
75 | end
76 |
77 | def test_assert_no_enqueued_jobs_failure
78 | error = assert_raise ActiveSupport::TestCase::Assertion do
79 | assert_no_enqueued_jobs do
80 | HelloJob.perform_later('jeremy')
81 | end
82 | end
83 |
84 | assert_match(/0 .* but 1/, error.message)
85 | end
86 |
87 | def test_assert_enqueued_job
88 | assert_enqueued_with(job: LoggingJob, queue: 'default') do
89 | NestedJob.set(wait_until: Date.tomorrow.noon).perform_later
90 | end
91 | end
92 |
93 | def test_assert_enqueued_job_failure
94 | assert_raise ActiveSupport::TestCase::Assertion do
95 | assert_enqueued_with(job: LoggingJob, queue: 'default') do
96 | NestedJob.perform_later
97 | end
98 | end
99 |
100 | assert_raise ActiveSupport::TestCase::Assertion do
101 | assert_enqueued_with(job: NestedJob, queue: 'low') do
102 | NestedJob.perform_later
103 | end
104 | end
105 | end
106 |
107 | def test_assert_enqueued_job_args
108 | assert_raise ArgumentError do
109 | assert_enqueued_with(class: LoggingJob) do
110 | NestedJob.set(wait_until: Date.tomorrow.noon).perform_later
111 | end
112 | end
113 | end
114 | end
115 |
116 | class PerformedJobsTest < ActiveJob::TestCase
117 | setup { queue_adapter.perform_enqueued_jobs = true }
118 |
119 | def test_assert_performed_jobs
120 | assert_nothing_raised do
121 | assert_performed_jobs 1 do
122 | HelloJob.perform_later('david')
123 | end
124 | end
125 | end
126 |
127 | def test_repeated_performed_jobs_calls
128 | assert_nothing_raised do
129 | assert_performed_jobs 1 do
130 | HelloJob.perform_later('abdelkader')
131 | end
132 | end
133 |
134 | assert_nothing_raised do
135 | assert_performed_jobs 2 do
136 | HelloJob.perform_later('sean')
137 | HelloJob.perform_later('yves')
138 | end
139 | end
140 | end
141 |
142 | def test_assert_performed_jobs_with_no_block
143 | assert_nothing_raised do
144 | HelloJob.perform_later('rafael')
145 | assert_performed_jobs 1
146 | end
147 |
148 | assert_nothing_raised do
149 | HelloJob.perform_later('aaron')
150 | HelloJob.perform_later('matthew')
151 | assert_performed_jobs 3
152 | end
153 | end
154 |
155 | def test_assert_no_performed_jobs
156 | assert_nothing_raised do
157 | assert_no_performed_jobs do
158 | # Scheduled jobs are being enqueued in this context
159 | HelloJob.set(wait_until: Date.tomorrow.noon).perform_later('godfrey')
160 | end
161 | end
162 | end
163 |
164 | def test_assert_performed_jobs_too_few_sent
165 | error = assert_raise ActiveSupport::TestCase::Assertion do
166 | assert_performed_jobs 2 do
167 | HelloJob.perform_later('xavier')
168 | end
169 | end
170 |
171 | assert_match(/2 .* but 1/, error.message)
172 | end
173 |
174 | def test_assert_performed_jobs_too_many_sent
175 | error = assert_raise ActiveSupport::TestCase::Assertion do
176 | assert_performed_jobs 1 do
177 | HelloJob.perform_later('cristian')
178 | HelloJob.perform_later('guillermo')
179 | end
180 | end
181 |
182 | assert_match(/1 .* but 2/, error.message)
183 | end
184 |
185 | def test_assert_no_performed_jobs_failure
186 | error = assert_raise ActiveSupport::TestCase::Assertion do
187 | assert_no_performed_jobs do
188 | HelloJob.perform_later('jeremy')
189 | end
190 | end
191 |
192 | assert_match(/0 .* but 1/, error.message)
193 | end
194 |
195 | def test_assert_performed_job
196 | assert_performed_with(job: NestedJob, queue: 'default') do
197 | NestedJob.perform_later
198 | end
199 | end
200 |
201 | def test_assert_performed_job_failure
202 | assert_raise ActiveSupport::TestCase::Assertion do
203 | assert_performed_with(job: LoggingJob, at: Date.tomorrow.noon, queue: 'default') do
204 | NestedJob.set(wait_until: Date.tomorrow.noon).perform_later
205 | end
206 | end
207 |
208 | assert_raise ActiveSupport::TestCase::Assertion do
209 | assert_performed_with(job: NestedJob, at: Date.tomorrow.noon, queue: 'low') do
210 | NestedJob.set(queue: 'low', wait_until: Date.tomorrow.noon).perform_later
211 | end
212 | end
213 | end
214 | end
215 |
--------------------------------------------------------------------------------
/lib/active_job/test_helper.rb:
--------------------------------------------------------------------------------
1 | module ActiveJob
2 | # Provides helper methods for testing Active Job
3 | module TestHelper
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | def before_setup
8 | @old_queue_adapter = queue_adapter
9 | ActiveJob::Base.queue_adapter = :test
10 | clear_enqueued_jobs
11 | clear_performed_jobs
12 | super
13 | end
14 |
15 | def after_teardown
16 | super
17 | ActiveJob::Base.queue_adapter = @old_queue_adapter
18 | end
19 |
20 | # Asserts that the number of enqueued jobs matches the given number.
21 | #
22 | # def test_jobs
23 | # assert_enqueued_jobs 0
24 | # HelloJob.perform_later('david')
25 | # assert_enqueued_jobs 1
26 | # HelloJob.perform_later('abdelkader')
27 | # assert_enqueued_jobs 2
28 | # end
29 | #
30 | # If a block is passed, that block should cause the specified number of
31 | # jobs to be enqueued.
32 | #
33 | # def test_jobs_again
34 | # assert_enqueued_jobs 1 do
35 | # HelloJob.perform_later('cristian')
36 | # end
37 | #
38 | # assert_enqueued_jobs 2 do
39 | # HelloJob.perform_later('aaron')
40 | # HelloJob.perform_later('rafael')
41 | # end
42 | # end
43 | def assert_enqueued_jobs(number)
44 | if block_given?
45 | original_count = enqueued_jobs.size
46 | yield
47 | new_count = enqueued_jobs.size
48 | assert_equal original_count + number, new_count,
49 | "#{number} jobs expected, but #{new_count - original_count} were enqueued"
50 | else
51 | enqueued_jobs_size = enqueued_jobs.size
52 | assert_equal number, enqueued_jobs_size, "#{number} jobs expected, but #{enqueued_jobs_size} were enqueued"
53 | end
54 | end
55 |
56 | # Assert that no job have been enqueued.
57 | #
58 | # def test_jobs
59 | # assert_no_enqueued_jobs
60 | # HelloJob.perform_later('jeremy')
61 | # assert_enqueued_jobs 1
62 | # end
63 | #
64 | # If a block is passed, that block should not cause any job to be enqueued.
65 | #
66 | # def test_jobs_again
67 | # assert_no_enqueued_jobs do
68 | # # No job should be enqueued from this block
69 | # end
70 | # end
71 | #
72 | # Note: This assertion is simply a shortcut for:
73 | #
74 | # assert_enqueued_jobs 0
75 | def assert_no_enqueued_jobs(&block)
76 | assert_enqueued_jobs 0, &block
77 | end
78 |
79 | # Asserts that the number of performed jobs matches the given number.
80 | #
81 | # def test_jobs
82 | # assert_performed_jobs 0
83 | # HelloJob.perform_later('xavier')
84 | # assert_performed_jobs 1
85 | # HelloJob.perform_later('yves')
86 | # assert_performed_jobs 2
87 | # end
88 | #
89 | # If a block is passed, that block should cause the specified number of
90 | # jobs to be performed.
91 | #
92 | # def test_jobs_again
93 | # assert_performed_jobs 1 do
94 | # HelloJob.perform_later('robin')
95 | # end
96 | #
97 | # assert_performed_jobs 2 do
98 | # HelloJob.perform_later('carlos')
99 | # HelloJob.perform_later('sean')
100 | # end
101 | # end
102 | def assert_performed_jobs(number)
103 | if block_given?
104 | original_count = performed_jobs.size
105 | yield
106 | new_count = performed_jobs.size
107 | assert_equal original_count + number, new_count,
108 | "#{number} jobs expected, but #{new_count - original_count} were performed"
109 | else
110 | performed_jobs_size = performed_jobs.size
111 | assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
112 | end
113 | end
114 |
115 | # Asserts that no jobs have been performed.
116 | #
117 | # def test_jobs
118 | # assert_no_performed_jobs
119 | # HelloJob.perform_later('matthew')
120 | # assert_performed_jobs 1
121 | # end
122 | #
123 | # If a block is passed, that block should not cause any job to be performed.
124 | #
125 | # def test_jobs_again
126 | # assert_no_performed_jobs do
127 | # # No job should be performed from this block
128 | # end
129 | # end
130 | #
131 | # Note: This assertion is simply a shortcut for:
132 | #
133 | # assert_performed_jobs 0
134 | def assert_no_performed_jobs(&block)
135 | assert_performed_jobs 0, &block
136 | end
137 |
138 | # Asserts that the job passed in the block has been enqueued with the given arguments.
139 | #
140 | # def assert_enqueued_job
141 | # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do
142 | # MyJob.perform_later(1,2,3)
143 | # end
144 | # end
145 | def assert_enqueued_with(args = {}, &_block)
146 | original_enqueued_jobs = enqueued_jobs.dup
147 | clear_enqueued_jobs
148 | args.assert_valid_keys(:job, :args, :at, :queue)
149 | yield
150 | matching_job = enqueued_jobs.any? do |job|
151 | args.all? { |key, value| value == job[key] }
152 | end
153 | assert matching_job
154 | ensure
155 | queue_adapter.enqueued_jobs = original_enqueued_jobs + enqueued_jobs
156 | end
157 |
158 | # Asserts that the job passed in the block has been performed with the given arguments.
159 | #
160 | # def test_assert_performed_with
161 | # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do
162 | # MyJob.perform_later(1,2,3)
163 | # end
164 | # end
165 | def assert_performed_with(args = {}, &_block)
166 | original_performed_jobs = performed_jobs.dup
167 | clear_performed_jobs
168 | args.assert_valid_keys(:job, :args, :at, :queue)
169 | yield
170 | matching_job = performed_jobs.any? do |job|
171 | args.all? { |key, value| value == job[key] }
172 | end
173 | assert matching_job, "No performed job found with #{args}"
174 | ensure
175 | queue_adapter.performed_jobs = original_performed_jobs + performed_jobs
176 | end
177 |
178 | def queue_adapter
179 | ActiveJob::Base.queue_adapter
180 | end
181 |
182 | delegate :enqueued_jobs, :enqueued_jobs=,
183 | :performed_jobs, :performed_jobs=,
184 | to: :queue_adapter
185 |
186 | private
187 | def clear_enqueued_jobs
188 | enqueued_jobs.clear
189 | end
190 |
191 | def clear_performed_jobs
192 | performed_jobs.clear
193 | end
194 | end
195 | end
196 | end
197 |
--------------------------------------------------------------------------------