delayed_job_groups_plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +104 -0
- data/Rakefile +9 -0
- data/delayed_job_groups.gemspec +37 -0
- data/lib/delayed/job_groups/compatibility.rb +16 -0
- data/lib/delayed/job_groups/job_extensions.rb +30 -0
- data/lib/delayed/job_groups/job_group.rb +99 -0
- data/lib/delayed/job_groups/plugin.rb +65 -0
- data/lib/delayed/job_groups/version.rb +7 -0
- data/lib/delayed_job_groups_plugin.rb +15 -0
- data/lib/generators/delayed_job_groups_plugin/install_generator.rb +22 -0
- data/lib/generators/delayed_job_groups_plugin/templates/migration.rb +46 -0
- data/spec/db/database.yml +3 -0
- data/spec/db/schema.rb +29 -0
- data/spec/delayed/job_groups/job_group_spec.rb +223 -0
- data/spec/delayed/job_groups/plugin_spec.rb +255 -0
- data/spec/spec_helper.rb +55 -0
- data/spec/support/destroyed_model.rb +15 -0
- metadata +205 -0
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
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
|
+
[][travis]
|
3
|
+
[][codeclimate]
|
4
|
+
[][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,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,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
|
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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|