├── 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 | [![Build Status](https://travis-ci.org/ankane/activejob_backport.png?branch=master)](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 | --------------------------------------------------------------------------------