sidekiq-activerecord 0.0.1
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.
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +22 -0
- data/README.md +117 -0
- data/Rakefile +1 -0
- data/lib/sidekiq/activerecord/version.rb +5 -0
- data/lib/sidekiq/activerecord.rb +10 -0
- data/lib/sidekiq/manager_worker.rb +144 -0
- data/lib/sidekiq/task_worker.rb +117 -0
- data/sidekiq-activerecord.gemspec +28 -0
- data/spec/lib/sidekiq/manager_worker.rb +167 -0
- data/spec/lib/sidekiq/task_worker.rb +150 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support.rb +39 -0
- metadata +163 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.idea
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color --format documentation
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
sidekiq-activerecord (0.0.1)
|
5
|
+
activerecord (~> 4.1.1)
|
6
|
+
sidekiq (>= 2.16.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (4.1.1)
|
12
|
+
activesupport (= 4.1.1)
|
13
|
+
builder (~> 3.1)
|
14
|
+
activerecord (4.1.1)
|
15
|
+
activemodel (= 4.1.1)
|
16
|
+
activesupport (= 4.1.1)
|
17
|
+
arel (~> 5.0.0)
|
18
|
+
activesupport (4.1.1)
|
19
|
+
i18n (~> 0.6, >= 0.6.9)
|
20
|
+
json (~> 1.7, >= 1.7.7)
|
21
|
+
minitest (~> 5.1)
|
22
|
+
thread_safe (~> 0.1)
|
23
|
+
tzinfo (~> 1.1)
|
24
|
+
arel (5.0.1.20140414130214)
|
25
|
+
builder (3.2.2)
|
26
|
+
celluloid (0.15.2)
|
27
|
+
timers (~> 1.1.0)
|
28
|
+
connection_pool (2.0.0)
|
29
|
+
database_cleaner (1.2.0)
|
30
|
+
diff-lcs (1.2.5)
|
31
|
+
factory_girl (4.2.0)
|
32
|
+
activesupport (>= 3.0.0)
|
33
|
+
i18n (0.6.9)
|
34
|
+
json (1.8.1)
|
35
|
+
minitest (5.3.4)
|
36
|
+
redis (3.0.7)
|
37
|
+
redis-namespace (1.4.1)
|
38
|
+
redis (~> 3.0.4)
|
39
|
+
rspec (2.14.1)
|
40
|
+
rspec-core (~> 2.14.0)
|
41
|
+
rspec-expectations (~> 2.14.0)
|
42
|
+
rspec-mocks (~> 2.14.0)
|
43
|
+
rspec-core (2.14.8)
|
44
|
+
rspec-expectations (2.14.5)
|
45
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
46
|
+
rspec-mocks (2.14.6)
|
47
|
+
sidekiq (3.0.2)
|
48
|
+
celluloid (>= 0.15.2)
|
49
|
+
connection_pool (>= 2.0.0)
|
50
|
+
json
|
51
|
+
redis (>= 3.0.6)
|
52
|
+
redis-namespace (>= 1.3.1)
|
53
|
+
sqlite3 (1.3.9)
|
54
|
+
thread_safe (0.3.3)
|
55
|
+
timers (1.1.0)
|
56
|
+
tzinfo (1.1.0)
|
57
|
+
thread_safe (~> 0.1)
|
58
|
+
|
59
|
+
PLATFORMS
|
60
|
+
ruby
|
61
|
+
|
62
|
+
DEPENDENCIES
|
63
|
+
database_cleaner (~> 1.2.0)
|
64
|
+
factory_girl (~> 4.0)
|
65
|
+
rspec (>= 2.14)
|
66
|
+
sidekiq-activerecord!
|
67
|
+
sqlite3 (~> 1.3.9)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Adam Farhi
|
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,117 @@
|
|
1
|
+
# Sidekiq::Activerecord
|
2
|
+
|
3
|
+
Encapsulates various interactions between Sidekiq and ActiveRecord.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'sidekiq-activerecord'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install sidekiq-activerecord
|
18
|
+
|
19
|
+
# Usage
|
20
|
+
|
21
|
+
If you've been using Sidekiq for a while, you've probably noticed a recurring pattern in your workers;
|
22
|
+
|
23
|
+
## Child-Parent Workers
|
24
|
+
A parent worker which goes over some model collection and enqueues a child worker for each model in the collection.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class ParentWorker
|
28
|
+
include Sidekiq::Worker
|
29
|
+
|
30
|
+
def perform
|
31
|
+
User.active.each do |user|
|
32
|
+
ChildWorker.perform_async(user.id)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
## Sidekiq::ManagerWorker - Example
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class UserTaskWorker
|
43
|
+
include Sidekiq::TaskWorker
|
44
|
+
end
|
45
|
+
|
46
|
+
class UserSyncer
|
47
|
+
include Sidekiq::ManagerWorker
|
48
|
+
|
49
|
+
sidekiq_delegate_task_to :user_task_worker # or UserTaskWorker
|
50
|
+
sidekiq_manager_options :batch_size => 500,
|
51
|
+
:identifier_key => :user_token,
|
52
|
+
:additional_keys => [:status]
|
53
|
+
end
|
54
|
+
|
55
|
+
UserSyncer.perform_query_async(User.active, :batch_size => 300)
|
56
|
+
```
|
57
|
+
|
58
|
+
## Model Task Workers
|
59
|
+
|
60
|
+
A worker which gets a model.id (like ChildWorker above) loads it, validates it and runs some logic on the model.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class ModelTaskWorker
|
64
|
+
include Sidekiq::Worker
|
65
|
+
|
66
|
+
def perform(user_id)
|
67
|
+
user = User.find(user_id)
|
68
|
+
return unless user.present?
|
69
|
+
return unless user.active?
|
70
|
+
|
71
|
+
UserService.run(user)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
## Sidekiq::TaskWorker - Example
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class UserMailerTaskWorker
|
81
|
+
include Sidekiq::TaskWorker
|
82
|
+
|
83
|
+
sidekiq_task_model :user_model # or UserModel
|
84
|
+
sidekiq_task_options :identifier_key => :token
|
85
|
+
|
86
|
+
def perform_on_model(user, email_type)
|
87
|
+
UserMailer.deliver_registration_confirmation(user, email_type)
|
88
|
+
end
|
89
|
+
|
90
|
+
# optional
|
91
|
+
def not_found_model(token)
|
92
|
+
Log.error "User not found for token:#{token}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# optional
|
96
|
+
def model_valid?(user)
|
97
|
+
user.active?
|
98
|
+
end
|
99
|
+
|
100
|
+
# optional
|
101
|
+
def invalid_model(user)
|
102
|
+
Log.error "User #{user.token} is invalid"
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
UserMailerTaskWorker.perform(user.id, :new_email)
|
109
|
+
```
|
110
|
+
|
111
|
+
## Contributing
|
112
|
+
|
113
|
+
1. Fork it ( http://github.com/<my-github-username>/sidekiq-activerecord/fork )
|
114
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
115
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
116
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
117
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module ManagerWorker
|
3
|
+
extend Sidekiq::Worker
|
4
|
+
|
5
|
+
DEFAULT_IDENTIFIER_KEY = :id
|
6
|
+
DEFAULT_BATCH_SIZE = 1000
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
base.class_attribute :sidekiq_manager_options_hash
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
# For a given model collection, it delegates each model to a sub-worker (e.g TaskWorker)
|
16
|
+
# Specify the TaskWoker with the `sidekiq_delegate_task_to` method.
|
17
|
+
#
|
18
|
+
# @param models_query ActiveRecord::Relation
|
19
|
+
# @param options Hash
|
20
|
+
# :worker_class - the worker class to delegate the task to. Alternative to the default `sidekiq_delegate_task_to`
|
21
|
+
# :identifier_key - the model identifier column. Default 'id'
|
22
|
+
# :additional_keys - additional model keys
|
23
|
+
# :batch_size - Specifies the size of the batch. Default to 1000.
|
24
|
+
#
|
25
|
+
# @example:
|
26
|
+
# class UserTaskWorker
|
27
|
+
# include Sidekiq::TaskWorker
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# class UserSyncer
|
31
|
+
# include Sidekiq::ManagerWorker
|
32
|
+
#
|
33
|
+
# sidekiq_delegate_task_to :user_task_worker # or UserTaskWorker
|
34
|
+
# sidekiq_manager_options :batch_size => 500,
|
35
|
+
# :identifier_key => :user_token,
|
36
|
+
# :additional_keys => [:status]
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# UserSyncer.perform_query_async(User.active, :batch_size => 300)
|
40
|
+
#
|
41
|
+
#
|
42
|
+
# is equivalent to doing:
|
43
|
+
# User.active.each {|user| UserTaskWorker.peform(user.id) }
|
44
|
+
#
|
45
|
+
def perform_query_async(models_query, options={})
|
46
|
+
set_runtime_options(options)
|
47
|
+
models = models_query.select(selected_attributes)
|
48
|
+
models.find_in_batches(batch_size: batch_size) do |models_batch|
|
49
|
+
model_attributes = models_batch.map { |model| model_attributes(model) }
|
50
|
+
Sidekiq::Client.push_bulk('class' => worker_class, 'args' => model_attributes)
|
51
|
+
end
|
52
|
+
# set_runtime_options(nil)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @required
|
56
|
+
# The task worker to delegate to.
|
57
|
+
# @param worker_klass (Sidekiq::Worker, Symbol) - UserTaskWorker or :user_task_worker
|
58
|
+
def sidekiq_delegate_task_to(worker_klass)
|
59
|
+
if worker_klass.is_a?(String) or is_a?(Symbol)
|
60
|
+
worker_klass.to_s.split('_').collect(&:capitalize).join.constantize
|
61
|
+
else
|
62
|
+
worker_klass
|
63
|
+
end
|
64
|
+
self.get_sidekiq_manager_options[:worker_class] = worker_klass
|
65
|
+
end
|
66
|
+
|
67
|
+
# Allows customization for this type of ManagerWorker.
|
68
|
+
# Legal options:
|
69
|
+
#
|
70
|
+
# :worker_class - the worker class to delegate the task to. Alternative to `sidekiq_delegate_task_to`
|
71
|
+
# :identifier_key - the model identifier column. Default 'id'
|
72
|
+
# :additional_keys - additional model keys
|
73
|
+
# :batch_size - Specifies the size of the batch. Default to 1000.
|
74
|
+
def sidekiq_manager_options(opts={})
|
75
|
+
self.sidekiq_manager_options_hash = get_sidekiq_manager_options.merge((opts || {}).symbolize_keys!)
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# private
|
80
|
+
|
81
|
+
def default_worker_manager_options
|
82
|
+
{
|
83
|
+
:identifier_key => DEFAULT_IDENTIFIER_KEY,
|
84
|
+
:additional_keys => [],
|
85
|
+
:worker_class => nil,
|
86
|
+
:batch_size => DEFAULT_BATCH_SIZE,
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
# returns the model attributes array:
|
91
|
+
# [model_id, attr1, attr2, ...]
|
92
|
+
def model_attributes(model)
|
93
|
+
additional_attributes = additional_keys.map { |key| model.send(key) }
|
94
|
+
id_attribute = model.send(identifier_key)
|
95
|
+
additional_attributes.unshift(id_attribute)
|
96
|
+
end
|
97
|
+
|
98
|
+
def selected_attributes
|
99
|
+
attrs = [identifier_key, additional_keys]
|
100
|
+
attrs << DEFAULT_IDENTIFIER_KEY unless default_identifier? # :id must be included
|
101
|
+
attrs
|
102
|
+
end
|
103
|
+
|
104
|
+
def worker_class
|
105
|
+
raise NotImplementedError.new('`worker_class` was not specified') unless manager_options[:worker_class].present?
|
106
|
+
manager_options[:worker_class]
|
107
|
+
end
|
108
|
+
|
109
|
+
def default_identifier?
|
110
|
+
identifier_key == DEFAULT_IDENTIFIER_KEY
|
111
|
+
end
|
112
|
+
|
113
|
+
def identifier_key
|
114
|
+
manager_options[:identifier_key]
|
115
|
+
end
|
116
|
+
|
117
|
+
def additional_keys
|
118
|
+
manager_options[:additional_keys]
|
119
|
+
end
|
120
|
+
|
121
|
+
def batch_size
|
122
|
+
manager_options[:batch_size]
|
123
|
+
end
|
124
|
+
|
125
|
+
def manager_options
|
126
|
+
self.get_sidekiq_manager_options.merge(runtime_options)
|
127
|
+
end
|
128
|
+
|
129
|
+
def get_sidekiq_manager_options
|
130
|
+
self.sidekiq_manager_options_hash ||= default_worker_manager_options
|
131
|
+
end
|
132
|
+
|
133
|
+
def runtime_options
|
134
|
+
@sidekiq_manager_runtime_options || {}
|
135
|
+
end
|
136
|
+
|
137
|
+
def set_runtime_options(options)
|
138
|
+
options = options.delete_if { |k, v| v.nil? } if options.present?
|
139
|
+
@sidekiq_manager_runtime_options = options
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module TaskWorker
|
3
|
+
extend Sidekiq::Worker
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
base.class_attribute :sidekiq_task_options_hash
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# @example:
|
13
|
+
# class UserMailerTaskWorker
|
14
|
+
# include Sidekiq::TaskWorker
|
15
|
+
#
|
16
|
+
# sidekiq_task_model :user_model # or UserModel
|
17
|
+
# sidekiq_task_options :identifier_key => :token
|
18
|
+
#
|
19
|
+
# def perform_on_model(user, email_type)
|
20
|
+
# UserMailer.deliver_registration_confirmation(user, email_type)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def not_found_model(token)
|
24
|
+
# Log.error "User not found for token:#{token}"
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# def model_valid?(user)
|
28
|
+
# user.active?
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def invalid_model(user)
|
32
|
+
# Log.error "User #{user.token} is invalid"
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
#
|
38
|
+
# UserMailerTaskWorker.perform(user.id, :new_email)
|
39
|
+
#
|
40
|
+
def perform(identifier, *args)
|
41
|
+
model = fetch_model(identifier)
|
42
|
+
return not_found_model(identifier) unless model.present?
|
43
|
+
|
44
|
+
if model_valid?(model)
|
45
|
+
perform_on_model(model, *args)
|
46
|
+
else
|
47
|
+
invalid_model(model)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def sidekiq_task_model(model_klass)
|
52
|
+
if model_klass.is_a?(String) or model_klass.is_a?(Symbol)
|
53
|
+
model_klass = model_klass.to_s.split('_').collect(&:capitalize).join.constantize
|
54
|
+
else
|
55
|
+
model_klass
|
56
|
+
end
|
57
|
+
self.get_sidekiq_task_options[:model_class] = model_klass
|
58
|
+
end
|
59
|
+
|
60
|
+
def perform_on_model(model)
|
61
|
+
model
|
62
|
+
end
|
63
|
+
|
64
|
+
# recheck the if one of the items is still valid
|
65
|
+
def model_valid?(model)
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
# Hook to handel an invalid model
|
70
|
+
def invalid_model(model)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Hook to handel not found model
|
74
|
+
def not_found_model(identifier)
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# private
|
79
|
+
|
80
|
+
def fetch_model(identifier)
|
81
|
+
model_class.find_by(identifier_key => identifier)
|
82
|
+
end
|
83
|
+
|
84
|
+
def model_class
|
85
|
+
klass = self.get_sidekiq_task_options[:model_class]
|
86
|
+
raise NotImplementedError.new('`model_class` was not specified') unless klass.present?
|
87
|
+
klass
|
88
|
+
end
|
89
|
+
|
90
|
+
def identifier_key
|
91
|
+
self.get_sidekiq_task_options[:identifier_key]
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Allows customization for this type of TaskWorker.
|
96
|
+
# Legal options:
|
97
|
+
#
|
98
|
+
# :identifier_key - the model identifier column. Default 'id'
|
99
|
+
def sidekiq_task_options(opts={})
|
100
|
+
self.sidekiq_task_options_hash = get_sidekiq_task_options.merge((opts || {}).symbolize_keys!)
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_sidekiq_task_options
|
104
|
+
self.sidekiq_task_options_hash ||= default_worker_task_options
|
105
|
+
end
|
106
|
+
|
107
|
+
def default_worker_task_options
|
108
|
+
{
|
109
|
+
:identifier_key => :id,
|
110
|
+
:model_class => nil
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sidekiq/activerecord/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "sidekiq-activerecord"
|
8
|
+
spec.version = Sidekiq::Activerecord::VERSION
|
9
|
+
spec.authors = ["Adam Farhi"]
|
10
|
+
spec.email = ["yelled3@gmail.com"]
|
11
|
+
spec.summary = 'Encapsulates various interactions between Sidekiq and ActiveRecord'
|
12
|
+
spec.description = 'Encapsulates various interactions between Sidekiq and ActiveRecord'
|
13
|
+
spec.homepage = "https://github.com/yelled3/sidekiq-activerecord"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'sidekiq', '>= 2.16.0'
|
22
|
+
spec.add_dependency 'activerecord', '~> 4.1.1'
|
23
|
+
|
24
|
+
spec.add_development_dependency "rspec", ">= 2.14"
|
25
|
+
spec.add_development_dependency "database_cleaner", '~> 1.2.0'
|
26
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3.9'
|
27
|
+
spec.add_development_dependency 'factory_girl', "~> 4.0"
|
28
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sidekiq::ManagerWorker do
|
4
|
+
|
5
|
+
before do
|
6
|
+
allow(Sidekiq::Client).to receive(:push_bulk)
|
7
|
+
end
|
8
|
+
|
9
|
+
class MockUserWorker; end
|
10
|
+
|
11
|
+
let(:worker_class) { MockUserWorker }
|
12
|
+
let(:sidekiq_client) { Sidekiq::Client }
|
13
|
+
|
14
|
+
class UserManagerWorker
|
15
|
+
include Sidekiq::ManagerWorker
|
16
|
+
sidekiq_delegate_task_to MockUserWorker
|
17
|
+
end
|
18
|
+
|
19
|
+
let!(:user_1) { create(:user, :active) }
|
20
|
+
let!(:user_2) { create(:user, :active) }
|
21
|
+
let!(:user_3) { create(:user, :active) }
|
22
|
+
let!(:user_4) { create(:user, :banned) }
|
23
|
+
|
24
|
+
let(:models_query) { User.active }
|
25
|
+
|
26
|
+
describe 'perform_query_async' do
|
27
|
+
|
28
|
+
def run_worker(options = {})
|
29
|
+
UserManagerWorker.perform_query_async(models_query, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def mock_options(options)
|
33
|
+
UserManagerWorker.send(:sidekiq_manager_options, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def batch_args(*ids)
|
37
|
+
{'class' => worker_class, 'args' => ids.map{ |id| [id] }}
|
38
|
+
end
|
39
|
+
|
40
|
+
let(:model_ids) { [[user_1.id], [user_2.id], [user_3.id]] }
|
41
|
+
|
42
|
+
context 'when the worker_class is specified' do
|
43
|
+
|
44
|
+
class MockCustomWorker; end
|
45
|
+
|
46
|
+
let(:custom_worker_class) { MockCustomWorker }
|
47
|
+
|
48
|
+
def batch_args(*ids)
|
49
|
+
{'class' => custom_worker_class, 'args' => ids.map{ |id| [id] }}
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'as method arguments' do
|
53
|
+
|
54
|
+
it 'pushes a bulk of all user ids for the specified worker_class' do
|
55
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_1.id, user_2.id, user_3.id) )
|
56
|
+
run_worker({:worker_class => custom_worker_class})
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'as sidekiq_delegate_task_to' do
|
61
|
+
|
62
|
+
around do |example|
|
63
|
+
UserManagerWorker.send(:sidekiq_delegate_task_to, custom_worker_class)
|
64
|
+
example.run
|
65
|
+
UserManagerWorker.send(:sidekiq_delegate_task_to, worker_class)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'pushes a bulk of all user ids for the specified worker_class' do
|
69
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_1.id, user_2.id, user_3.id) )
|
70
|
+
run_worker
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'when the batch size is specified' do
|
77
|
+
|
78
|
+
let(:batch_size) { 2 }
|
79
|
+
|
80
|
+
context 'as method arguments' do
|
81
|
+
it 'pushes a bulk of user ids batches' do
|
82
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_1.id, user_2.id) )
|
83
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_3.id) )
|
84
|
+
run_worker({batch_size: batch_size})
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'as sidekiq_manager_options' do
|
89
|
+
|
90
|
+
around do |example|
|
91
|
+
mock_options(:batch_size => batch_size)
|
92
|
+
example.run
|
93
|
+
mock_options(:batch_size => Sidekiq::ManagerWorker::DEFAULT_BATCH_SIZE)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'pushes a bulk of user ids batches' do
|
97
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_1.id, user_2.id) )
|
98
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_3.id) )
|
99
|
+
run_worker
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'when the additional_keys are specified' do
|
105
|
+
|
106
|
+
let(:additional_keys) { [:email, :status] }
|
107
|
+
|
108
|
+
def batch_args(*users)
|
109
|
+
{'class' => worker_class, 'args' => users.map{ |user| [user.id, user.email, user.status] }}
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'as method arguments' do
|
113
|
+
it 'pushes a bulk of all user ids and additional_keys' do
|
114
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_1, user_2, user_3) )
|
115
|
+
run_worker({additional_keys: additional_keys})
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'as sidekiq_manager_options' do
|
120
|
+
around do |example|
|
121
|
+
mock_options(:additional_keys => additional_keys)
|
122
|
+
example.run
|
123
|
+
mock_options(:additional_keys => [])
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'pushes a bulk of all user ids and additional_keys' do
|
127
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_1, user_2, user_3) )
|
128
|
+
run_worker
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'when the identifier_key is specified' do
|
135
|
+
|
136
|
+
def batch_args(*users)
|
137
|
+
{'class' => worker_class, 'args' => users.map{ |user| [user.email] }}
|
138
|
+
end
|
139
|
+
|
140
|
+
let(:identifier_key) { :email }
|
141
|
+
|
142
|
+
context 'as method arguments' do
|
143
|
+
it 'pushes a bulk of all user emails as the identifier_key' do
|
144
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_1, user_2, user_3) )
|
145
|
+
run_worker({identifier_key: identifier_key})
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'as sidekiq_manager_options' do
|
150
|
+
|
151
|
+
around do |example|
|
152
|
+
mock_options(:identifier_key => identifier_key)
|
153
|
+
example.run
|
154
|
+
mock_options(:identifier_key => Sidekiq::ManagerWorker::DEFAULT_IDENTIFIER_KEY)
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'pushes a bulk of all user emails as the identifier_key' do
|
158
|
+
expect(sidekiq_client).to receive(:push_bulk).with( batch_args(user_1, user_2, user_3) )
|
159
|
+
run_worker
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sidekiq::TaskWorker do
|
4
|
+
|
5
|
+
class UserTaskWorker
|
6
|
+
include Sidekiq::TaskWorker
|
7
|
+
end
|
8
|
+
|
9
|
+
let!(:user) { create(:user, :active) }
|
10
|
+
|
11
|
+
subject(:task_worker) {UserTaskWorker }
|
12
|
+
|
13
|
+
def run_worker
|
14
|
+
task_worker.perform(user.id)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'sidekiq_task_model' do
|
18
|
+
|
19
|
+
context 'when the identifier_key is specified' do
|
20
|
+
before do
|
21
|
+
class UserTaskWorker
|
22
|
+
sidekiq_task_model User
|
23
|
+
sidekiq_task_options :identifier_key => :email
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
after do
|
28
|
+
class UserTaskWorker
|
29
|
+
sidekiq_task_model nil
|
30
|
+
sidekiq_task_options :identifier_key => :id
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'sets the identifier_key' do
|
35
|
+
identifier = task_worker.send(:identifier_key)
|
36
|
+
expect(identifier).to eq :email
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'calls the perform_on_model with the model' do
|
40
|
+
expect(task_worker).to receive(:perform_on_model).with(user)
|
41
|
+
task_worker.perform(user.email)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when a Class is specified' do
|
46
|
+
|
47
|
+
before do
|
48
|
+
class UserTaskWorker
|
49
|
+
sidekiq_task_model User
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'sets the model' do
|
54
|
+
klass = task_worker.send(:model_class)
|
55
|
+
expect(klass).to eq User
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'when a class name is specified' do
|
61
|
+
before do
|
62
|
+
class UserTaskWorker
|
63
|
+
sidekiq_task_model :user
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'sets the model' do
|
68
|
+
klass = task_worker.send(:model_class)
|
69
|
+
expect(klass).to eq User
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'when the model is not found' do
|
73
|
+
|
74
|
+
let(:trash_id) { user.id + 10 }
|
75
|
+
|
76
|
+
def run_worker
|
77
|
+
task_worker.perform(trash_id)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'calls the not_found_model hook' do
|
81
|
+
expect(task_worker).to receive(:not_found_model).with(trash_id)
|
82
|
+
run_worker
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'skips the perform_on_model' do
|
86
|
+
expect(task_worker).to_not receive(:perform_on_model)
|
87
|
+
run_worker
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'when the mode is found' do
|
92
|
+
|
93
|
+
context 'when the model validation is specified' do
|
94
|
+
it 'calls the model_valid? hook' do
|
95
|
+
expect(task_worker).to receive(:model_valid?).with(user)
|
96
|
+
run_worker
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when the model is valid' do
|
101
|
+
|
102
|
+
before do
|
103
|
+
allow(task_worker).to receive(:model_valid?).and_return(true)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'calls the perform_on_model with the model' do
|
107
|
+
expect(task_worker).to receive(:perform_on_model).with(user)
|
108
|
+
run_worker
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'perform_on_model' do
|
112
|
+
context 'when passing only the model identifier' do
|
113
|
+
it 'calls the perform_on_model with the model' do
|
114
|
+
expect(task_worker).to receive(:perform_on_model).with(user)
|
115
|
+
run_worker
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'when passing additional arguments' do
|
120
|
+
it 'calls the perform_on_model with the model' do
|
121
|
+
expect(task_worker).to receive(:perform_on_model).with(user, user.email)
|
122
|
+
task_worker.perform(user.id, user.email)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'when the model is invalid' do
|
130
|
+
|
131
|
+
before do
|
132
|
+
allow(task_worker).to receive(:model_valid?).and_return(false)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'calls the invalid_model hook' do
|
136
|
+
expect(task_worker).to receive(:invalid_model).with(user)
|
137
|
+
run_worker
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'skips the perform_on_model' do
|
141
|
+
expect(task_worker).to_not receive(:perform_on_model)
|
142
|
+
run_worker
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "sidekiq"
|
2
|
+
require "sidekiq/activerecord"
|
3
|
+
require 'factory_girl'
|
4
|
+
require 'database_cleaner'
|
5
|
+
require 'support'
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.alias_example_to :expect_it
|
9
|
+
|
10
|
+
# config.full_backtrace = true
|
11
|
+
|
12
|
+
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
13
|
+
# examples within a transaction, remove the following line or assign false
|
14
|
+
# instead of true.
|
15
|
+
# config.use_transactional_fixtures = true
|
16
|
+
|
17
|
+
config.expect_with :rspec do |config|
|
18
|
+
config.syntax = :expect
|
19
|
+
end
|
20
|
+
|
21
|
+
config.include FactoryGirl::Syntax::Methods # Don't need to write FactoryGirl.create => create
|
22
|
+
|
23
|
+
config.before(:suite) do
|
24
|
+
DatabaseCleaner.strategy = :transaction
|
25
|
+
DatabaseCleaner.clean_with(:truncation)
|
26
|
+
end
|
27
|
+
|
28
|
+
config.before(:each) do
|
29
|
+
DatabaseCleaner.start
|
30
|
+
end
|
31
|
+
|
32
|
+
config.after(:each) do
|
33
|
+
DatabaseCleaner.clean
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
data/spec/support.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "active_record"
|
2
|
+
|
3
|
+
def set_database
|
4
|
+
db_config = {:adapter => "sqlite3", :database => ":memory:"}
|
5
|
+
ActiveRecord::Base.establish_connection(db_config)
|
6
|
+
connection = ActiveRecord::Base.connection
|
7
|
+
|
8
|
+
connection.create_table :users, force: true do |t|
|
9
|
+
t.string :name
|
10
|
+
t.string :email
|
11
|
+
t.string :status
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
set_database
|
17
|
+
|
18
|
+
class User < ActiveRecord::Base
|
19
|
+
scope :active, -> { where(:status => :active) }
|
20
|
+
scope :banned, -> { where(:status => :banned) }
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
FactoryGirl.define do
|
25
|
+
factory :user do
|
26
|
+
|
27
|
+
sequence(:name) { |n| "name-#{n}" }
|
28
|
+
sequence(:email) { |n| "email-#{n}" }
|
29
|
+
|
30
|
+
trait :active do
|
31
|
+
status 'active'
|
32
|
+
end
|
33
|
+
|
34
|
+
trait :banned do
|
35
|
+
status 'banned'
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sidekiq-activerecord
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Adam Farhi
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-05-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sidekiq
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.16.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.16.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: activerecord
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 4.1.1
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 4.1.1
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.14'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.14'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: database_cleaner
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.2.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.2.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: sqlite3
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 1.3.9
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.3.9
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: factory_girl
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '4.0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '4.0'
|
110
|
+
description: Encapsulates various interactions between Sidekiq and ActiveRecord
|
111
|
+
email:
|
112
|
+
- yelled3@gmail.com
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- .gitignore
|
118
|
+
- .rspec
|
119
|
+
- Gemfile
|
120
|
+
- Gemfile.lock
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- lib/sidekiq/activerecord.rb
|
125
|
+
- lib/sidekiq/activerecord/version.rb
|
126
|
+
- lib/sidekiq/manager_worker.rb
|
127
|
+
- lib/sidekiq/task_worker.rb
|
128
|
+
- sidekiq-activerecord.gemspec
|
129
|
+
- spec/lib/sidekiq/manager_worker.rb
|
130
|
+
- spec/lib/sidekiq/task_worker.rb
|
131
|
+
- spec/spec_helper.rb
|
132
|
+
- spec/support.rb
|
133
|
+
homepage: https://github.com/yelled3/sidekiq-activerecord
|
134
|
+
licenses:
|
135
|
+
- MIT
|
136
|
+
post_install_message:
|
137
|
+
rdoc_options: []
|
138
|
+
require_paths:
|
139
|
+
- lib
|
140
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
141
|
+
none: false
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
|
+
none: false
|
148
|
+
requirements:
|
149
|
+
- - ! '>='
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 1.8.24
|
155
|
+
signing_key:
|
156
|
+
specification_version: 3
|
157
|
+
summary: Encapsulates various interactions between Sidekiq and ActiveRecord
|
158
|
+
test_files:
|
159
|
+
- spec/lib/sidekiq/manager_worker.rb
|
160
|
+
- spec/lib/sidekiq/task_worker.rb
|
161
|
+
- spec/spec_helper.rb
|
162
|
+
- spec/support.rb
|
163
|
+
has_rdoc:
|