delayed_job_groups_plugin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2a4de75ba53fbd7b7777b5c65ccdbb29a509cf51
4
+ data.tar.gz: 611191660f2dcf4b942efe7916006e1f710a8e29
5
+ SHA512:
6
+ metadata.gz: 0894bd53ffa33e924e1319034f462f7cd2e4b6e2b0ca39bc9cb4209dbdcf89cd5f36cfcca63012639685a05e86aa4402b97608faa672debc4607b6c96b55f106
7
+ data.tar.gz: b301f22a1c2334058f8d6f3b4c463e5be9cc7aa252d60a713ef79d328a2ea0692e610fd123e1fb367bf8cd4d215c0f0d5e34350fb5df26605fe262ccef6b3ad9
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.iml
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .idea/
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ log/
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ env:
3
+ matrix:
4
+ - RAILS_VERSION="~> 3.2.0" JRUBY_OPTS="$JRUBY_OPTS --debug"
5
+ - RAILS_VERSION="~> 4.0.0" JRUBY_OPTS="$JRUBY_OPTS --debug"
6
+ rvm:
7
+ - 1.9.3
8
+ - 2.0.0
9
+ - 2.1.0
10
+ - jruby-19mode
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Joel Turkel
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # Delayed Job Groups
2
+ [![Build Status](https://secure.travis-ci.org/salsify/delayed_job_groups_plugin.png?branch=master)][travis]
3
+ [![Code Climate](https://codeclimate.com/github/salsify/delayed_job_groups_plugin.png)][codeclimate]
4
+ [![Coverage Status](https://coveralls.io/repos/salsify/delayed_job_groups_plugin/badge.png)][coveralls]
5
+
6
+ [travis]: http://travis-ci.org/salsify/delayed_job_groups_plugin
7
+ [codeclimate]: https://codeclimate.com/github/salsify/delayed_job_groups_plugin
8
+ [coveralls]: https://coveralls.io/r/salsify/delayed_job_groups_plugin
9
+
10
+ A [Delayed Job](https://github.com/collectiveidea/delayed_job) plugin that adds job groups supporting:
11
+
12
+ * Canceling all jobs in a job group
13
+ * Canceling the job group when a job in the job group fails
14
+ * Blocking and unblocking all jobs in a job group
15
+ * Running additional processing after all jobs in a job group complete or a job group fails
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ gem 'delayed_job_groups_plugin'
22
+
23
+ And then execute:
24
+
25
+ $ bundle install
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install delayed_job_groups_plugin
30
+
31
+ Run the required database migrations:
32
+
33
+ $ rails generate delayed_job_groups_plugin:install
34
+ $ rake db:migrate
35
+
36
+ ## Usage
37
+
38
+ Creating a job group and queueing some jobs:
39
+
40
+ ```ruby
41
+ job_group = Delayed::JobGroups::JobGroup.create!
42
+
43
+ # JobGroup#enqueue has the same signature as Delayed::Job.enqueue
44
+ # i.e. it takes a job and an optional hash of options.
45
+ job_group.enqueue(MyJob.new('some arg'), queue: 'general')
46
+ job_group.enqueue(MyJob.new('some other arg'), queue: 'general', priority: 10)
47
+
48
+ # Tell the JobGroup we're done queueing jobs
49
+ job_group.mark_queueing_complete
50
+ ```
51
+
52
+ Registering a job to run after all jobs in the job group have completed:
53
+
54
+ ```ruby
55
+ # We can optionally pass options that will be used when queueing the on completion job
56
+ job_group = Delayed::JobGroups::JobGroup.create!(on_completion_job: MyCompletionJob.new,
57
+ on_completion_job_options: { queue: 'general' })
58
+ ```
59
+
60
+ Registering a job to run if the job group is canceled or fails:
61
+
62
+ ```ruby
63
+ # We can optionally pass options that will be used when queueing the on cancellation job
64
+ job_group = Delayed::JobGroups::JobGroup.create!(on_cancellation_job: MyCancellationJob.new,
65
+ on_cancellation_job_options: { queue: 'general' })
66
+ ```
67
+
68
+ Block and unblock jobs in a job group:
69
+
70
+ ```ruby
71
+ # Construct the JobGroup in a blocked state
72
+ job_group = Delayed::JobGroups::JobGroup.create!(blocked: true)
73
+ job_group.enqueue(MyJob.new('some arg'), queue: 'general')
74
+ job_group.mark_queueing_complete
75
+
76
+ # Do more stuff...
77
+
78
+ # Unblock the JobGroup so its jobs can run
79
+ job_group.unblock
80
+ ```
81
+
82
+ Cancel a job group:
83
+
84
+ ```ruby
85
+ job_group = Delayed::JobGroups::JobGroup.create!
86
+
87
+ # Do more stuff...
88
+
89
+ job_group.cancel
90
+ ```
91
+
92
+ ## Supported Platforms
93
+
94
+ * Only the Delayed Job Active Record backend is supported.
95
+ * Tested with Rails 3.2 and 4.0.
96
+ * Tested with MRI 1.9.3, 2.0.0, 2.1.0 and JRuby in 1.9 mode.
97
+
98
+ ## Contributing
99
+
100
+ 1. Fork it
101
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
102
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
103
+ 4. Push to the branch (`git push origin my-new-feature`)
104
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |task|
6
+ task.verbose = false
7
+ end
8
+
9
+ task default: :spec
@@ -0,0 +1,37 @@
1
+ # encoding: UTF-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'delayed/job_groups/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'delayed_job_groups_plugin'
8
+ spec.version = Delayed::JobGroups::VERSION
9
+ spec.authors = ['Joel Turkel']
10
+ spec.email = ['jturkel@salsify.com']
11
+ spec.description = %q{Aggregates Delayed::Job jobs into groups with group level management and lifecycle callbacks}
12
+ spec.summary = %q{Delayed::Job job groups plugin}
13
+ spec.homepage = 'https://github.com/salsify/delayed_job_groups_plugin'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.test_files = Dir.glob('spec/**/*')
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_dependency 'delayed_job', '>= 3.0'
21
+ spec.add_dependency 'delayed_job_active_record', '>= 0.4'
22
+
23
+ spec.add_development_dependency 'activerecord', ENV.fetch('RAILS_VERSION', ['>= 3.2', '< 4.1'])
24
+ spec.add_development_dependency 'coveralls'
25
+ spec.add_development_dependency 'database_cleaner', '>= 1.2'
26
+ spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'rspec', '>= 2.14'
28
+ spec.add_development_dependency 'simplecov', '~> 0.7.1'
29
+
30
+ if RUBY_PLATFORM == 'java'
31
+ spec.add_development_dependency 'jdbc-sqlite3'
32
+ spec.add_development_dependency 'activerecord-jdbcsqlite3-adapter'
33
+ else
34
+ spec.add_development_dependency 'sqlite3'
35
+ end
36
+
37
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'active_support/version'
4
+ require 'active_record/version'
5
+
6
+ module Delayed
7
+ module JobGroups
8
+ module Compatibility
9
+
10
+ def self.mass_assignment_security_enabled?
11
+ ::ActiveRecord::VERSION::MAJOR < 4 || defined?(::ActiveRecord::MassAssignmentSecurity)
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: UTF-8
2
+
3
+ module Delayed
4
+ module JobGroups
5
+ module JobExtensions
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ if Delayed::JobGroups::Compatibility.mass_assignment_security_enabled?
10
+ attr_accessible :job_group_id, :blocked
11
+ end
12
+
13
+ belongs_to :job_group, class_name: 'Delayed::JobGroups::JobGroup'
14
+
15
+ class << self
16
+
17
+ # Patch ready_to_run to exclude blocked jobs
18
+ def ready_to_run_with_blocked_filtering(worker_name, max_run_time)
19
+ ready_to_run_without_blocked_filtering(worker_name, max_run_time).where(blocked: false)
20
+ end
21
+ alias_method_chain :ready_to_run, :blocked_filtering
22
+ end
23
+ end
24
+
25
+ def in_job_group?
26
+ job_group_id.present?
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,99 @@
1
+ # encoding: UTF-8
2
+
3
+ module Delayed
4
+ module JobGroups
5
+ class JobGroup < ActiveRecord::Base
6
+
7
+ self.table_name = "#{ActiveRecord::Base.table_name_prefix}delayed_job_groups"
8
+
9
+ if Delayed::JobGroups::Compatibility.mass_assignment_security_enabled?
10
+ attr_accessible :on_completion_job, :on_completion_job_options, :blocked, :on_cancellation_job,
11
+ :on_cancellation_job_options
12
+ end
13
+
14
+ serialize :on_completion_job
15
+ serialize :on_completion_job_options, Hash
16
+ serialize :on_cancellation_job
17
+ serialize :on_cancellation_job_options, Hash
18
+
19
+ validates :queueing_complete, :blocked, inclusion: [true, false]
20
+
21
+ if ActiveRecord::VERSION::MAJOR >= 4
22
+ has_many :active_jobs, -> { where(failed_at: nil) }, class_name: Job
23
+ else
24
+ has_many :active_jobs, class_name: Job, conditions: {failed_at: nil}
25
+ end
26
+
27
+
28
+ # Only delete dependent jobs that are unlocked so we can determine if there are in-flight jobs
29
+ # for canceled job groups
30
+ if ActiveRecord::VERSION::MAJOR >= 4
31
+ has_many :queued_jobs, -> { where(failed_at: nil, locked_by: nil) }, class_name: Job,
32
+ dependent: :delete_all
33
+ else
34
+ has_many :queued_jobs, class_name: Job, conditions: {failed_at: nil, locked_by: nil},
35
+ dependent: :delete_all
36
+ end
37
+
38
+ def mark_queueing_complete
39
+ with_lock do
40
+ raise 'JobGroup has already completed queueing' if queueing_complete?
41
+ update_column(:queueing_complete, true)
42
+ complete if ready_for_completion?
43
+ end
44
+ end
45
+
46
+ def enqueue(job, options = {})
47
+ options = options.merge(job_group_id: id)
48
+ options[:blocked] = blocked?
49
+ Delayed::Job.enqueue(job, options)
50
+ end
51
+
52
+ def unblock
53
+ return unless blocked?
54
+
55
+ with_lock do
56
+ update_column(:blocked, false)
57
+ active_jobs.update_all(blocked: false)
58
+ complete if ready_for_completion?
59
+ end
60
+ end
61
+
62
+ def cancel
63
+ Delayed::Job.enqueue(on_cancellation_job, on_cancellation_job_options || {}) if on_cancellation_job
64
+ destroy
65
+ end
66
+
67
+ def self.check_for_completion(job_group_id)
68
+ # Optimization to avoid loading and locking the JobGroup when the group
69
+ # still has pending jobs
70
+ return if has_pending_jobs?(job_group_id)
71
+
72
+ transaction do
73
+ # The first completed job to notice the job group's queue count has dropped to
74
+ # zero will queue the job group's completion job and destroy the job group so
75
+ # other jobs need to handle the job group having been destroyed already.
76
+ job_group = where(id: job_group_id).lock(true).first
77
+ job_group.send(:complete) if job_group && job_group.send(:ready_for_completion?)
78
+ end
79
+ end
80
+
81
+ def self.has_pending_jobs?(job_group_ids)
82
+ job_group_ids = Array(job_group_ids)
83
+ return false if job_group_ids.empty?
84
+ Delayed::Job.where(job_group_id: job_group_ids, failed_at: nil).exists?
85
+ end
86
+
87
+ private
88
+
89
+ def ready_for_completion?
90
+ queueing_complete? && !JobGroup.has_pending_jobs?(id) && !blocked?
91
+ end
92
+
93
+ def complete
94
+ Delayed::Job.enqueue(on_completion_job, on_completion_job_options || {}) if on_completion_job
95
+ destroy
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,65 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'delayed_job'
4
+ require 'set'
5
+
6
+ module Delayed
7
+ module JobGroups
8
+ class Plugin < Delayed::Plugin
9
+
10
+ # Delayed job callbacks will be registered in a global Delayed::Lifecycle every time a
11
+ # Delayed::Worker is created. This creates problems in test runs that create
12
+ # multiple workers because we register the callbacks multiple times on the same
13
+ # global Lifecycle.
14
+ def self.callbacks(&block)
15
+ registered_lifecycles = Set.new
16
+ super do |lifecycle|
17
+ if registered_lifecycles.add?(lifecycle.object_id)
18
+ block.call(lifecycle)
19
+ end
20
+ end
21
+ end
22
+
23
+ callbacks do |lifecycle|
24
+ lifecycle.before(:error) do |worker, job|
25
+ # If the job group has been cancelled then don't let the job be retried
26
+ if job.in_job_group? && job_group_cancelled?(job.job_group_id)
27
+ def job.max_attempts
28
+ 1
29
+ end
30
+ end
31
+ end
32
+
33
+ lifecycle.before(:failure) do |worker, job|
34
+ # If a job in the job group fails, then cancel the whole job group.
35
+ # Need to check that the job group is present since another
36
+ # job may have concurrently cancelled it.
37
+ if job.in_job_group? && job.job_group
38
+ job.job_group.cancel
39
+ end
40
+ end
41
+
42
+ lifecycle.after(:perform) do |worker, job|
43
+ # Make sure we only check to see if the job group is complete
44
+ # if the job succeeded
45
+ if job.in_job_group? && job_completed?(job)
46
+ JobGroup.check_for_completion(job.job_group_id)
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def self.job_group_cancelled?(job_group_id)
54
+ !JobGroup.exists?(job_group_id)
55
+ end
56
+
57
+ def self.job_completed?(job)
58
+ # Delayed job will already have marked the job for destruction
59
+ # if it has completed
60
+ job.destroyed?
61
+ end
62
+ end
63
+ end
64
+ end
65
+
@@ -0,0 +1,7 @@
1
+ # encoding: UTF-8
2
+
3
+ module Delayed
4
+ module JobGroups
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'active_support'
4
+ require 'active_record'
5
+ require 'delayed_job'
6
+ require 'delayed_job_active_record'
7
+ require 'delayed/job_groups/compatibility'
8
+ require 'delayed/job_groups/job_extensions'
9
+ require 'delayed/job_groups/job_group'
10
+ require 'delayed/job_groups/plugin'
11
+ require 'delayed/job_groups/version'
12
+
13
+ Delayed::Backend::ActiveRecord::Job.send(:include, Delayed::JobGroups::JobExtensions)
14
+
15
+ Delayed::Worker.plugins << Delayed::JobGroups::Plugin
@@ -0,0 +1,22 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/migration'
5
+ require 'rails/generators/active_record'
6
+
7
+ module DelayedJobGroupsPlugin
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ self.source_paths << File.join(File.dirname(__FILE__), 'templates')
12
+
13
+ def create_migration_file
14
+ migration_template('migration.rb', 'db/migrate/create_delayed_job_groups.rb')
15
+ end
16
+
17
+ def self.next_migration_number(dirname)
18
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: UTF-8
2
+
3
+ class CreateDelayedJobGroups < ActiveRecord::Migration
4
+
5
+ def up
6
+ add_column(:delayed_jobs, :blocked, :boolean, default: false, null: false)
7
+ add_column(:delayed_jobs, :job_group_id, :integer)
8
+ add_index(:delayed_jobs, :job_group_id)
9
+
10
+ if partial_indexes_supported?
11
+ remove_index(:delayed_jobs, name: :delayed_jobs_priority)
12
+ execute <<-SQL
13
+ CREATE INDEX delayed_jobs_priority
14
+ ON delayed_jobs(priority, run_at)
15
+ WHERE failed_at IS NULL AND blocked = FALSE
16
+ SQL
17
+ end
18
+
19
+ create_table(:delayed_job_groups) do |t|
20
+ t.text :on_completion_job
21
+ t.text :on_completion_job_options
22
+ t.text :on_cancellation_job
23
+ t.text :on_cancellation_job_options
24
+ t.boolean :queueing_complete, default: false, null: false
25
+ t.boolean :blocked, default: false, null: false
26
+ end
27
+ end
28
+
29
+ def down
30
+ remove_columns(:delayed_jobs, :blocked, :job_group_id)
31
+
32
+ if partial_indexes_supported?
33
+ execute <<-SQL
34
+ CREATE INDEX delayed_jobs_priority
35
+ ON delayed_jobs(priority, run_at)
36
+ WHERE failed_at IS NULL
37
+ SQL
38
+ end
39
+
40
+ drop_table(:delayed_job_groups)
41
+ end
42
+
43
+ def partial_indexes_supported?
44
+ connection.adapter_name == 'PostgreSQL'
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: ":memory:"
data/spec/db/schema.rb ADDED
@@ -0,0 +1,29 @@
1
+ # encoding: UTF-8
2
+
3
+ ActiveRecord::Schema.define(:version => 0) do
4
+
5
+ create_table(:delayed_jobs, force: true) do |t|
6
+ t.integer :priority, default: 0
7
+ t.integer :attempts, default: 0
8
+ t.text :handler
9
+ t.text :last_error
10
+ t.datetime :run_at
11
+ t.datetime :locked_at
12
+ t.datetime :failed_at
13
+ t.string :locked_by
14
+ t.string :queue
15
+ t.timestamps
16
+ t.boolean :blocked, default: false, null: false
17
+ t.integer :job_group_id
18
+ end
19
+ add_index(:delayed_jobs, :job_group_id)
20
+
21
+ create_table(:delayed_job_groups, force: true) do |t|
22
+ t.text :on_completion_job
23
+ t.text :on_completion_job_options
24
+ t.text :on_cancellation_job
25
+ t.text :on_cancellation_job_options
26
+ t.boolean :queueing_complete, default: false, null: false
27
+ t.boolean :blocked, default: false, null: false
28
+ end
29
+ end
@@ -0,0 +1,223 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Delayed::JobGroups::JobGroup do
6
+
7
+ let(:blocked) { false }
8
+ let(:on_completion_job) { 'dummy on completion job' }
9
+ let(:on_completion_job_options) do
10
+ { foo: 'bar' }
11
+ end
12
+ let(:current_time) { Time.utc(2013) }
13
+
14
+ subject(:job_group) do
15
+ Delayed::JobGroups::JobGroup.create!(on_completion_job: on_completion_job,
16
+ on_completion_job_options: on_completion_job_options,
17
+ blocked: blocked)
18
+ end
19
+
20
+ before do
21
+ Time.stub(:now).and_return(current_time)
22
+ Delayed::Job.stub(:enqueue)
23
+ end
24
+
25
+ shared_examples "the job group was completed" do
26
+ it "queues the completion job" do
27
+ Delayed::Job.should have_received(:enqueue).with(on_completion_job, on_completion_job_options)
28
+ end
29
+
30
+ it "destroys the job group" do
31
+ job_group.should have_been_destroyed
32
+ end
33
+ end
34
+
35
+ shared_examples "the job group was not completed" do
36
+ it "does not queue the completion job" do
37
+ Delayed::Job.should_not have_received(:enqueue)
38
+ end
39
+
40
+ it "does not destroy the job group" do
41
+ job_group.should_not have_been_destroyed
42
+ end
43
+ end
44
+
45
+ describe "#mark_queueing_complete" do
46
+
47
+ context "when no jobs exist" do
48
+ before { job_group.mark_queueing_complete }
49
+
50
+ it { should be_queueing_complete }
51
+ it_behaves_like "the job group was completed"
52
+ end
53
+
54
+ context "when no jobs exist but the job group is blocked" do
55
+ let(:blocked) { true }
56
+ before { job_group.mark_queueing_complete }
57
+
58
+ it { should be_queueing_complete }
59
+ it_behaves_like "the job group was not completed"
60
+ end
61
+
62
+ context "when active jobs exist" do
63
+ before do
64
+ Delayed::Job.create!(job_group_id: job_group.id)
65
+ job_group.mark_queueing_complete
66
+ end
67
+
68
+ it { should be_queueing_complete }
69
+ it_behaves_like "the job group was not completed"
70
+ end
71
+ end
72
+
73
+ describe ".check_for_completion" do
74
+ let!(:job) { Delayed::Job.create!(job_group_id: job_group.id) }
75
+
76
+ before do
77
+ job_group.mark_queueing_complete
78
+ end
79
+
80
+ shared_context "complete job and check job group complete" do
81
+ before do
82
+ job.destroy
83
+ Delayed::JobGroups::JobGroup.check_for_completion(job_group.id)
84
+ end
85
+ end
86
+
87
+ context "when no jobs exist" do
88
+ include_context "complete job and check job group complete"
89
+
90
+ it_behaves_like "the job group was completed"
91
+ end
92
+
93
+ context "when active jobs exist" do
94
+ before do
95
+ Delayed::JobGroups::JobGroup.check_for_completion(job_group.id)
96
+ end
97
+
98
+ it_behaves_like "the job group was not completed"
99
+ end
100
+
101
+ context "when on failed jobs exist" do
102
+ before do
103
+ job.update_attributes!(failed_at: Time.now)
104
+ Delayed::JobGroups::JobGroup.check_for_completion(job_group.id)
105
+ end
106
+
107
+ it_behaves_like "the job group was completed"
108
+ end
109
+
110
+ context "when there are no on_completion_job_options" do
111
+ let(:on_completion_job_options) { nil }
112
+
113
+ include_context "complete job and check job group complete"
114
+
115
+ it "queues the completion job with empty options" do
116
+ Delayed::Job.should have_received(:enqueue).with(on_completion_job, {})
117
+ end
118
+
119
+ it "destroys the job group" do
120
+ job_group.should have_been_destroyed
121
+ end
122
+ end
123
+
124
+ context "when there is no on_completion_job" do
125
+ let(:on_completion_job) { nil }
126
+
127
+ include_context "complete job and check job group complete"
128
+
129
+ it "doesn't queues the non-existent completion job" do
130
+ Delayed::Job.should_not have_received(:enqueue)
131
+ end
132
+
133
+ it "destroys the job group" do
134
+ job_group.should have_been_destroyed
135
+ end
136
+ end
137
+ end
138
+
139
+ describe "#enqueue" do
140
+ let(:job) { 'dummy job' }
141
+
142
+ before do
143
+ job_group.enqueue(job)
144
+ end
145
+
146
+ shared_examples "it enqueues the job in the correct blocked state" do
147
+ it "enqueues the job in the same blocked state as the job group" do
148
+ Delayed::Job.should have_received(:enqueue).with(job, job_group_id: job_group.id, blocked: blocked)
149
+ end
150
+ end
151
+
152
+ it_behaves_like "it enqueues the job in the correct blocked state"
153
+
154
+ context "when the job_group is blocked" do
155
+ let(:blocked) { true }
156
+
157
+ it_behaves_like "it enqueues the job in the correct blocked state"
158
+ end
159
+ end
160
+
161
+ describe "#unblock" do
162
+
163
+ context "when the JobGroup isn't blocked" do
164
+ before do
165
+ job_group.unblock
166
+ end
167
+
168
+ its(:blocked?) { should be_false }
169
+ end
170
+
171
+ context "when the JobGroup is blocked" do
172
+ let(:blocked) { true }
173
+
174
+ context "when there are pending jobs" do
175
+ let!(:job) { Delayed::Job.create!(job_group_id: job_group.id, blocked: true) }
176
+
177
+ before do
178
+ job_group.mark_queueing_complete
179
+ job_group.unblock
180
+ end
181
+
182
+ its(:blocked?) { should be_false }
183
+
184
+ it "sets the job's run_at to the current time" do
185
+ job.reload.run_at.should eq current_time
186
+ end
187
+
188
+ it_behaves_like "the job group was not completed"
189
+ end
190
+
191
+ describe "when there are no pending jobs" do
192
+ before do
193
+ job_group.mark_queueing_complete
194
+ job_group.unblock
195
+ end
196
+
197
+ its(:blocked?) { should be_false }
198
+ it_behaves_like "the job group was completed"
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "#cancel" do
204
+ let!(:queued_job) { Delayed::Job.create!(job_group_id: job_group.id) }
205
+ let!(:running_job) { Delayed::Job.create!(job_group_id: job_group.id, locked_at: Time.now, locked_by: 'test') }
206
+
207
+ before do
208
+ job_group.cancel
209
+ end
210
+
211
+ it "destroys the job group" do
212
+ job_group.should have_been_destroyed
213
+ end
214
+
215
+ it "destroys queued jobs" do
216
+ queued_job.should have_been_destroyed
217
+ end
218
+
219
+ it "does not destroy running jobs" do
220
+ running_job.should_not have_been_destroyed
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,255 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Delayed::JobGroups::Plugin do
6
+
7
+ before(:all) do
8
+ @old_max_attempts = Delayed::Worker.max_attempts
9
+ Delayed::Worker.max_attempts = 2
10
+ end
11
+
12
+ after(:all) do
13
+ Delayed::Worker.max_attempts = @old_max_attempts
14
+ end
15
+
16
+ before(:each) do
17
+ CompletionJob.invoked = false
18
+ CancellationJob.invoked = false
19
+ end
20
+
21
+ let!(:job_group) { Delayed::JobGroups::JobGroup.create!(on_completion_job: CompletionJob.new) }
22
+
23
+ it "runs the completion job after completing other jobs" do
24
+ job_group.enqueue(NoOpJob.new)
25
+ job_group.enqueue(NoOpJob.new)
26
+ job_group.mark_queueing_complete
27
+ job_group_count.should eq 1
28
+ queued_job_count.should eq 2
29
+
30
+ # Run our first job
31
+ Delayed::Worker.new.work_off(1)
32
+ CompletionJob.invoked.should be_false
33
+ job_group_count.should eq 1
34
+ queued_job_count.should eq 1
35
+
36
+ # Run our second job which should enqueue the completion job
37
+ Delayed::Worker.new.work_off(1)
38
+ CompletionJob.invoked.should be_false
39
+ job_group_count.should eq 0
40
+ queued_job_count.should eq 1
41
+
42
+ # Now we should run the completion job
43
+ Delayed::Worker.new.work_off(1)
44
+ CompletionJob.invoked.should be_true
45
+ queued_job_count.should eq 0
46
+ end
47
+
48
+ it "only runs the completion job after queueing is completed" do
49
+ job_group.enqueue(NoOpJob.new)
50
+ job_group.enqueue(NoOpJob.new)
51
+ job_group_count.should eq 1
52
+ queued_job_count.should eq 2
53
+
54
+ # Run our first job
55
+ Delayed::Worker.new.work_off(1)
56
+ CompletionJob.invoked.should be_false
57
+ job_group_count.should eq 1
58
+ queued_job_count.should eq 1
59
+
60
+ # Run our second job
61
+ Delayed::Worker.new.work_off(1)
62
+ CompletionJob.invoked.should be_false
63
+ job_group_count.should eq 1
64
+ queued_job_count.should eq 0
65
+
66
+ # Mark queueing complete which should queue the completion job
67
+ job_group.mark_queueing_complete
68
+ job_group_count.should eq 0
69
+ queued_job_count.should eq 1
70
+
71
+ # Now we should run the completion job
72
+ Delayed::Worker.new.work_off(1)
73
+ CompletionJob.invoked.should be_true
74
+ queued_job_count.should eq 0
75
+ end
76
+
77
+ it "cancels the job group if a job fails" do
78
+ Delayed::Worker.max_attempts = 1
79
+
80
+ job_group.enqueue(FailingJob.new)
81
+ job_group.enqueue(NoOpJob.new)
82
+ job_group.mark_queueing_complete
83
+ queued_job_count.should eq 2
84
+ job_group_count.should eq 1
85
+
86
+ # Run the job which should fail and cancel the JobGroup
87
+ Delayed::Worker.new.work_off(1)
88
+ CompletionJob.invoked.should be_false
89
+ failed_job_count.should eq 1
90
+ queued_job_count.should eq 0
91
+ job_group_count.should eq 0
92
+ end
93
+
94
+ it "doesn't retry failed jobs if the job group has been canceled" do
95
+ job_group.cancel
96
+ Delayed::Job.enqueue(FailingJob.new, job_group_id: job_group.id)
97
+ queued_job_count.should eq 1
98
+
99
+ # Run the job which should fail and should not queue a retry
100
+ Delayed::Worker.new.work_off(1)
101
+ failed_job_count.should eq 1
102
+ queued_job_count.should eq 0
103
+ end
104
+
105
+ it "doesn't run jobs until they're unblocked" do
106
+ job_group.blocked = true
107
+ job_group.save!
108
+
109
+ job_group.enqueue(NoOpJob.new)
110
+ job_group.enqueue(NoOpJob.new)
111
+ job_group.mark_queueing_complete
112
+ Delayed::Job.count.should eq 2
113
+
114
+ # No jobs should run because they're blocked
115
+ (successes, failures) = Delayed::Worker.new.work_off
116
+ successes.should eq 0
117
+ failures.should eq 0
118
+ Delayed::Job.count.should eq 2
119
+
120
+ job_group.unblock
121
+
122
+ # Run our first job
123
+ Delayed::Worker.new.work_off(1)
124
+ CompletionJob.invoked.should be_false
125
+ job_group_count.should eq 1
126
+ Delayed::Job.count.should eq 1
127
+
128
+ # Run our second job which should enqueue the completion job
129
+ Delayed::Worker.new.work_off(1)
130
+ CompletionJob.invoked.should be_false
131
+ job_group_count.should eq 0
132
+ Delayed::Job.count.should eq 1
133
+
134
+ # Now we should run the completion job
135
+ Delayed::Worker.new.work_off(1)
136
+ CompletionJob.invoked.should be_true
137
+ Delayed::Job.count.should eq 0
138
+ end
139
+
140
+ context "when a cancellation job is provided" do
141
+ let!(:job_group) do
142
+ Delayed::JobGroups::JobGroup.create!(on_completion_job: CompletionJob.new,
143
+ on_cancellation_job: CancellationJob.new)
144
+ end
145
+
146
+ it "runs the cancellation job after a job error causes cancellation" do
147
+ Delayed::Worker.max_attempts = 1
148
+
149
+ job_group.enqueue(FailingJob.new)
150
+ job_group.enqueue(NoOpJob.new)
151
+ job_group.mark_queueing_complete
152
+ queued_job_count.should eq 2
153
+ job_group_count.should eq 1
154
+
155
+ # Run the job which should fail and cancel the JobGroup
156
+ Delayed::Worker.new.work_off(1)
157
+ CompletionJob.invoked.should be_false
158
+ CancellationJob.invoked.should be_false
159
+ failed_job_count.should eq 1
160
+
161
+ queued_job_count.should eq 1
162
+ job_group_count.should eq 0
163
+
164
+ # Now we should run the cancellation job
165
+ Delayed::Worker.new.work_off(1)
166
+ CompletionJob.invoked.should be_false
167
+ CancellationJob.invoked.should be_true
168
+ queued_job_count.should eq 0
169
+ end
170
+
171
+ it "runs the cancellation job after the job group is cancelled" do
172
+ job_group.enqueue(NoOpJob.new)
173
+ job_group.enqueue(FailingJob.new)
174
+ job_group.mark_queueing_complete
175
+ job_group.cancel
176
+
177
+ #cancellation job should be queued
178
+ queued_job_count.should eq 1
179
+ CancellationJob.invoked.should be_false
180
+
181
+ # Run the cancellation job
182
+ Delayed::Worker.new.work_off(1)
183
+ CancellationJob.invoked.should be_true
184
+ queued_job_count.should eq 0
185
+ end
186
+ end
187
+
188
+ context "when a no completion job is provided" do
189
+ let!(:job_group) { Delayed::JobGroups::JobGroup.create! }
190
+
191
+ it "doesn't queue a non-existent completion job" do
192
+ job_group.enqueue(NoOpJob.new)
193
+ job_group.enqueue(NoOpJob.new)
194
+ job_group.mark_queueing_complete
195
+ job_group_count.should eq 1
196
+ queued_job_count.should eq 2
197
+ failed_job_count.should eq 0
198
+
199
+ # Run our first job
200
+ Delayed::Worker.new.work_off(1)
201
+ job_group_count.should eq 1
202
+ queued_job_count.should eq 1
203
+ failed_job_count.should eq 0
204
+
205
+ # Run our second job which should delete the job group
206
+ Delayed::Worker.new.work_off(1)
207
+ job_group_count.should eq 0
208
+ queued_job_count.should eq 0
209
+ failed_job_count.should eq 0
210
+ end
211
+ end
212
+
213
+ class FailingJob
214
+
215
+ def perform
216
+ raise 'Test failure'
217
+ end
218
+
219
+ end
220
+
221
+ class NoOpJob
222
+
223
+ def perform
224
+
225
+ end
226
+ end
227
+
228
+ class CompletionJob
229
+ cattr_accessor :invoked
230
+
231
+ def perform
232
+ CompletionJob.invoked = true
233
+ end
234
+ end
235
+
236
+ class CancellationJob
237
+ cattr_accessor :invoked
238
+
239
+ def perform
240
+ CancellationJob.invoked = true
241
+ end
242
+ end
243
+
244
+ def job_group_count
245
+ Delayed::JobGroups::JobGroup.count
246
+ end
247
+
248
+ def queued_job_count
249
+ Delayed::Job.where(failed_at: nil).count
250
+ end
251
+
252
+ def failed_job_count
253
+ Delayed::Job.where('failed_at IS NOT NULL').count
254
+ end
255
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'simplecov'
4
+ require 'coveralls'
5
+
6
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
7
+ SimpleCov::Formatter::HTMLFormatter,
8
+ Coveralls::SimpleCov::Formatter
9
+ ]
10
+ SimpleCov.start do
11
+ add_filter 'spec'
12
+ end
13
+
14
+ require 'database_cleaner'
15
+ require 'delayed_job_groups_plugin'
16
+ require 'yaml'
17
+
18
+ spec_dir = File.dirname(__FILE__)
19
+ Dir["#{spec_dir}/support/**/*.rb"].sort.each { |f| require f }
20
+
21
+ FileUtils.makedirs('log')
22
+
23
+ Delayed::Worker.read_ahead = 1
24
+ Delayed::Worker.destroy_failed_jobs = false
25
+
26
+ Delayed::Worker.logger = Logger.new('log/test.log')
27
+ Delayed::Worker.logger.level = Logger::DEBUG
28
+ ActiveRecord::Base.logger = Delayed::Worker.logger
29
+ ActiveRecord::Migration.verbose = false
30
+
31
+ db_adapter = ENV.fetch('ADAPTER', 'sqlite3')
32
+ config = YAML.load(File.read('spec/db/database.yml'))
33
+ ActiveRecord::Base.establish_connection(config[db_adapter])
34
+ require 'db/schema'
35
+
36
+ RSpec.configure do |config|
37
+ config.treat_symbols_as_metadata_keys_with_true_values = true
38
+ config.order = 'random'
39
+
40
+ config.before(:suite) do
41
+ DatabaseCleaner.clean_with(:truncation)
42
+ end
43
+
44
+ config.before(:each) do
45
+ DatabaseCleaner.strategy = :transaction
46
+ end
47
+
48
+ config.before(:each) do
49
+ DatabaseCleaner.start
50
+ end
51
+
52
+ config.after(:each) do
53
+ DatabaseCleaner.clean
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: UTF-8
2
+
3
+ RSpec::Matchers.define :have_been_destroyed do
4
+ match do |actual|
5
+ !actual.class.where(id: actual.id).exists?
6
+ end
7
+
8
+ description do
9
+ "model should have been destroyed"
10
+ end
11
+
12
+ failure_message_for_should do |actual|
13
+ "expected #{actual.class}(id: #{actual.id}) to have been destroyed"
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,205 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: delayed_job_groups_plugin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joel Turkel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: delayed_job
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: delayed_job_active_record
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '3.2'
48
+ - - <
49
+ - !ruby/object:Gem::Version
50
+ version: '4.1'
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '3.2'
58
+ - - <
59
+ - !ruby/object:Gem::Version
60
+ version: '4.1'
61
+ - !ruby/object:Gem::Dependency
62
+ name: coveralls
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: database_cleaner
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '1.2'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '1.2'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '2.14'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '2.14'
117
+ - !ruby/object:Gem::Dependency
118
+ name: simplecov
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ~>
122
+ - !ruby/object:Gem::Version
123
+ version: 0.7.1
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ~>
129
+ - !ruby/object:Gem::Version
130
+ version: 0.7.1
131
+ - !ruby/object:Gem::Dependency
132
+ name: sqlite3
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - '>='
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ description: Aggregates Delayed::Job jobs into groups with group level management
146
+ and lifecycle callbacks
147
+ email:
148
+ - jturkel@salsify.com
149
+ executables: []
150
+ extensions: []
151
+ extra_rdoc_files: []
152
+ files:
153
+ - .gitignore
154
+ - .rspec
155
+ - .travis.yml
156
+ - Gemfile
157
+ - LICENSE.txt
158
+ - README.md
159
+ - Rakefile
160
+ - delayed_job_groups.gemspec
161
+ - lib/delayed/job_groups/compatibility.rb
162
+ - lib/delayed/job_groups/job_extensions.rb
163
+ - lib/delayed/job_groups/job_group.rb
164
+ - lib/delayed/job_groups/plugin.rb
165
+ - lib/delayed/job_groups/version.rb
166
+ - lib/delayed_job_groups_plugin.rb
167
+ - lib/generators/delayed_job_groups_plugin/install_generator.rb
168
+ - lib/generators/delayed_job_groups_plugin/templates/migration.rb
169
+ - spec/db/database.yml
170
+ - spec/db/schema.rb
171
+ - spec/delayed/job_groups/job_group_spec.rb
172
+ - spec/delayed/job_groups/plugin_spec.rb
173
+ - spec/spec_helper.rb
174
+ - spec/support/destroyed_model.rb
175
+ homepage: https://github.com/salsify/delayed_job_groups_plugin
176
+ licenses:
177
+ - MIT
178
+ metadata: {}
179
+ post_install_message:
180
+ rdoc_options: []
181
+ require_paths:
182
+ - lib
183
+ required_ruby_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - '>='
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ required_rubygems_version: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - '>='
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ requirements: []
194
+ rubyforge_project:
195
+ rubygems_version: 2.0.14
196
+ signing_key:
197
+ specification_version: 4
198
+ summary: Delayed::Job job groups plugin
199
+ test_files:
200
+ - spec/db/database.yml
201
+ - spec/db/schema.rb
202
+ - spec/delayed/job_groups/job_group_spec.rb
203
+ - spec/delayed/job_groups/plugin_spec.rb
204
+ - spec/spec_helper.rb
205
+ - spec/support/destroyed_model.rb