diff options
104 files changed, 2276 insertions, 0 deletions
diff --git a/activejob/.gitignore b/activejob/.gitignore new file mode 100644 index 0000000000..ee45263e91 --- /dev/null +++ b/activejob/.gitignore @@ -0,0 +1,2 @@ +/test/dummy/log/* +/test/dummy/tmp/* diff --git a/activejob/.travis.yml b/activejob/.travis.yml new file mode 100644 index 0000000000..fba12f9d0d --- /dev/null +++ b/activejob/.travis.yml @@ -0,0 +1,39 @@ +before_install: +- travis_retry gem install bundler +- sudo apt-get update -qq +- sudo apt-get install beanstalkd +- echo "START=yes" | sudo tee -a /etc/default/beanstalkd +- sudo /etc/init.d/beanstalkd start + +rvm: +- 1.9.3 +- 2.0.0 +- 2.1 +- ruby-head +- rbx-2 +- jruby +env: +- QC_DATABASE_URL="postgres://postgres@localhost/active_jobs_qc_int_test" QUE_DATABASE_URL="postgres://postgres@localhost/active_jobs_qc_int_test" +matrix: + allow_failures: + - rvm: rbx-2 + - rvm: jruby + - rvm: ruby-head + fast_finish: true +notifications: + email: false + irc: + on_success: change + on_failure: always + channels: + - irc.freenode.org#rails-contrib + campfire: + on_success: change + on_failure: always + rooms: + - secure: AgZwJA+9VdnWAw7QN9Z5s6RpQIzsEB0q7V+p3pCzXY45156WocL8iNQx+KnyOQ8jbRUt4L/XIOiZl5xHf4pHjXytHWHNhetAlVQP/hPeDcCSk/h0g5gqgf6QABdp38tBNaUq866bXHgCOZYPwwP9bypcmuv2SLyfIO+b/PBgqN0= +services: +- redis +- rabbitmq +addons: + postgresql: "9.3" diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md new file mode 100644 index 0000000000..b04883413e --- /dev/null +++ b/activejob/CHANGELOG.md @@ -0,0 +1 @@ +* Started project.
\ No newline at end of file diff --git a/activejob/Gemfile b/activejob/Gemfile new file mode 100644 index 0000000000..12a1798fd5 --- /dev/null +++ b/activejob/Gemfile @@ -0,0 +1,25 @@ +source 'https://rubygems.org' + +gemspec + +gem 'rake' +gem 'resque' +gem 'resque-scheduler' +gem 'sidekiq' +gem 'sucker_punch' +gem 'delayed_job' +gem 'queue_classic' +gem 'sneakers', '0.1.1.pre' +gem 'que' +gem 'backburner' +gem 'qu-rails', github: "bkeepers/qu", branch: "master" +gem 'qu-redis' + +#for integration testing +gem 'arel', github: 'rails/arel' +gem 'rack', github: 'rack/rack' +gem 'i18n', github: 'svenfuchs/i18n' +gem 'rails', github: 'rails/rails' +gem 'sqlite3' +gem 'delayed_job_active_record' +gem 'sequel' diff --git a/activejob/Gemfile.lock b/activejob/Gemfile.lock new file mode 100644 index 0000000000..53ba5c055b --- /dev/null +++ b/activejob/Gemfile.lock @@ -0,0 +1,206 @@ +GIT + remote: git://github.com/bkeepers/qu.git + revision: 50f3788f2b55ddd4dc939767fb35aebefa260322 + branch: master + specs: + qu (0.2.0) + qu-rails (0.2.0) + qu (= 0.2.0) + railties (>= 3.2, < 5) + qu-redis (0.2.0) + qu (= 0.2.0) + redis-namespace + +GIT + remote: git://github.com/rack/rack.git + revision: e98a9f7ef0ddd9589145ea953948c73a8ce3caa9 + specs: + rack (1.6.0.alpha) + +GIT + remote: git://github.com/rails/arel.git + revision: 66cee768bc163537087037a583f60639eae49fc3 + specs: + arel (6.0.0.20140505020427) + +GIT + remote: git://github.com/rails/rails.git + revision: b2e88043b52a8f83820a0f4e8a65aa42fd40c544 + specs: + actionmailer (4.2.0.alpha) + actionpack (= 4.2.0.alpha) + actionview (= 4.2.0.alpha) + mail (~> 2.5, >= 2.5.4) + actionpack (4.2.0.alpha) + actionview (= 4.2.0.alpha) + activesupport (= 4.2.0.alpha) + rack (~> 1.6.0.alpha) + rack-test (~> 0.6.2) + actionview (4.2.0.alpha) + activesupport (= 4.2.0.alpha) + builder (~> 3.1) + erubis (~> 2.7.0) + activemodel (4.2.0.alpha) + activesupport (= 4.2.0.alpha) + builder (~> 3.1) + activerecord (4.2.0.alpha) + activemodel (= 4.2.0.alpha) + activesupport (= 4.2.0.alpha) + arel (~> 6.0.0) + activesupport (4.2.0.alpha) + i18n (>= 0.7.0.dev, < 0.8) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.1) + tzinfo (~> 1.1) + rails (4.2.0.alpha) + actionmailer (= 4.2.0.alpha) + actionpack (= 4.2.0.alpha) + actionview (= 4.2.0.alpha) + activemodel (= 4.2.0.alpha) + activerecord (= 4.2.0.alpha) + activesupport (= 4.2.0.alpha) + bundler (>= 1.3.0, < 2.0) + railties (= 4.2.0.alpha) + sprockets-rails (~> 2.1) + railties (4.2.0.alpha) + actionpack (= 4.2.0.alpha) + activesupport (= 4.2.0.alpha) + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + +GIT + remote: git://github.com/svenfuchs/i18n.git + revision: cb679b8cdbab675703a3f88de4d48a48f7b50e06 + specs: + i18n (0.7.0.dev) + +PATH + remote: . + specs: + activejob (4.2.0.alpha) + activemodel-globalid + activesupport (>= 4.1.0) + +GEM + remote: https://rubygems.org/ + specs: + activemodel-globalid (0.1.1) + activemodel (>= 4.1.0) + activesupport (>= 4.1.0) + amq-protocol (1.9.2) + backburner (0.4.5) + beaneater (~> 0.3.1) + dante (~> 0.1.5) + beaneater (0.3.2) + builder (3.2.2) + bunny (1.1.9) + amq-protocol (>= 1.9.2) + celluloid (0.15.2) + timers (~> 1.1.0) + connection_pool (2.0.0) + dante (0.1.5) + delayed_job (4.0.2) + activesupport (>= 3.0, < 4.2) + delayed_job_active_record (4.0.1) + activerecord (>= 3.0, < 4.2) + delayed_job (>= 3.0, < 4.1) + erubis (2.7.0) + hike (1.2.3) + json (1.8.1) + mail (2.6.1) + mime-types (>= 1.16, < 3) + mime-types (2.3) + minitest (5.4.0) + mono_logger (1.1.0) + multi_json (1.10.1) + pg (0.17.1) + que (0.8.1) + queue_classic (2.2.3) + pg (~> 0.17.0) + rack-protection (1.5.3) + rack + rack-test (0.6.2) + rack (>= 1.0) + rake (10.3.2) + redis (3.1.0) + redis-namespace (1.5.1) + redis (~> 3.0, >= 3.0.4) + resque (1.25.2) + mono_logger (~> 1.0) + multi_json (~> 1.0) + redis-namespace (~> 1.3) + sinatra (>= 0.9.2) + vegas (~> 0.1.2) + resque-scheduler (3.0.0) + mono_logger (~> 1.0) + redis (~> 3.0) + resque (~> 1.25) + rufus-scheduler (~> 2.0) + rufus-scheduler (2.0.24) + tzinfo (>= 0.3.22) + sequel (4.8.0) + serverengine (1.5.9) + sigdump (~> 0.2.2) + sidekiq (3.2.1) + celluloid (>= 0.15.2) + connection_pool (>= 2.0.0) + json + redis (>= 3.0.6) + redis-namespace (>= 1.3.1) + sigdump (0.2.2) + sinatra (1.4.5) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) + sneakers (0.1.1.pre) + bunny (~> 1.1.3) + serverengine + thor + thread + sprockets (2.12.1) + hike (~> 1.2) + multi_json (~> 1.0) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + sprockets-rails (2.1.3) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (~> 2.8) + sqlite3 (1.3.9) + sucker_punch (1.1) + celluloid (~> 0.15.2) + thor (0.19.1) + thread (0.1.4) + thread_safe (0.3.4) + tilt (1.4.1) + timers (1.1.0) + tzinfo (1.2.1) + thread_safe (~> 0.1) + vegas (0.1.11) + rack (>= 1.0.0) + +PLATFORMS + ruby + +DEPENDENCIES + activejob! + arel! + backburner + delayed_job + delayed_job_active_record + i18n! + qu-rails! + qu-redis + que + queue_classic + rack! + rails! + rake + resque + resque-scheduler + sequel + sidekiq + sneakers (= 0.1.1.pre) + sqlite3 + sucker_punch diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE new file mode 100644 index 0000000000..8b1e97b776 --- /dev/null +++ b/activejob/MIT-LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/activejob/README.md b/activejob/README.md new file mode 100644 index 0000000000..5adb41b0b6 --- /dev/null +++ b/activejob/README.md @@ -0,0 +1,120 @@ +# Active Job -- Make work happen later + +Active Job is a framework for declaring jobs and making them run on a variety +of queueing backends. These jobs can be everything from regularly scheduled +clean-ups, billing charges, or mailings. Anything that can be chopped up into +small units of work and run in parallel, really. + +It also serves as the backend for [ActionMailer's #deliver_later functionality](https://github.com/rails/activejob/issues/13) +that makes it easy to turn any mailing into a job for running later. That's +one of the most common jobs in a modern web application: Sending emails outside +of the request-response cycle, so the user doesn't have to wait on it. + +The main point is to ensure that all Rails apps will have a job infrastructure +in place, even if it's in the form of an "immediate runner". We can then have +framework features and other gems build on top of that, without having to worry +about API differences between Delayed Job and Resque. Picking your queuing +backend becomes more of an operational concern, then. And you'll be able to +switch between them without having to rewrite your jobs. + + +## Usage + +Set the queue adapter for Active Job: + +``` ruby +ActiveJob::Base.queue_adapter = :inline # default queue adapter +# Adapters currently supported: :backburner, :delayed_job, :qu, :que, :queue_classic, +# :resque, :sidekiq, :sneakers, :sucker_punch +``` + +Declare a job like so: + +```ruby +class MyJob < ActiveJob::Base + queue_as :my_jobs + + def perform(record) + record.do_work + end +end +``` + +Enqueue a job like so: + +```ruby +MyJob.enqueue record # Enqueue a job to be performed as soon the queueing system is free. +``` + +```ruby +MyJob.enqueue_at Date.tomorrow.noon, record # Enqueue a job to be performed tomorrow at noon. +``` + +```ruby +MyJob.enqueue_in 1.week, record # Enqueue a job to be performed 1 week from now. +``` + +That's it! + + +## GlobalID support + +Active Job supports [GlobalID serialization](https://github.com/rails/activemodel-globalid/) for parameters. This makes it possible +to pass live Active Record objects to your job instead of class/id pairs, which +you then have to manually deserialize. Before, jobs would look like this: + +```ruby +class TrashableCleanupJob + def perform(trashable_class, trashable_id, depth) + trashable = trashable_class.constantize.find(trashable_id) + trashable.cleanup(depth) + end +end +``` + +Now you can simply do: + +```ruby +class TrashableCleanupJob + def perform(trashable, depth) + trashable.cleanup(depth) + end +end +``` + +This works with any class that mixes in ActiveModel::GlobalIdentification, which +by default has been mixed into Active Record classes. + + +## Supported queueing systems + +We currently have adapters for: + +* [Backburner](https://github.com/nesquena/backburner) +* [Delayed Job](https://github.com/collectiveidea/delayed_job) +* [Qu](https://github.com/bkeepers/qu) +* [Que](https://github.com/chanks/que) +* [QueueClassic](https://github.com/ryandotsmith/queue_classic) +* [Resque 1.x](https://github.com/resque/resque) +* [Sidekiq](https://github.com/mperham/sidekiq) +* [Sneakers](https://github.com/jondot/sneakers) +* [Sucker Punch](https://github.com/brandonhilkert/sucker_punch) + + +## Auxiliary gems + +* [activejob-stats](https://github.com/seuros/activejob-stats) + +## Under development as a gem, targeted for Rails inclusion + +Active Job is currently being developed in a separate repository until it's +ready to be merged in with Rails. The current plan is to have Active Job +be part of the Rails 4.2 release, but plans may change depending on when +this framework stabilizes and feels ready. + + +## License + +Active Job is released under the MIT license: + +* http://www.opensource.org/licenses/MIT diff --git a/activejob/Rakefile b/activejob/Rakefile new file mode 100644 index 0000000000..b2910de81d --- /dev/null +++ b/activejob/Rakefile @@ -0,0 +1,68 @@ +require 'bundler/gem_tasks' + +require 'rake/testtask' + +def run_without_aborting(*tasks) + errors = [] + + tasks.each do |task| + begin + Rake::Task[task].invoke + rescue Exception + errors << task + end + end + + abort "Errors running #{errors.join(', ')}" if errors.any? +end + +task default: :test + +ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner) + +desc 'Run all adapter tests' +task :test do + tasks = ADAPTERS.map{|a| "test_#{a}" }+["integration_test"] + run_without_aborting(*tasks) +end + +ADAPTERS.each do |adapter| + Rake::TestTask.new("test_#{adapter}") do |t| + t.libs << 'test' + t.test_files = FileList['test/cases/**/*_test.rb'] + t.verbose = true + end + + namespace adapter do + task test: "test_#{adapter}" + task(:env) { ENV['AJADAPTER'] = adapter } + end + + task "test_#{adapter}" => "#{adapter}:env" +end + + + +desc 'Run all adapter integration tests' +task :integration_test do + tasks = (ADAPTERS-['inline']).map{|a| "integration_test_#{a}" } + run_without_aborting(*tasks) +end + +(ADAPTERS-['inline']).each do |adapter| + Rake::TestTask.new("integration_test_#{adapter}") do |t| + t.libs << 'test' + t.test_files = FileList['test/integration/**/*_test.rb'] + t.verbose = true + end + + namespace "integration_#{adapter}" do + task test: "integration_test_#{adapter}" + task(:env) do + ENV['AJADAPTER'] = adapter + ENV['AJ_INTEGRATION_TESTS'] = "1" + end + end + + task "integration_test_#{adapter}" => "integration_#{adapter}:env" +end diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec new file mode 100644 index 0000000000..23fd25c6c8 --- /dev/null +++ b/activejob/activejob.gemspec @@ -0,0 +1,21 @@ +Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = 'activejob' + s.version = '4.2.0.alpha' + s.summary = 'Job framework with pluggable queues (will be part of Rails).' + s.description = 'Declare job classes that can be run by a variety of queueing backends.' + + s.required_ruby_version = '>= 1.9.3' + + s.license = 'MIT' + + s.author = 'David Heinemeier Hansson' + s.email = 'david@loudthinking.com' + s.homepage = 'http://www.rubyonrails.org' + + s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*'] + s.require_path = 'lib' + + s.add_dependency 'activesupport', '>= 4.1.0' + s.add_dependency 'activemodel-globalid' +end diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb new file mode 100644 index 0000000000..ddfdda4fb4 --- /dev/null +++ b/activejob/lib/active_job.rb @@ -0,0 +1,34 @@ +#-- +# Copyright (c) 2014 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +require 'active_support' +require 'active_support/rails' + +require 'active_job/railtie' if defined?(Rails) +require 'active_job/version' + +module ActiveJob + extend ActiveSupport::Autoload + + autoload :Base +end
\ No newline at end of file diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb new file mode 100644 index 0000000000..ef6a3fce1d --- /dev/null +++ b/activejob/lib/active_job/arguments.rb @@ -0,0 +1,52 @@ +require 'active_model/global_locator' +require 'active_model/global_identification' + +module ActiveJob + class Arguments + TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ] + + def self.serialize(arguments) + arguments.map { |argument| serialize_argument(argument) } + end + + def self.deserialize(arguments) + arguments.map { |argument| deserialize_argument(argument) } + end + + private + def self.serialize_argument(argument) + case argument + when ActiveModel::GlobalIdentification + argument.global_id.to_s + when *TYPE_WHITELIST + argument + when Array + serialize(argument) + when Hash + Hash[ argument.map { |key, value| [ serialize_hash_key(key), serialize_argument(value) ] } ] + else + raise "Unsupported argument type: #{argument.class.name}" + end + end + + def self.deserialize_argument(argument) + case argument + when Array + deserialize(argument) + when Hash + Hash[ argument.map { |key, value| [ key, deserialize_argument(value) ] } ].with_indifferent_access + else + ActiveModel::GlobalLocator.locate(argument) || argument + end + end + + def self.serialize_hash_key(key) + case key + when String, Symbol + key.to_s + else + raise "Unsupported hash key type: #{key.class.name}" + end + end + end +end diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb new file mode 100644 index 0000000000..d5ba253016 --- /dev/null +++ b/activejob/lib/active_job/base.rb @@ -0,0 +1,22 @@ +require 'active_job/queue_adapter' +require 'active_job/queue_name' +require 'active_job/enqueuing' +require 'active_job/execution' +require 'active_job/callbacks' +require 'active_job/identifier' +require 'active_job/logging' + +module ActiveJob + class Base + extend QueueAdapter + + include QueueName + include Enqueuing + include Execution + include Callbacks + include Identifier + include Logging + + ActiveSupport.run_load_hooks(:active_job, self) + end +end diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb new file mode 100644 index 0000000000..c69e4a3b55 --- /dev/null +++ b/activejob/lib/active_job/callbacks.rb @@ -0,0 +1,40 @@ +require 'active_support/callbacks' + +module ActiveJob + module Callbacks + extend ActiveSupport::Concern + include ActiveSupport::Callbacks + + included do + define_callbacks :perform + define_callbacks :enqueue + end + + module ClassMethods + def before_perform(*filters, &blk) + set_callback(:perform, :before, *filters, &blk) + end + + def after_perform(*filters, &blk) + set_callback(:perform, :after, *filters, &blk) + end + + def around_perform(*filters, &blk) + set_callback(:perform, :around, *filters, &blk) + end + + + def before_enqueue(*filters, &blk) + set_callback(:enqueue, :before, *filters, &blk) + end + + def after_enqueue(*filters, &blk) + set_callback(:enqueue, :after, *filters, &blk) + end + + def around_enqueue(*filters, &blk) + set_callback(:enqueue, :around, *filters, &blk) + end + end + end +end
\ No newline at end of file diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb new file mode 100644 index 0000000000..e3ac11ba97 --- /dev/null +++ b/activejob/lib/active_job/enqueuing.rb @@ -0,0 +1,71 @@ +require 'active_job/arguments' + +module ActiveJob + module Enqueuing + extend ActiveSupport::Concern + + module ClassMethods + # Push a job onto the queue. The arguments must be legal JSON types + # (string, int, float, nil, true, false, hash or array) or + # ActiveModel::GlobalIdentication instances. Arbitrary Ruby objects + # are not supported. + # + # Returns an instance of the job class queued with args available in + # Job#arguments. + def enqueue(*args) + new(args).tap do |job| + job.run_callbacks :enqueue do + queue_adapter.enqueue self, job.job_id, *Arguments.serialize(args) + end + end + end + + # Enqueue a job to be performed at +interval+ from now. + # + # enqueue_in(1.week, "mike") + # + # Returns an instance of the job class queued with args available in + # Job#arguments and the timestamp in Job#enqueue_at. + def enqueue_in(interval, *args) + enqueue_at interval.seconds.from_now, *args + end + + # Enqueue a job to be performed at an explicit point in time. + # + # enqueue_at(Date.tomorrow.midnight, "mike") + # + # Returns an instance of the job class queued with args available in + # Job#arguments and the timestamp in Job#enqueue_at. + def enqueue_at(timestamp, *args) + new(args).tap do |job| + job.enqueued_at = timestamp + + job.run_callbacks :enqueue do + queue_adapter.enqueue_at self, timestamp.to_f, job.job_id, *Arguments.serialize(args) + end + end + end + end + + included do + attr_accessor :arguments + attr_accessor :enqueued_at + end + + def initialize(arguments = nil) + @arguments = arguments + end + + def retry_now + self.class.enqueue *arguments + end + + def retry_in(interval) + self.class.enqueue_in interval, *arguments + end + + def retry_at(timestamp) + self.class.enqueue_at timestamp, *arguments + end + end +end diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb new file mode 100644 index 0000000000..78ada3d908 --- /dev/null +++ b/activejob/lib/active_job/execution.rb @@ -0,0 +1,27 @@ +require 'active_support/rescuable' +require 'active_job/arguments' + +module ActiveJob + module Execution + extend ActiveSupport::Concern + + included do + include ActiveSupport::Rescuable + end + + def execute(job_id, *serialized_args) + self.job_id = job_id + self.arguments = Arguments.deserialize(serialized_args) + + run_callbacks :perform do + perform *arguments + end + rescue => exception + rescue_with_handler(exception) || raise(exception) + end + + def perform(*) + raise NotImplementedError + end + end +end diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb new file mode 100644 index 0000000000..c166020b28 --- /dev/null +++ b/activejob/lib/active_job/gem_version.rb @@ -0,0 +1,15 @@ +module ActiveJob + # Returns the version of the currently loaded ActiveJob as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activejob/lib/active_job/identifier.rb b/activejob/lib/active_job/identifier.rb new file mode 100644 index 0000000000..c7f522087d --- /dev/null +++ b/activejob/lib/active_job/identifier.rb @@ -0,0 +1,15 @@ +require 'active_job/arguments' + +module ActiveJob + module Identifier + extend ActiveSupport::Concern + + included do + attr_writer :job_id + end + + def job_id + @job_id ||= SecureRandom.uuid + end + end +end diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb new file mode 100644 index 0000000000..d913aee03d --- /dev/null +++ b/activejob/lib/active_job/logging.rb @@ -0,0 +1,88 @@ +require 'active_support/core_ext/string/filters' + +module ActiveJob + module Logging + extend ActiveSupport::Concern + + included do + cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) } + + around_enqueue do |job, block, _| + tag_logger do + block.call + end + end + + around_perform do |job, block, _| + tag_logger(job.class.name, job.job_id) do + payload = {adapter: job.class.queue_adapter, job: job.class, args: job.arguments} + ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup) + ActiveSupport::Notifications.instrument("perform.active_job", payload) do |payload| + block.call + end + end + end + + before_enqueue do |job| + if job.enqueued_at + ActiveSupport::Notifications.instrument "enqueue_at.active_job", + adapter: job.class.queue_adapter, job: job.class, job_id: job.job_id, args: job.arguments, timestamp: job.enqueued_at + else + ActiveSupport::Notifications.instrument "enqueue.active_job", + adapter: job.class.queue_adapter, job: job.class, job_id: job.job_id, args: job.arguments + end + end + end + + private + def tag_logger(*tags) + if logger.respond_to?(:tagged) + tags.unshift "ActiveJob" unless logger_tagged_by_active_job? + ActiveJob::Base.logger.tagged(*tags){ yield } + else + yield + end + end + + def logger_tagged_by_active_job? + logger.formatter.current_tags.include?("ActiveJob") + end + + class LogSubscriber < ActiveSupport::LogSubscriber + def enqueue(event) + info "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)}" + args_info(event) + end + + def enqueue_at(event) + info "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)} at #{enqueued_at(event)}" + args_info(event) + end + + def perform_start(event) + info "Performing #{event.payload[:job].name} from #{queue_name(event)}" + args_info(event) + end + + def perform(event) + info "Performed #{event.payload[:job].name} from #{queue_name(event)} in #{event.duration.round(2).to_s}ms" + end + + private + def queue_name(event) + event.payload[:adapter].name.demodulize.remove('Adapter') + "(#{event.payload[:job].queue_name})" + end + + def args_info(event) + event.payload[:args].any? ? " with arguments: #{event.payload[:args].map(&:inspect).join(", ")}" : "" + end + + def enqueued_at(event) + Time.at(event.payload[:timestamp]).utc + end + + def logger + ActiveJob::Base.logger + end + end + end +end + +ActiveJob::Logging::LogSubscriber.attach_to :active_job diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb new file mode 100644 index 0000000000..8f2f8b86ea --- /dev/null +++ b/activejob/lib/active_job/queue_adapter.rb @@ -0,0 +1,24 @@ +require 'active_job/queue_adapters/inline_adapter' +require 'active_support/core_ext/string/inflections' + +module ActiveJob + module QueueAdapter + mattr_reader(:queue_adapter) { ActiveJob::QueueAdapters::InlineAdapter } + + def queue_adapter=(name_or_adapter) + @@queue_adapter = \ + case name_or_adapter + when Symbol, String + load_adapter(name_or_adapter) + when Class + name_or_adapter + end + end + + private + def load_adapter(name) + require "active_job/queue_adapters/#{name}_adapter" + "ActiveJob::QueueAdapters::#{name.to_s.camelize}Adapter".constantize + end + end +end
\ No newline at end of file diff --git a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb new file mode 100644 index 0000000000..7a6032e56b --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb @@ -0,0 +1,25 @@ +require 'backburner' + +module ActiveJob + module QueueAdapters + class BackburnerAdapter + class << self + def enqueue(job, *args) + Backburner::Worker.enqueue JobWrapper, [ job.name, *args ], queue: job.queue_name + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper + class << self + def perform(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb new file mode 100644 index 0000000000..bfeaa836d2 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb @@ -0,0 +1,23 @@ +require 'delayed_job' + +module ActiveJob + module QueueAdapters + class DelayedJobAdapter + class << self + def enqueue(job, *args) + JobWrapper.new.delay(queue: job.queue_name).perform(job, *args) + end + + def enqueue_at(job, timestamp, *args) + JobWrapper.new.delay(queue: job.queue_name, run_at: Time.at(timestamp)).perform(job, *args) + end + end + + class JobWrapper + def perform(job, *args) + job.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb new file mode 100644 index 0000000000..50d14a321d --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb @@ -0,0 +1,15 @@ +module ActiveJob + module QueueAdapters + class InlineAdapter + class << self + def enqueue(job, *args) + job.new.execute *args + end + + def enqueue_at(*) + raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at https://github.com/rails/activejob") + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb new file mode 100644 index 0000000000..cdf4ae4ce9 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb @@ -0,0 +1,30 @@ +require 'qu' + +module ActiveJob + module QueueAdapters + class QuAdapter + class << self + def enqueue(job, *args) + Qu::Payload.new(klass: JobWrapper, args: [job.name, *args]).tap do |payload| + payload.instance_variable_set(:@queue, job.queue_name) + end.push + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper < Qu::Job + def initialize(job_name, *args) + @job = job_name.constantize + @args = args + end + + def perform + @job.new.execute *@args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb new file mode 100644 index 0000000000..15a607bcb6 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb @@ -0,0 +1,23 @@ +require 'que' + +module ActiveJob + module QueueAdapters + class QueAdapter + class << self + def enqueue(job, *args) + JobWrapper.enqueue job.name, *args, queue: job.queue_name + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper < Que::Job + def run(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb new file mode 100644 index 0000000000..c61e0e30db --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb @@ -0,0 +1,23 @@ +require 'queue_classic' + +module ActiveJob + module QueueAdapters + class QueueClassicAdapter + class << self + def enqueue(job, *args) + QC::Queue.new(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.name, *args) + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper + def self.perform(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/resque_adapter.rb b/activejob/lib/active_job/queue_adapters/resque_adapter.rb new file mode 100644 index 0000000000..b228825f07 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/resque_adapter.rb @@ -0,0 +1,38 @@ +require 'resque' +require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/array/access' + +begin + require 'resque-scheduler' +rescue LoadError + begin + require 'resque_scheduler' + rescue LoadError + $stderr.puts 'The ActiveJob resque adapter requires resque-scheduler. Please add it to your Gemfile and run bundle install' + raise e + end +end + +module ActiveJob + module QueueAdapters + class ResqueAdapter + class << self + def enqueue(job, *args) + Resque.enqueue_to job.queue_name, JobWrapper, job.name, *args + end + + def enqueue_at(job, timestamp, *args) + Resque.enqueue_at_with_queue job.queue_name, timestamp, JobWrapper, job.name, *args + end + end + + class JobWrapper + class << self + def perform(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb new file mode 100644 index 0000000000..f738a7d91c --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb @@ -0,0 +1,35 @@ +require 'sidekiq' + +module ActiveJob + module QueueAdapters + class SidekiqAdapter + class << self + def enqueue(job, *args) + #Sidekiq::Client does not support symbols as keys + Sidekiq::Client.push \ + 'class' => JobWrapper, + 'queue' => job.queue_name, + 'args' => [ job, *args ], + 'retry' => true + end + + def enqueue_at(job, timestamp, *args) + Sidekiq::Client.push \ + 'class' => JobWrapper, + 'queue' => job.queue_name, + 'args' => [ job, *args ], + 'retry' => true, + 'at' => timestamp + end + end + + class JobWrapper + include Sidekiq::Worker + + def perform(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb new file mode 100644 index 0000000000..051a8c3bd7 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb @@ -0,0 +1,34 @@ +require 'sneakers' +require 'thread' + +module ActiveJob + module QueueAdapters + class SneakersAdapter + @monitor = Monitor.new + + class << self + def enqueue(job, *args) + @monitor.synchronize do + JobWrapper.from_queue job.queue_name + JobWrapper.enqueue ActiveSupport::JSON.encode([ job.name, *args ]) + end + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper + include Sneakers::Worker + from_queue 'active_jobs_default' + + def work(msg) + job_name, *args = ActiveSupport::JSON.decode(msg) + job_name.constantize.new.execute *args + ack! + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb new file mode 100644 index 0000000000..64b9c3ca15 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb @@ -0,0 +1,25 @@ +require 'sucker_punch' + +module ActiveJob + module QueueAdapters + class SuckerPunchAdapter + class << self + def enqueue(job, *args) + JobWrapper.new.async.perform job, *args + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper + include SuckerPunch::Job + + def perform(job, *args) + job.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb new file mode 100644 index 0000000000..859ddad034 --- /dev/null +++ b/activejob/lib/active_job/queue_name.rb @@ -0,0 +1,18 @@ +module ActiveJob + module QueueName + extend ActiveSupport::Concern + + module ClassMethods + mattr_accessor(:queue_base_name) { "active_jobs" } + + def queue_as(part_name) + self.queue_name = "#{queue_base_name}_#{part_name}" + end + end + + included do + class_attribute :queue_name + self.queue_name = queue_base_name + end + end +end
\ No newline at end of file diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb new file mode 100644 index 0000000000..08555d1d77 --- /dev/null +++ b/activejob/lib/active_job/railtie.rb @@ -0,0 +1,10 @@ +require 'active_model/railtie' + +module ActiveJob + # = Active Job Railtie + class Railtie < Rails::Railtie # :nodoc: + initializer 'active_job' do + ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger } + end + end +end
\ No newline at end of file diff --git a/activejob/lib/active_job/version.rb b/activejob/lib/active_job/version.rb new file mode 100644 index 0000000000..7e646fa3c4 --- /dev/null +++ b/activejob/lib/active_job/version.rb @@ -0,0 +1,8 @@ +require_relative 'gem_version' + +module ActiveJob + # Returns the version of the currently loaded ActiveJob as a <tt>Gem::Version</tt> + def self.version + gem_version + end +end diff --git a/activejob/lib/activejob.rb b/activejob/lib/activejob.rb new file mode 100644 index 0000000000..fea38731af --- /dev/null +++ b/activejob/lib/activejob.rb @@ -0,0 +1 @@ +require 'active_job' diff --git a/activejob/lib/rails/generators/active_job/job_generator.rb b/activejob/lib/rails/generators/active_job/job_generator.rb new file mode 100644 index 0000000000..fe4af0d2cc --- /dev/null +++ b/activejob/lib/rails/generators/active_job/job_generator.rb @@ -0,0 +1,22 @@ +require 'rails/generators/named_base' + +module ActiveJob + module Generators # :nodoc: + class JobGenerator < Rails::Generators::NamedBase # :nodoc: + desc 'This generator creates an active job file at app/jobs' + + class_option :queue, type: :string, default: 'default', desc: 'The queue name for the generated job' + + def self.default_generator_root + File.dirname(__FILE__) + end + + check_class_collision suffix: 'Job' + + def create_job_file + template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb") + end + + end + end +end
\ No newline at end of file diff --git a/activejob/lib/rails/generators/active_job/templates/job.rb b/activejob/lib/rails/generators/active_job/templates/job.rb new file mode 100644 index 0000000000..6a21616d30 --- /dev/null +++ b/activejob/lib/rails/generators/active_job/templates/job.rb @@ -0,0 +1,9 @@ +<% module_namespacing do -%> +class <%= class_name %>Job < ActiveJob::Base + queue_as :<%= options[:queue] %> + + def perform + # Do something later + end +end +<% end -%>
\ No newline at end of file diff --git a/activejob/test/adapters/backburner.rb b/activejob/test/adapters/backburner.rb new file mode 100644 index 0000000000..65d05f850b --- /dev/null +++ b/activejob/test/adapters/backburner.rb @@ -0,0 +1,3 @@ +require 'support/backburner/inline' + +ActiveJob::Base.queue_adapter = :backburner
\ No newline at end of file diff --git a/activejob/test/adapters/delayed_job.rb b/activejob/test/adapters/delayed_job.rb new file mode 100644 index 0000000000..afd9c9deb7 --- /dev/null +++ b/activejob/test/adapters/delayed_job.rb @@ -0,0 +1,7 @@ +ActiveJob::Base.queue_adapter = :delayed_job + +$LOAD_PATH << File.dirname(__FILE__) + "/../support/delayed_job" + +Delayed::Worker.delay_jobs = false +Delayed::Worker.backend = :test + diff --git a/activejob/test/adapters/inline.rb b/activejob/test/adapters/inline.rb new file mode 100644 index 0000000000..e0092552c4 --- /dev/null +++ b/activejob/test/adapters/inline.rb @@ -0,0 +1 @@ +ActiveJob::Base.queue_adapter = :inline
\ No newline at end of file diff --git a/activejob/test/adapters/qu.rb b/activejob/test/adapters/qu.rb new file mode 100644 index 0000000000..7728c843b4 --- /dev/null +++ b/activejob/test/adapters/qu.rb @@ -0,0 +1,3 @@ +require 'qu-immediate' + +ActiveJob::Base.queue_adapter = :qu diff --git a/activejob/test/adapters/que.rb b/activejob/test/adapters/que.rb new file mode 100644 index 0000000000..640061bf54 --- /dev/null +++ b/activejob/test/adapters/que.rb @@ -0,0 +1,2 @@ +ActiveJob::Base.queue_adapter = :que +Que.mode = :sync diff --git a/activejob/test/adapters/queue_classic.rb b/activejob/test/adapters/queue_classic.rb new file mode 100644 index 0000000000..ad5ced3cc2 --- /dev/null +++ b/activejob/test/adapters/queue_classic.rb @@ -0,0 +1,2 @@ +require 'support/queue_classic/inline' +ActiveJob::Base.queue_adapter = :queue_classic diff --git a/activejob/test/adapters/resque.rb b/activejob/test/adapters/resque.rb new file mode 100644 index 0000000000..af7080358d --- /dev/null +++ b/activejob/test/adapters/resque.rb @@ -0,0 +1,2 @@ +ActiveJob::Base.queue_adapter = :resque +Resque.inline = true diff --git a/activejob/test/adapters/sidekiq.rb b/activejob/test/adapters/sidekiq.rb new file mode 100644 index 0000000000..cd9d2034de --- /dev/null +++ b/activejob/test/adapters/sidekiq.rb @@ -0,0 +1,2 @@ +require 'sidekiq/testing/inline' +ActiveJob::Base.queue_adapter = :sidekiq diff --git a/activejob/test/adapters/sneakers.rb b/activejob/test/adapters/sneakers.rb new file mode 100644 index 0000000000..204166a700 --- /dev/null +++ b/activejob/test/adapters/sneakers.rb @@ -0,0 +1,2 @@ +require 'support/sneakers/inline' +ActiveJob::Base.queue_adapter = :sneakers diff --git a/activejob/test/adapters/sucker_punch.rb b/activejob/test/adapters/sucker_punch.rb new file mode 100644 index 0000000000..d2d1712946 --- /dev/null +++ b/activejob/test/adapters/sucker_punch.rb @@ -0,0 +1,2 @@ +require 'sucker_punch/testing/inline' +ActiveJob::Base.queue_adapter = :sucker_punch diff --git a/activejob/test/cases/adapter_test.rb b/activejob/test/cases/adapter_test.rb new file mode 100644 index 0000000000..7f6f4c1159 --- /dev/null +++ b/activejob/test/cases/adapter_test.rb @@ -0,0 +1,56 @@ +require 'helper' + +class AdapterTest < ActiveSupport::TestCase + setup { @old_adapter = ActiveJob::Base.queue_adapter } + teardown { ActiveJob::Base.queue_adapter = @old_adapter } + + test 'should load inline adapter' do + ActiveJob::Base.queue_adapter = :inline + assert_equal ActiveJob::QueueAdapters::InlineAdapter, ActiveJob::Base.queue_adapter + end + + test 'should load Delayed Job adapter' do + ActiveJob::Base.queue_adapter = :delayed_job + assert_equal ActiveJob::QueueAdapters::DelayedJobAdapter, ActiveJob::Base.queue_adapter + end + + test 'should load Qu adapter' do + ActiveJob::Base.queue_adapter = :qu + assert_equal ActiveJob::QueueAdapters::QuAdapter, ActiveJob::Base.queue_adapter + end + + test 'should load Que adapter' do + ActiveJob::Base.queue_adapter = :que + assert_equal ActiveJob::QueueAdapters::QueAdapter, ActiveJob::Base.queue_adapter + end + + test 'should load Queue Classic adapter' do + ActiveJob::Base.queue_adapter = :queue_classic + assert_equal ActiveJob::QueueAdapters::QueueClassicAdapter, ActiveJob::Base.queue_adapter + end + + test 'should load Resque adapter' do + ActiveJob::Base.queue_adapter = :resque + assert_equal ActiveJob::QueueAdapters::ResqueAdapter, ActiveJob::Base.queue_adapter + end + + test 'should load Sidekiq adapter' do + ActiveJob::Base.queue_adapter = :sidekiq + assert_equal ActiveJob::QueueAdapters::SidekiqAdapter, ActiveJob::Base.queue_adapter + end + + test 'should load Sucker Punch adapter' do + ActiveJob::Base.queue_adapter = :sucker_punch + assert_equal ActiveJob::QueueAdapters::SuckerPunchAdapter, ActiveJob::Base.queue_adapter + end + + test 'should load Sneakers adapter' do + ActiveJob::Base.queue_adapter = :sneakers + assert_equal ActiveJob::QueueAdapters::SneakersAdapter, ActiveJob::Base.queue_adapter + end + + test 'should load Backburner adapter' do + ActiveJob::Base.queue_adapter = :backburner + assert_equal ActiveJob::QueueAdapters::BackburnerAdapter, ActiveJob::Base.queue_adapter + end +end diff --git a/activejob/test/cases/callbacks_test.rb b/activejob/test/cases/callbacks_test.rb new file mode 100644 index 0000000000..9a0657ee89 --- /dev/null +++ b/activejob/test/cases/callbacks_test.rb @@ -0,0 +1,22 @@ +require 'helper' +require 'jobs/callback_job' + +require 'active_support/core_ext/object/inclusion' + +class CallbacksTest < ActiveSupport::TestCase + test 'perform callbacks' do + performed_callback_job = CallbackJob.new.tap { |j| j.execute("A-JOB-ID") } + assert "CallbackJob ran before_perform".in? performed_callback_job.history + assert "CallbackJob ran after_perform".in? performed_callback_job.history + assert "CallbackJob ran around_perform_start".in? performed_callback_job.history + assert "CallbackJob ran around_perform_stop".in? performed_callback_job.history + end + + test 'enqueue callbacks' do + enqueued_callback_job = CallbackJob.enqueue + assert "CallbackJob ran before_enqueue".in? enqueued_callback_job.history + assert "CallbackJob ran after_enqueue".in? enqueued_callback_job.history + assert "CallbackJob ran around_enqueue_start".in? enqueued_callback_job.history + assert "CallbackJob ran around_enqueue_stop".in? enqueued_callback_job.history + end +end diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb new file mode 100644 index 0000000000..b1e24db22e --- /dev/null +++ b/activejob/test/cases/job_serialization_test.rb @@ -0,0 +1,15 @@ +require 'helper' +require 'jobs/gid_job' +require 'models/person' + +class JobSerializationTest < ActiveSupport::TestCase + setup do + $BUFFER = [] + @person = Person.find(5) + end + + test 'serialize job with gid' do + GidJob.enqueue @person + assert_equal "Person with ID: 5", $BUFFER.pop + end +end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb new file mode 100644 index 0000000000..537702edd4 --- /dev/null +++ b/activejob/test/cases/logging_test.rb @@ -0,0 +1,94 @@ +require 'helper' +require "active_support/log_subscriber/test_helper" +require 'jobs/logging_job' +require 'jobs/nested_job' + +class AdapterTest < ActiveSupport::TestCase + include ActiveSupport::LogSubscriber::TestHelper + include ActiveSupport::Logger::Severity + + class TestLogger < ActiveSupport::Logger + def initialize + @file = StringIO.new + super(@file) + end + + def messages + @file.rewind + @file.read + end + end + + def setup + super + $BUFFER = [] + @old_logger = ActiveJob::Base.logger + @logger = ActiveSupport::TaggedLogging.new(TestLogger.new) + set_logger @logger + ActiveJob::Logging::LogSubscriber.attach_to :active_job + end + + def teardown + super + ActiveJob::Logging::LogSubscriber.log_subscribers.pop + ActiveJob::Base.logger = @old_logger + end + + def set_logger(logger) + ActiveJob::Base.logger = logger + end + + + def test_uses_active_job_as_tag + HelloJob.enqueue "Cristian" + assert_match(/\[ActiveJob\]/, @logger.messages) + end + + def test_enqueue_job_logging + HelloJob.enqueue "Cristian" + assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages) + end + + def test_perform_job_logging + LoggingJob.enqueue "Dummy" + assert_match(/Performing LoggingJob from .*? with arguments:.*Dummy/, @logger.messages) + assert_match(/Dummy, here is it: Dummy/, @logger.messages) + assert_match(/Performed LoggingJob from .*? in .*ms/, @logger.messages) + end + + def test_perform_uses_job_name_job_logging + LoggingJob.enqueue "Dummy" + assert_match(/\[LoggingJob\]/, @logger.messages) + end + + def test_perform_uses_job_id_job_logging + LoggingJob.enqueue "Dummy" + assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages) + end + + def test_perform_nested_jobs_logging + NestedJob.enqueue + assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages) + assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob from/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob from .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob from .* in/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob from .* in/, @logger.messages) + end + + def test_enqueue_at_job_logging + HelloJob.enqueue_at 1, "Cristian" + assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages) + rescue NotImplementedError + skip + end + + def test_enqueue_in_job_logging + HelloJob.enqueue_in 2, "Cristian" + assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages) + rescue NotImplementedError + skip + end +end diff --git a/activejob/test/cases/parameters_test.rb b/activejob/test/cases/parameters_test.rb new file mode 100644 index 0000000000..76e8a8059a --- /dev/null +++ b/activejob/test/cases/parameters_test.rb @@ -0,0 +1,77 @@ +require 'helper' +require 'active_job/arguments' +require 'models/person' +require 'active_support/core_ext/hash/indifferent_access' + +class ParameterSerializationTest < ActiveSupport::TestCase + test 'should make no change to regular values' do + assert_equal [ 1, "something" ], ActiveJob::Arguments.serialize([ 1, "something" ]) + end + + test 'should not allow complex objects' do + assert_equal [ nil ], ActiveJob::Arguments.serialize([ nil ]) + assert_equal [ 1 ], ActiveJob::Arguments.serialize([ 1 ]) + assert_equal [ 1.0 ], ActiveJob::Arguments.serialize([ 1.0 ]) + assert_equal [ 'a' ], ActiveJob::Arguments.serialize([ 'a' ]) + assert_equal [ true ], ActiveJob::Arguments.serialize([ true ]) + assert_equal [ false ], ActiveJob::Arguments.serialize([ false ]) + assert_equal [ { "a" => 1, "b" => 2 } ], ActiveJob::Arguments.serialize([ { a: 1, "b" => 2 } ]) + assert_equal [ [ 1 ] ], ActiveJob::Arguments.serialize([ [ 1 ] ]) + assert_equal [ 1_000_000_000_000_000_000_000 ], ActiveJob::Arguments.serialize([ 1_000_000_000_000_000_000_000 ]) + + err = assert_raises RuntimeError do + ActiveJob::Arguments.serialize([ 1, self ]) + end + assert_equal "Unsupported argument type: #{self.class.name}", err.message + end + + test 'should dive deep into arrays or hashes' do + assert_equal [ { "a" => Person.find(5).gid.to_s }.with_indifferent_access ], ActiveJob::Arguments.serialize([ { a: Person.find(5) } ]) + assert_equal [ [ Person.find(5).gid.to_s ] ], ActiveJob::Arguments.serialize([ [ Person.find(5) ] ]) + end + + test 'should dive deep into arrays or hashes and raise exception on complex objects' do + err = assert_raises RuntimeError do + ActiveJob::Arguments.serialize([ 1, [self] ]) + end + assert_equal "Unsupported argument type: #{self.class.name}", err.message + end + + test 'shoud dive deep into hashes and allow raise exception on not string/symbol keys' do + err = assert_raises RuntimeError do + ActiveJob::Arguments.serialize([ [ { 1 => 2 } ] ]) + end + assert_equal "Unsupported hash key type: Fixnum", err.message + end + + test 'should serialize records with global id' do + assert_equal [ Person.find(5).gid.to_s ], ActiveJob::Arguments.serialize([ Person.find(5) ]) + end + + test 'should serialize values and records together' do + assert_equal [ 3, Person.find(5).gid.to_s ], ActiveJob::Arguments.serialize([ 3, Person.find(5) ]) + end +end + +class ParameterDeserializationTest < ActiveSupport::TestCase + test 'should make no change to regular values' do + assert_equal [ 1, "something" ], ActiveJob::Arguments.deserialize([ 1, "something" ]) + end + + test 'should deserialize records with global id' do + assert_equal [ Person.find(5) ], ActiveJob::Arguments.deserialize([ Person.find(5).gid ]) + end + + test 'should serialize values and records together' do + assert_equal [ 3, Person.find(5) ], ActiveJob::Arguments.deserialize([ 3, Person.find(5).gid ]) + end + + test 'should dive deep when deserialising arrays' do + assert_equal [ [ 3, Person.find(5) ] ], ActiveJob::Arguments.deserialize([ [ 3, Person.find(5).gid ] ]) + end + + test 'should dive deep when deserialising hashes' do + assert_equal [ { "5" => Person.find(5) } ], ActiveJob::Arguments.deserialize([ { "5" => Person.find(5).gid } ]) + end + +end diff --git a/activejob/test/cases/queue_naming_test.rb b/activejob/test/cases/queue_naming_test.rb new file mode 100644 index 0000000000..852643b9f6 --- /dev/null +++ b/activejob/test/cases/queue_naming_test.rb @@ -0,0 +1,21 @@ +require 'helper' +require 'jobs/hello_job' + +class QueueNamingTest < ActiveSupport::TestCase + test 'name derived from base' do + assert_equal "active_jobs", HelloJob.queue_name + end + + test 'name appended in job' do + begin + HelloJob.queue_as :greetings + LoggingJob.queue_as :bookkeeping + + assert_equal "active_jobs", NestedJob.queue_name + assert_equal "active_jobs_greetings", HelloJob.queue_name + assert_equal "active_jobs_bookkeeping", LoggingJob.queue_name + ensure + HelloJob.queue_name = LoggingJob.queue_name = ActiveJob::Base.queue_base_name + end + end +end diff --git a/activejob/test/cases/queuing_test.rb b/activejob/test/cases/queuing_test.rb new file mode 100644 index 0000000000..3dd9ecd8d2 --- /dev/null +++ b/activejob/test/cases/queuing_test.rb @@ -0,0 +1,44 @@ +require 'helper' +require 'jobs/hello_job' +require 'active_support/core_ext/numeric/time' + + +class QueuingTest < ActiveSupport::TestCase + setup do + $BUFFER = [] + end + + test 'run queued job' do + HelloJob.enqueue + assert_equal "David says hello", $BUFFER.pop + end + + test 'run queued job with arguments' do + HelloJob.enqueue "Jamie" + assert_equal "Jamie says hello", $BUFFER.pop + end + + test 'run queued job later' do + begin + result = HelloJob.enqueue_at 1.second.ago, "Jamie" + assert result + rescue NotImplementedError + skip + end + end + + test 'job returned by enqueue has the arguments available' do + job = HelloJob.enqueue "Jamie" + assert_equal [ "Jamie" ], job.arguments + end + + + test 'job returned by enqueue_at has the timestamp available' do + begin + job = HelloJob.enqueue_at Time.utc(2014, 1, 1) + assert_equal Time.utc(2014, 1, 1), job.enqueued_at + rescue NotImplementedError + skip + end + end +end diff --git a/activejob/test/cases/rescue_test.rb b/activejob/test/cases/rescue_test.rb new file mode 100644 index 0000000000..3d4831bc62 --- /dev/null +++ b/activejob/test/cases/rescue_test.rb @@ -0,0 +1,23 @@ +require 'helper' +require 'jobs/rescue_job' + +require 'active_support/core_ext/object/inclusion' + +class RescueTest < ActiveSupport::TestCase + setup do + $BUFFER = [] + end + + test 'rescue perform exception with retry' do + job = RescueJob.new + job.execute(SecureRandom.uuid, "david") + assert_equal [ "rescued from ArgumentError", "performed beautifully" ], $BUFFER + end + + test 'let through unhandled perform exception' do + job = RescueJob.new + assert_raises(RescueJob::OtherError) do + job.execute(SecureRandom.uuid, "other") + end + end +end diff --git a/activejob/test/dummy/Rakefile b/activejob/test/dummy/Rakefile new file mode 100644 index 0000000000..9866295e6a --- /dev/null +++ b/activejob/test/dummy/Rakefile @@ -0,0 +1,3 @@ +require 'sneakers/tasks' +require File.expand_path('../config/application', __FILE__) +Rails.application.load_tasks diff --git a/activejob/test/dummy/app/assets/images/.keep b/activejob/test/dummy/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activejob/test/dummy/app/assets/images/.keep diff --git a/activejob/test/dummy/app/controllers/application_controller.rb b/activejob/test/dummy/app/controllers/application_controller.rb new file mode 100644 index 0000000000..1c07694e9d --- /dev/null +++ b/activejob/test/dummy/app/controllers/application_controller.rb @@ -0,0 +1,3 @@ +class ApplicationController < ActionController::Base + protect_from_forgery with: :exception +end diff --git a/activejob/test/dummy/app/controllers/concerns/.keep b/activejob/test/dummy/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activejob/test/dummy/app/controllers/concerns/.keep diff --git a/activejob/test/dummy/app/helpers/application_helper.rb b/activejob/test/dummy/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/activejob/test/dummy/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/activejob/test/dummy/app/jobs/test_job.rb b/activejob/test/dummy/app/jobs/test_job.rb new file mode 100644 index 0000000000..281771a851 --- /dev/null +++ b/activejob/test/dummy/app/jobs/test_job.rb @@ -0,0 +1,9 @@ +class TestJob < ActiveJob::Base + queue_as :default + + def perform(x) + File.open(Rails.root.join("tmp/#{x}"), "w+") do |f| + f.write x + end + end +end diff --git a/activejob/test/dummy/app/mailers/.keep b/activejob/test/dummy/app/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activejob/test/dummy/app/mailers/.keep diff --git a/activejob/test/dummy/app/models/.keep b/activejob/test/dummy/app/models/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activejob/test/dummy/app/models/.keep diff --git a/activejob/test/dummy/app/models/concerns/.keep b/activejob/test/dummy/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activejob/test/dummy/app/models/concerns/.keep diff --git a/activejob/test/dummy/config.ru b/activejob/test/dummy/config.ru new file mode 100644 index 0000000000..5bc2a619e8 --- /dev/null +++ b/activejob/test/dummy/config.ru @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run Rails.application diff --git a/activejob/test/dummy/config/application.rb b/activejob/test/dummy/config/application.rb new file mode 100644 index 0000000000..8b06039a68 --- /dev/null +++ b/activejob/test/dummy/config/application.rb @@ -0,0 +1,9 @@ +require File.expand_path('../boot', __FILE__) +require 'rails/all' +Bundler.require(*Rails.groups) + +module Dummy + class Application < Rails::Application + end +end + diff --git a/activejob/test/dummy/config/boot.rb b/activejob/test/dummy/config/boot.rb new file mode 100644 index 0000000000..6266cfc509 --- /dev/null +++ b/activejob/test/dummy/config/boot.rb @@ -0,0 +1,5 @@ +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) diff --git a/activejob/test/dummy/config/database.yml b/activejob/test/dummy/config/database.yml new file mode 100644 index 0000000000..f5f3aaf1fa --- /dev/null +++ b/activejob/test/dummy/config/database.yml @@ -0,0 +1,3 @@ +test: + adapter: sqlite3 + database: "db/test.sqlite3" diff --git a/activejob/test/dummy/config/environment.rb b/activejob/test/dummy/config/environment.rb new file mode 100644 index 0000000000..7fd2f91b8f --- /dev/null +++ b/activejob/test/dummy/config/environment.rb @@ -0,0 +1,2 @@ +require File.expand_path('../application', __FILE__) +Rails.application.initialize! diff --git a/activejob/test/dummy/config/environments/test.rb b/activejob/test/dummy/config/environments/test.rb new file mode 100644 index 0000000000..ff0318412f --- /dev/null +++ b/activejob/test/dummy/config/environments/test.rb @@ -0,0 +1,13 @@ +Rails.application.configure do + config.cache_classes = true + config.eager_load = false + config.serve_static_assets = true + config.static_cache_control = 'public, max-age=3600' + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.action_dispatch.show_exceptions = false + config.action_controller.allow_forgery_protection = false + config.action_mailer.delivery_method = :test + config.active_support.deprecation = :stderr + config.log_level = :debug +end diff --git a/activejob/test/dummy/config/initializers/activejob.rb b/activejob/test/dummy/config/initializers/activejob.rb new file mode 100644 index 0000000000..5fcde86c96 --- /dev/null +++ b/activejob/test/dummy/config/initializers/activejob.rb @@ -0,0 +1,65 @@ +case ENV['AJADAPTER'] +when "delayed_job" + ActiveJob::Base.queue_adapter = :delayed_job +when "sidekiq" + ActiveJob::Base.queue_adapter = :sidekiq +when "resque" + ActiveJob::Base.queue_adapter = :resque + Resque.redis = Redis::Namespace.new 'active_jobs_int_test', redis: Redis.connect(url: "tcp://127.0.0.1:6379/12", :thread_safe => true) + Resque.logger = Rails.logger +when 'qu' + ActiveJob::Base.queue_adapter = :qu + ENV['REDISTOGO_URL'] = "tcp://127.0.0.1:6379/12" + backend = Qu::Backend::Redis.new + backend.namespace = "active_jobs_int_test" + Qu.backend = backend + Qu.logger = Rails.logger + Qu.interval = 0.5 +when 'que' + ActiveJob::Base.queue_adapter = :que + QUE_URL = ENV['QUE_DATABASE_URL'] || 'postgres://localhost/active_jobs_que_int_test' + uri = URI.parse(QUE_URL) + user = uri.user||ENV['USER'] + pass = uri.password + db = uri.path[1..-1] + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} + Que.connection = Sequel.connect(QUE_URL) + Que.migrate! + Que.mode = :off + Que.worker_count = 1 +when 'queue_classic' + ENV['QC_DATABASE_URL'] ||= 'postgres://localhost/active_jobs_qc_int_test' + ENV['QC_LISTEN_TIME'] = "0.5" + ActiveJob::Base.queue_adapter = :queue_classic + uri = URI.parse(ENV['QC_DATABASE_URL']) + user = uri.user||ENV['USER'] + pass = uri.password + db = uri.path[1..-1] + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} + QC::Setup.create +when 'sidekiq' + ActiveJob::Base.queue_adapter = :sidekiq +when 'sneakers' + ActiveJob::Base.queue_adapter = :sneakers + Sneakers.configure :heartbeat => 2, + :amqp => 'amqp://guest:guest@localhost:5672', + :vhost => '/', + :exchange => 'active_jobs_sneakers_int_test', + :exchange_type => :direct, + :daemonize => true, + :threads => 1, + :workers => 1, + :pid_path => Rails.root.join("tmp/sneakers.pid").to_s, + :log => Rails.root.join("log/sneakers.log").to_s +when 'sucker_punch' + ActiveJob::Base.queue_adapter = :sucker_punch +when 'backburner' + ActiveJob::Base.queue_adapter = :backburner + Backburner.configure do |config| + config.logger = Rails.logger + end +else + ActiveJob::Base.queue_adapter = nil +end diff --git a/activejob/test/dummy/config/initializers/session_store.rb b/activejob/test/dummy/config/initializers/session_store.rb new file mode 100644 index 0000000000..70a5a506a9 --- /dev/null +++ b/activejob/test/dummy/config/initializers/session_store.rb @@ -0,0 +1 @@ +Rails.application.config.session_store :cookie_store, key: '_dummy_session' diff --git a/activejob/test/dummy/config/routes.rb b/activejob/test/dummy/config/routes.rb new file mode 100644 index 0000000000..1daf9a4121 --- /dev/null +++ b/activejob/test/dummy/config/routes.rb @@ -0,0 +1,2 @@ +Rails.application.routes.draw do +end diff --git a/activejob/test/dummy/config/secrets.yml b/activejob/test/dummy/config/secrets.yml new file mode 100644 index 0000000000..7dfacb38ea --- /dev/null +++ b/activejob/test/dummy/config/secrets.yml @@ -0,0 +1,2 @@ +test: + secret_key_base: b83ee5aeada663bc4270a1817d0ca43b2784017cc77dc8afcd60967cc968d4ce30caff9eb682766129e18a4048c4d5ebf14eabf463fc37ad67c18934f4345545 diff --git a/activejob/test/dummy/db/migrate/20140804200445_create_delayed_jobs.rb b/activejob/test/dummy/db/migrate/20140804200445_create_delayed_jobs.rb new file mode 100644 index 0000000000..ec0dd93ce1 --- /dev/null +++ b/activejob/test/dummy/db/migrate/20140804200445_create_delayed_jobs.rb @@ -0,0 +1,22 @@ +class CreateDelayedJobs < ActiveRecord::Migration + def self.up + create_table :delayed_jobs, :force => true do |table| + table.integer :priority, :default => 0, :null => false # Allows some jobs to jump to the front of the queue + table.integer :attempts, :default => 0, :null => false # Provides for retries, but still fail eventually. + table.text :handler, :null => false # YAML-encoded string of the object that will do work + table.text :last_error # reason for last failure (See Note below) + table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. + table.datetime :locked_at # Set when a client is working on this object + table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) + table.string :locked_by # Who is working on this object (if locked) + table.string :queue # The name of the queue this job is in + table.timestamps + end + + add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority' + end + + def self.down + drop_table :delayed_jobs + end +end diff --git a/activejob/test/dummy/db/schema.rb b/activejob/test/dummy/db/schema.rb new file mode 100644 index 0000000000..012a099f9c --- /dev/null +++ b/activejob/test/dummy/db/schema.rb @@ -0,0 +1,32 @@ +# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20140804200445) do + + create_table "delayed_jobs", force: true do |t| + t.integer "priority", default: 0, null: false + t.integer "attempts", default: 0, null: false + t.text "handler", null: false + t.text "last_error" + t.datetime "run_at" + t.datetime "locked_at" + t.datetime "failed_at" + t.string "locked_by" + t.string "queue" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority" + +end diff --git a/activejob/test/dummy/db/test.sqlite3 b/activejob/test/dummy/db/test.sqlite3 Binary files differnew file mode 100644 index 0000000000..671e20102e --- /dev/null +++ b/activejob/test/dummy/db/test.sqlite3 diff --git a/activejob/test/dummy/lib/assets/.keep b/activejob/test/dummy/lib/assets/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activejob/test/dummy/lib/assets/.keep diff --git a/activejob/test/dummy/log/.keep b/activejob/test/dummy/log/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activejob/test/dummy/log/.keep diff --git a/activejob/test/dummy/tmp/.keep b/activejob/test/dummy/tmp/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activejob/test/dummy/tmp/.keep diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb new file mode 100644 index 0000000000..104dabd861 --- /dev/null +++ b/activejob/test/helper.rb @@ -0,0 +1,19 @@ +require 'bundler' +Bundler.setup + +$LOAD_PATH << File.dirname(__FILE__) + "/../lib" + +require 'active_job' + +adapter = ENV['AJADAPTER'] || 'inline' +puts "Testing#{" integration" if ENV['AJ_INTEGRATION_TESTS']} using #{adapter}" + +if ENV['AJ_INTEGRATION_TESTS'] + require 'support/integration/helper' +else + require "adapters/#{adapter}" +end + +require 'active_support/testing/autorun' + +ActiveJob::Base.logger.level = Logger::DEBUG diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb new file mode 100644 index 0000000000..bf6137af4c --- /dev/null +++ b/activejob/test/integration/queuing_test.rb @@ -0,0 +1,18 @@ +require 'helper' +require 'jobs/logging_job' +require 'active_support/core_ext/numeric/time' + + +class QueuingTest < ActiveSupport::TestCase + setup do + + end + + test 'run queued job' do + id = "AJ-#{SecureRandom.uuid}" + TestJob.enqueue id + sleep 2 + assert Dummy::Application.root.join("tmp/#{id}").exist? + end + +end diff --git a/activejob/test/jobs/callback_job.rb b/activejob/test/jobs/callback_job.rb new file mode 100644 index 0000000000..056dd073e8 --- /dev/null +++ b/activejob/test/jobs/callback_job.rb @@ -0,0 +1,32 @@ +class CallbackJob < ActiveJob::Base + before_perform ->(job) { job.history << "CallbackJob ran before_perform" } + after_perform ->(job) { job.history << "CallbackJob ran after_perform" } + + before_enqueue ->(job) { job.history << "CallbackJob ran before_enqueue" } + after_enqueue ->(job) { job.history << "CallbackJob ran after_enqueue" } + + around_perform :around_perform + around_enqueue :around_enqueue + + + def perform(person = "david") + # NOTHING! + end + + def history + @history ||= [] + end + + # FIXME: Not sure why these can't be declared inline like before/after + def around_perform + history << "CallbackJob ran around_perform_start" + yield + history << "CallbackJob ran around_perform_stop" + end + + def around_enqueue + history << "CallbackJob ran around_enqueue_start" + yield + history << "CallbackJob ran around_enqueue_stop" + end +end diff --git a/activejob/test/jobs/gid_job.rb b/activejob/test/jobs/gid_job.rb new file mode 100644 index 0000000000..c69e38d3cc --- /dev/null +++ b/activejob/test/jobs/gid_job.rb @@ -0,0 +1,6 @@ +class GidJob < ActiveJob::Base + def perform(person) + $BUFFER << "Person with ID: #{person.id}" + end +end +
\ No newline at end of file diff --git a/activejob/test/jobs/hello_job.rb b/activejob/test/jobs/hello_job.rb new file mode 100644 index 0000000000..25441dd0c8 --- /dev/null +++ b/activejob/test/jobs/hello_job.rb @@ -0,0 +1,5 @@ +class HelloJob < ActiveJob::Base + def perform(greeter = "David") + $BUFFER << "#{greeter} says hello" + end +end diff --git a/activejob/test/jobs/logging_job.rb b/activejob/test/jobs/logging_job.rb new file mode 100644 index 0000000000..d84ed8589b --- /dev/null +++ b/activejob/test/jobs/logging_job.rb @@ -0,0 +1,10 @@ +class LoggingJob < ActiveJob::Base + def perform(dummy) + logger.info "Dummy, here is it: #{dummy}" + end + + def job_id + "LOGGING-JOB-ID" + end +end + diff --git a/activejob/test/jobs/nested_job.rb b/activejob/test/jobs/nested_job.rb new file mode 100644 index 0000000000..fd66f68991 --- /dev/null +++ b/activejob/test/jobs/nested_job.rb @@ -0,0 +1,10 @@ +class NestedJob < ActiveJob::Base + def perform + LoggingJob.enqueue "NestedJob" + end + + def job_id + "NESTED-JOB-ID" + end +end + diff --git a/activejob/test/jobs/rescue_job.rb b/activejob/test/jobs/rescue_job.rb new file mode 100644 index 0000000000..acf2b81515 --- /dev/null +++ b/activejob/test/jobs/rescue_job.rb @@ -0,0 +1,20 @@ +class RescueJob < ActiveJob::Base + class OtherError < StandardError; end + + rescue_from(ArgumentError) do + $BUFFER << "rescued from ArgumentError" + arguments[0] = "DIFFERENT!" + retry_now + end + + def perform(person = "david") + case person + when "david" + raise ArgumentError, "Hair too good" + when "other" + raise OtherError + else + $BUFFER << "performed beautifully" + end + end +end diff --git a/activejob/test/models/person.rb b/activejob/test/models/person.rb new file mode 100644 index 0000000000..a5bdbc462b --- /dev/null +++ b/activejob/test/models/person.rb @@ -0,0 +1,19 @@ +require 'active_model/global_identification' + +class Person + include ActiveModel::GlobalIdentification + + attr_reader :id + + def self.find(id) + new(id) + end + + def initialize(id) + @id = id + end + + def ==(other_person) + other_person.is_a?(Person) && id.to_s == other_person.id.to_s + end +end diff --git a/activejob/test/support/backburner/inline.rb b/activejob/test/support/backburner/inline.rb new file mode 100644 index 0000000000..f761b53e27 --- /dev/null +++ b/activejob/test/support/backburner/inline.rb @@ -0,0 +1,8 @@ +require 'backburner' + +Backburner::Worker.class_eval do + class << self; alias_method :original_enqueue, :enqueue; end + def self.enqueue(job_class, args=[], opts={}) + job_class.perform(*args) + end +end
\ No newline at end of file diff --git a/activejob/test/support/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb new file mode 100644 index 0000000000..b50ed36fc2 --- /dev/null +++ b/activejob/test/support/delayed_job/delayed/backend/test.rb @@ -0,0 +1,113 @@ +#copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb +require 'ostruct' + +# An in-memory backend suitable only for testing. Tries to behave as if it were an ORM. +module Delayed + module Backend + module Test + class Job + attr_accessor :id + attr_accessor :priority + attr_accessor :attempts + attr_accessor :handler + attr_accessor :last_error + attr_accessor :run_at + attr_accessor :locked_at + attr_accessor :locked_by + attr_accessor :failed_at + attr_accessor :queue + + include Delayed::Backend::Base + + cattr_accessor :id + self.id = 0 + + def initialize(hash = {}) + self.attempts = 0 + self.priority = 0 + self.id = (self.class.id += 1) + hash.each{|k,v| send(:"#{k}=", v)} + end + + @jobs = [] + def self.all + @jobs + end + + def self.count + all.size + end + + def self.delete_all + all.clear + end + + def self.create(attrs = {}) + new(attrs).tap do |o| + o.save + end + end + + def self.create!(*args); create(*args); end + + def self.clear_locks!(worker_name) + all.select{|j| j.locked_by == worker_name}.each {|j| j.locked_by = nil; j.locked_at = nil} + end + + # Find a few candidate jobs to run (in case some immediately get locked by others). + def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time) + jobs = all.select do |j| + j.run_at <= db_time_now && + (j.locked_at.nil? || j.locked_at < db_time_now - max_run_time || j.locked_by == worker_name) && + !j.failed? + end + + jobs = jobs.select{|j| Worker.queues.include?(j.queue)} if Worker.queues.any? + jobs = jobs.select{|j| j.priority >= Worker.min_priority} if Worker.min_priority + jobs = jobs.select{|j| j.priority <= Worker.max_priority} if Worker.max_priority + jobs.sort_by{|j| [j.priority, j.run_at]}[0..limit-1] + end + + # Lock this job for this worker. + # Returns true if we have the lock, false otherwise. + def lock_exclusively!(max_run_time, worker) + now = self.class.db_time_now + if locked_by != worker + # We don't own this job so we will update the locked_by name and the locked_at + self.locked_at = now + self.locked_by = worker + end + + return true + end + + def self.db_time_now + Time.current + end + + def update_attributes(attrs = {}) + attrs.each{|k,v| send(:"#{k}=", v)} + save + end + + def destroy + self.class.all.delete(self) + end + + def save + self.run_at ||= Time.current + + self.class.all << self unless self.class.all.include?(self) + true + end + + def save!; save; end + + def reload + reset + self + end + end + end + end +end diff --git a/activejob/test/support/delayed_job/delayed/serialization/test.rb b/activejob/test/support/delayed_job/delayed/serialization/test.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/activejob/test/support/delayed_job/delayed/serialization/test.rb diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb new file mode 100644 index 0000000000..7271d50a45 --- /dev/null +++ b/activejob/test/support/integration/adapters/backburner.rb @@ -0,0 +1,15 @@ +module BackburnerJobsManager + def clear_jobs + Backburner::Worker.connection.tubes.all.map &:clear + end + + def start_workers + @thread = Thread.new { Backburner.work "active-jobs-default" } + end + + def stop_workers + @thread.kill + end + +end + diff --git a/activejob/test/support/integration/adapters/delayed_job.rb b/activejob/test/support/integration/adapters/delayed_job.rb new file mode 100644 index 0000000000..3e55933438 --- /dev/null +++ b/activejob/test/support/integration/adapters/delayed_job.rb @@ -0,0 +1,14 @@ +module DelayedJobJobsManager + def clear_jobs + Delayed::Job.delete_all + end + + def start_workers + @worker = Delayed::Worker.new(quiet: false, sleep_delay: 0.5) + @thread = Thread.new { @worker.start } + end + + def stop_workers + @worker.stop + end +end diff --git a/activejob/test/support/integration/adapters/qu.rb b/activejob/test/support/integration/adapters/qu.rb new file mode 100644 index 0000000000..12d063ea0d --- /dev/null +++ b/activejob/test/support/integration/adapters/qu.rb @@ -0,0 +1,14 @@ +module QuJobsManager + def clear_jobs + Qu.clear "active_jobs_default" + end + + def start_workers + @thread = Thread.new { Qu::Worker.new("active_jobs_default").start } + end + + def stop_workers + @thread.kill + end +end + diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb new file mode 100644 index 0000000000..f15c9af910 --- /dev/null +++ b/activejob/test/support/integration/adapters/que.rb @@ -0,0 +1,19 @@ +module QueJobsManager + def clear_jobs + Que.clear! + end + + def start_workers + @thread = Thread.new do + loop do + Que::Job.work("active_jobs_default") + sleep 0.5 + end + end + end + + def stop_workers + @thread.kill + end +end + diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb new file mode 100644 index 0000000000..3b24eca5b9 --- /dev/null +++ b/activejob/test/support/integration/adapters/queue_classic.rb @@ -0,0 +1,21 @@ +module QC; WAIT_TIME = 0.5; end + +module QueueClassicJobsManager + def clear_jobs + # disabling this as it locks + # QC::Queue.new("active_jobs_default").delete_all + end + + def start_workers + @pid = fork do + QC::Conn.connection = QC::Conn.connect + worker = QC::Worker.new(q_name: 'active_jobs_default') + worker.start + end + end + + def stop_workers + Process.kill 'HUP', @pid + end +end + diff --git a/activejob/test/support/integration/adapters/resque.rb b/activejob/test/support/integration/adapters/resque.rb new file mode 100644 index 0000000000..b9811bd3e1 --- /dev/null +++ b/activejob/test/support/integration/adapters/resque.rb @@ -0,0 +1,18 @@ +module ResqueJobsManager + def clear_jobs + Resque.queues.each { |queue_name| Resque.redis.del "queue:#{queue_name}" } + Resque.redis.keys("delayed:*").each { |key| Resque.redis.del "#{key}" } + Resque.redis.del "delayed_queue_schedule" + end + + def start_workers + @thread = Thread.new do + Resque::Worker.new("*").work(0.5) + end + end + + def stop_workers + @thread.kill + end +end + diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb new file mode 100644 index 0000000000..d3b7d15614 --- /dev/null +++ b/activejob/test/support/integration/adapters/sidekiq.rb @@ -0,0 +1,19 @@ +require 'sidekiq/launcher' +require 'sidekiq/api' + +module SidekiqJobsManager + def clear_jobs + Sidekiq::Queue.new("active_jobs_default").clear + end + + def start_workers + options = {:queues=>["active_jobs_default"], :concurrency=>1, :environment=>"test", :timeout=>8, :daemon=>true, :strict=>true} + @launcher = Sidekiq::Launcher.new(options) + @launcher.run + end + + def stop_workers + @launcher.stop + end +end + diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb new file mode 100644 index 0000000000..5dcab68515 --- /dev/null +++ b/activejob/test/support/integration/adapters/sneakers.rb @@ -0,0 +1,18 @@ +require 'sneakers/runner' + +module SneakersJobsManager + def clear_jobs + end + + def start_workers + cmd = %{cd #{Rails.root.to_s} && (RAILS_ENV=test AJADAPTER=sneakers WORKERS=ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper bundle exec rake --trace sneakers:run)} + `#{cmd}` + while !Rails.root.join("tmp/sneakers.pid").exist? do + sleep 0.5 + end + end + + def stop_workers + Process.kill 'TERM', File.open(Rails.root.join("tmp/sneakers.pid").to_s).read.to_i + end +end diff --git a/activejob/test/support/integration/adapters/sucker_punch.rb b/activejob/test/support/integration/adapters/sucker_punch.rb new file mode 100644 index 0000000000..317f9c80fd --- /dev/null +++ b/activejob/test/support/integration/adapters/sucker_punch.rb @@ -0,0 +1,5 @@ +module SuckerPunchJobsManager + def clear_jobs + end +end + diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb new file mode 100644 index 0000000000..cb94e7cfb5 --- /dev/null +++ b/activejob/test/support/integration/helper.rb @@ -0,0 +1,12 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path("../../../dummy/config/environment.rb", __FILE__) +require "rails/test_help" +Rails.backtrace_cleaner.remove_silencers! + +require_relative 'test_case_helpers' +ActiveSupport::TestCase.send(:include, TestCaseHelpers) + +JobsManager.current_manager.setup +JobsManager.current_manager.start_workers +Minitest.after_run { JobsManager.current_manager.stop_workers } + diff --git a/activejob/test/support/integration/jobs_manager.rb b/activejob/test/support/integration/jobs_manager.rb new file mode 100644 index 0000000000..1da74193b1 --- /dev/null +++ b/activejob/test/support/integration/jobs_manager.rb @@ -0,0 +1,23 @@ +class JobsManager + @@managers = {} + attr :adapter_name + + def self.current_manager + @@managers[ENV['AJADAPTER']] ||= new(ENV['AJADAPTER']) + end + + def initialize(adapter_name) + @adapter_name = adapter_name + require_relative "adapters/#{adapter_name}" + extend "#{adapter_name.camelize}JobsManager".constantize + end + + def setup + end + + def start_workers + end + + def stop_workers + end +end diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb new file mode 100644 index 0000000000..9a5eea0783 --- /dev/null +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -0,0 +1,30 @@ +require 'active_support/concern' +require 'support/integration/jobs_manager' + +module TestCaseHelpers + extend ActiveSupport::Concern + + included do + self.use_transactional_fixtures = false + + setup do + clear_jobs + end + + teardown do + clear_jobs + FileUtils.rm_rf Dir[Dummy::Application.root.join("tmp/AJ-*")] + end + end + + protected + + def jobs_manager + JobsManager.current_manager + end + + def clear_jobs + jobs_manager.clear_jobs + end + +end diff --git a/activejob/test/support/queue_classic/inline.rb b/activejob/test/support/queue_classic/inline.rb new file mode 100644 index 0000000000..5e9c295e01 --- /dev/null +++ b/activejob/test/support/queue_classic/inline.rb @@ -0,0 +1,11 @@ +require 'queue_classic' + +module QC + class Queue + def enqueue(method, *args) + receiver_str, _, message = method.rpartition('.') + receiver = eval(receiver_str) + receiver.send(message, *args) + end + end +end diff --git a/activejob/test/support/sneakers/inline.rb b/activejob/test/support/sneakers/inline.rb new file mode 100644 index 0000000000..16d9b830fa --- /dev/null +++ b/activejob/test/support/sneakers/inline.rb @@ -0,0 +1,12 @@ +require 'sneakers' + +module Sneakers + module Worker + module ClassMethods + def enqueue(msg) + worker = self.new(nil, nil, {}) + worker.work(*msg) + end + end + end +end |