sidekiq-activerecord 0.0.1 → 0.0.2
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 +2 -1
- data/.rspec +3 -1
- data/.travis.yml +12 -0
- data/Gemfile +0 -2
- data/Gemfile.lock +20 -16
- data/README.md +3 -3
- data/Rakefile +12 -1
- data/lib/sidekiq/active_record/manager_worker.rb +141 -0
- data/lib/sidekiq/active_record/task_worker.rb +114 -0
- data/lib/sidekiq/active_record/version.rb +5 -0
- data/lib/sidekiq/activerecord.rb +12 -6
- data/pkg/sidekiq-activerecord-0.0.1.gem +0 -0
- data/pkg/sidekiq-activerecord-0.0.2.gem +0 -0
- data/sidekiq-activerecord.gemspec +16 -16
- data/spec/{lib/sidekiq/manager_worker.rb → sidekiq/active_record/manager_worker_spec.rb} +8 -10
- data/spec/{lib/sidekiq/task_worker.rb → sidekiq/active_record/task_worker_spec.rb} +2 -4
- data/spec/spec_helper.rb +4 -29
- data/spec/support/database.rb +10 -0
- data/spec/support/database_cleaner.rb +14 -0
- data/spec/support/factory_girl.rb +20 -0
- data/spec/support/models.rb +4 -0
- metadata +35 -26
- data/lib/sidekiq/activerecord/version.rb +0 -5
- data/lib/sidekiq/manager_worker.rb +0 -144
- data/lib/sidekiq/task_worker.rb +0 -117
- data/spec/support.rb +0 -39
data/.gitignore
CHANGED
@@ -1 +1,2 @@
|
|
1
|
-
*.idea
|
1
|
+
*.idea
|
2
|
+
*.lock
|
data/.rspec
CHANGED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
sidekiq-activerecord (0.0.
|
5
|
-
activerecord (
|
6
|
-
sidekiq (>= 2.16
|
4
|
+
sidekiq-activerecord (0.0.2)
|
5
|
+
activerecord (>= 4.0)
|
6
|
+
sidekiq (>= 2.16)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
@@ -28,7 +28,7 @@ GEM
|
|
28
28
|
connection_pool (2.0.0)
|
29
29
|
database_cleaner (1.2.0)
|
30
30
|
diff-lcs (1.2.5)
|
31
|
-
factory_girl (4.
|
31
|
+
factory_girl (4.4.0)
|
32
32
|
activesupport (>= 3.0.0)
|
33
33
|
i18n (0.6.9)
|
34
34
|
json (1.8.1)
|
@@ -36,15 +36,19 @@ GEM
|
|
36
36
|
redis (3.0.7)
|
37
37
|
redis-namespace (1.4.1)
|
38
38
|
redis (~> 3.0.4)
|
39
|
-
rspec (
|
40
|
-
rspec-core (
|
41
|
-
rspec-expectations (
|
42
|
-
rspec-mocks (
|
43
|
-
rspec-core (
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
39
|
+
rspec (3.0.0.rc1)
|
40
|
+
rspec-core (= 3.0.0.rc1)
|
41
|
+
rspec-expectations (= 3.0.0.rc1)
|
42
|
+
rspec-mocks (= 3.0.0.rc1)
|
43
|
+
rspec-core (3.0.0.rc1)
|
44
|
+
rspec-support (= 3.0.0.rc1)
|
45
|
+
rspec-expectations (3.0.0.rc1)
|
46
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
47
|
+
rspec-support (= 3.0.0.rc1)
|
48
|
+
rspec-mocks (3.0.0.rc1)
|
49
|
+
rspec-support (= 3.0.0.rc1)
|
50
|
+
rspec-support (3.0.0.rc1)
|
51
|
+
sidekiq (3.1.0)
|
48
52
|
celluloid (>= 0.15.2)
|
49
53
|
connection_pool (>= 2.0.0)
|
50
54
|
json
|
@@ -60,8 +64,8 @@ PLATFORMS
|
|
60
64
|
ruby
|
61
65
|
|
62
66
|
DEPENDENCIES
|
63
|
-
database_cleaner (
|
67
|
+
database_cleaner (>= 1.2.0)
|
64
68
|
factory_girl (~> 4.0)
|
65
|
-
rspec (
|
69
|
+
rspec (= 3.0.0.rc1)
|
66
70
|
sidekiq-activerecord!
|
67
|
-
sqlite3 (
|
71
|
+
sqlite3 (>= 1.3.9)
|
data/README.md
CHANGED
@@ -44,7 +44,7 @@ class UserTaskWorker
|
|
44
44
|
end
|
45
45
|
|
46
46
|
class UserSyncer
|
47
|
-
include Sidekiq::ManagerWorker
|
47
|
+
include Sidekiq::ActiveRecord::ManagerWorker
|
48
48
|
|
49
49
|
sidekiq_delegate_task_to :user_task_worker # or UserTaskWorker
|
50
50
|
sidekiq_manager_options :batch_size => 500,
|
@@ -78,7 +78,7 @@ end
|
|
78
78
|
|
79
79
|
```ruby
|
80
80
|
class UserMailerTaskWorker
|
81
|
-
include Sidekiq::TaskWorker
|
81
|
+
include Sidekiq::ActiveRecord::TaskWorker
|
82
82
|
|
83
83
|
sidekiq_task_model :user_model # or UserModel
|
84
84
|
sidekiq_task_options :identifier_key => :token
|
@@ -110,7 +110,7 @@ UserMailerTaskWorker.perform(user.id, :new_email)
|
|
110
110
|
|
111
111
|
## Contributing
|
112
112
|
|
113
|
-
1. Fork it ( http://github.com
|
113
|
+
1. Fork it ( http://github.com/yelled3/sidekiq-activerecord/fork )
|
114
114
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
115
115
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
116
116
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/Rakefile
CHANGED
@@ -1 +1,12 @@
|
|
1
|
-
require
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc 'Default: run specs'
|
6
|
+
task default: :spec
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new do |t|
|
9
|
+
t.pattern = 'spec/**/*_spec.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
Bundler::GemHelper.install_tasks
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module ActiveRecord
|
3
|
+
module ManagerWorker
|
4
|
+
extend Sidekiq::Worker
|
5
|
+
|
6
|
+
DEFAULT_IDENTIFIER_KEY = :id
|
7
|
+
DEFAULT_BATCH_SIZE = 1000
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
base.class_attribute :sidekiq_manager_options_hash
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
# For a given model collection, it delegates each model to a sub-worker (e.g TaskWorker)
|
16
|
+
# Specify the TaskWorker 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::ActiveRecord::TaskWorker
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# class UserSyncer
|
31
|
+
# include Sidekiq::ActiveRecord::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.perform(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
|
+
end
|
53
|
+
|
54
|
+
# @required
|
55
|
+
# The task worker to delegate to.
|
56
|
+
# @param worker_klass (Sidekiq::Worker, Symbol) - UserTaskWorker or :user_task_worker
|
57
|
+
def sidekiq_delegate_task_to(worker_klass)
|
58
|
+
case worker_klass
|
59
|
+
when String, Symbol
|
60
|
+
worker_klass.to_s.split('_').map(&:capitalize).join.constantize
|
61
|
+
else
|
62
|
+
worker_klass
|
63
|
+
end
|
64
|
+
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 || {}))
|
76
|
+
end
|
77
|
+
|
78
|
+
# private
|
79
|
+
|
80
|
+
def default_worker_manager_options
|
81
|
+
{
|
82
|
+
identifier_key: DEFAULT_IDENTIFIER_KEY,
|
83
|
+
additional_keys: [],
|
84
|
+
batch_size: DEFAULT_BATCH_SIZE
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
# returns the model attributes array:
|
89
|
+
# [model_id, attr1, attr2, ...]
|
90
|
+
def model_attributes(model)
|
91
|
+
additional_attributes = additional_keys.map { |key| model.send(key) }
|
92
|
+
id_attribute = model.send(identifier_key)
|
93
|
+
additional_attributes.unshift(id_attribute)
|
94
|
+
end
|
95
|
+
|
96
|
+
def selected_attributes
|
97
|
+
attrs = [identifier_key, additional_keys]
|
98
|
+
attrs << DEFAULT_IDENTIFIER_KEY unless default_identifier? # :id must be included
|
99
|
+
attrs
|
100
|
+
end
|
101
|
+
|
102
|
+
def worker_class
|
103
|
+
fail NotImplementedError.new('`worker_class` was not specified') unless manager_options[:worker_class].present?
|
104
|
+
manager_options[:worker_class]
|
105
|
+
end
|
106
|
+
|
107
|
+
def default_identifier?
|
108
|
+
identifier_key == DEFAULT_IDENTIFIER_KEY
|
109
|
+
end
|
110
|
+
|
111
|
+
def identifier_key
|
112
|
+
manager_options[:identifier_key]
|
113
|
+
end
|
114
|
+
|
115
|
+
def additional_keys
|
116
|
+
manager_options[:additional_keys]
|
117
|
+
end
|
118
|
+
|
119
|
+
def batch_size
|
120
|
+
manager_options[:batch_size]
|
121
|
+
end
|
122
|
+
|
123
|
+
def manager_options
|
124
|
+
get_sidekiq_manager_options.merge(runtime_options)
|
125
|
+
end
|
126
|
+
|
127
|
+
def get_sidekiq_manager_options
|
128
|
+
self.sidekiq_manager_options_hash ||= default_worker_manager_options
|
129
|
+
end
|
130
|
+
|
131
|
+
def runtime_options
|
132
|
+
@sidekiq_manager_runtime_options || {}
|
133
|
+
end
|
134
|
+
|
135
|
+
def set_runtime_options(options={})
|
136
|
+
@sidekiq_manager_runtime_options = options.delete_if { |_, v| v.to_s.strip == '' }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module ActiveRecord
|
3
|
+
module TaskWorker
|
4
|
+
extend Sidekiq::Worker
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
base.class_attribute :sidekiq_task_options_hash
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
# @example:
|
13
|
+
# class UserMailerTaskWorker
|
14
|
+
# include Sidekiq::ActiveRecord::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) || model_klass.is_a?(Symbol)
|
53
|
+
model_klass = model_klass.to_s.split('_').map(&:capitalize).join.constantize
|
54
|
+
else
|
55
|
+
model_klass
|
56
|
+
end
|
57
|
+
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
|
+
# private
|
78
|
+
|
79
|
+
def fetch_model(identifier)
|
80
|
+
model_class.find_by(identifier_key => identifier)
|
81
|
+
end
|
82
|
+
|
83
|
+
def model_class
|
84
|
+
klass = get_sidekiq_task_options[:model_class]
|
85
|
+
fail NotImplementedError.new('`model_class` was not specified') unless klass.present?
|
86
|
+
klass
|
87
|
+
end
|
88
|
+
|
89
|
+
def identifier_key
|
90
|
+
get_sidekiq_task_options[:identifier_key]
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Allows customization for this type of TaskWorker.
|
95
|
+
# Legal options:
|
96
|
+
#
|
97
|
+
# :identifier_key - the model identifier column. Default 'id'
|
98
|
+
def sidekiq_task_options(opts = {})
|
99
|
+
self.sidekiq_task_options_hash = get_sidekiq_task_options.merge((opts || {}).symbolize_keys!)
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_sidekiq_task_options
|
103
|
+
self.sidekiq_task_options_hash ||= default_worker_task_options
|
104
|
+
end
|
105
|
+
|
106
|
+
def default_worker_task_options
|
107
|
+
{
|
108
|
+
identifier_key: :id
|
109
|
+
}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/sidekiq/activerecord.rb
CHANGED
@@ -1,10 +1,16 @@
|
|
1
|
-
|
2
|
-
# gems
|
1
|
+
# dependencies
|
3
2
|
require 'sidekiq'
|
4
3
|
require 'active_record'
|
5
4
|
|
6
|
-
|
5
|
+
# core
|
6
|
+
require 'sidekiq/active_record/version'
|
7
|
+
|
8
|
+
|
9
|
+
module Sidekiq
|
10
|
+
module ActiveRecord
|
11
|
+
extend ActiveSupport::Autoload
|
7
12
|
|
8
|
-
|
9
|
-
|
10
|
-
|
13
|
+
autoload :TaskWorker
|
14
|
+
autoload :ManagerWorker
|
15
|
+
end
|
16
|
+
end
|
Binary file
|
Binary file
|
@@ -1,28 +1,28 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'sidekiq/
|
4
|
+
require 'sidekiq/active_record/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
8
|
-
spec.version = Sidekiq::
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
7
|
+
spec.name = 'sidekiq-activerecord'
|
8
|
+
spec.version = Sidekiq::ActiveRecord::VERSION
|
9
|
+
spec.authors = ['Adam Farhi']
|
10
|
+
spec.email = ['afarhi@ebay.com']
|
11
11
|
spec.summary = 'Encapsulates various interactions between Sidekiq and ActiveRecord'
|
12
|
-
spec.description =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = 'https://github.com/yelled3/sidekiq-activerecord'
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
-
spec.test_files = spec.files.grep(%r{^
|
19
|
-
spec.require_paths = [
|
18
|
+
spec.test_files = spec.files.grep(%r{^spec/})
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.add_dependency 'sidekiq', '>= 2.16
|
22
|
-
spec.add_dependency 'activerecord', '
|
21
|
+
spec.add_dependency 'sidekiq', '>= 2.16'
|
22
|
+
spec.add_dependency 'activerecord', '>= 4.0'
|
23
23
|
|
24
|
-
spec.add_development_dependency
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency 'sqlite3', '
|
27
|
-
spec.add_development_dependency 'factory_girl',
|
24
|
+
spec.add_development_dependency 'rspec', '3.0.0.rc1'
|
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
28
|
end
|
@@ -1,6 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
describe Sidekiq::ManagerWorker do
|
1
|
+
describe Sidekiq::ActiveRecord::ManagerWorker do
|
4
2
|
|
5
3
|
before do
|
6
4
|
allow(Sidekiq::Client).to receive(:push_bulk)
|
@@ -12,7 +10,7 @@ describe Sidekiq::ManagerWorker do
|
|
12
10
|
let(:sidekiq_client) { Sidekiq::Client }
|
13
11
|
|
14
12
|
class UserManagerWorker
|
15
|
-
include Sidekiq::ManagerWorker
|
13
|
+
include Sidekiq::ActiveRecord::ManagerWorker
|
16
14
|
sidekiq_delegate_task_to MockUserWorker
|
17
15
|
end
|
18
16
|
|
@@ -34,7 +32,7 @@ describe Sidekiq::ManagerWorker do
|
|
34
32
|
end
|
35
33
|
|
36
34
|
def batch_args(*ids)
|
37
|
-
{
|
35
|
+
{class: worker_class, args: ids.map{ |id| [id] }}
|
38
36
|
end
|
39
37
|
|
40
38
|
let(:model_ids) { [[user_1.id], [user_2.id], [user_3.id]] }
|
@@ -46,7 +44,7 @@ describe Sidekiq::ManagerWorker do
|
|
46
44
|
let(:custom_worker_class) { MockCustomWorker }
|
47
45
|
|
48
46
|
def batch_args(*ids)
|
49
|
-
{
|
47
|
+
{class: custom_worker_class, args: ids.map{ |id| [id] }}
|
50
48
|
end
|
51
49
|
|
52
50
|
context 'as method arguments' do
|
@@ -90,7 +88,7 @@ describe Sidekiq::ManagerWorker do
|
|
90
88
|
around do |example|
|
91
89
|
mock_options(:batch_size => batch_size)
|
92
90
|
example.run
|
93
|
-
mock_options(:batch_size => Sidekiq::ManagerWorker::DEFAULT_BATCH_SIZE)
|
91
|
+
mock_options(:batch_size => Sidekiq::ActiveRecord::ManagerWorker::DEFAULT_BATCH_SIZE)
|
94
92
|
end
|
95
93
|
|
96
94
|
it 'pushes a bulk of user ids batches' do
|
@@ -106,7 +104,7 @@ describe Sidekiq::ManagerWorker do
|
|
106
104
|
let(:additional_keys) { [:email, :status] }
|
107
105
|
|
108
106
|
def batch_args(*users)
|
109
|
-
{
|
107
|
+
{class: worker_class, args: users.map{ |user| [user.id, user.email, user.status] }}
|
110
108
|
end
|
111
109
|
|
112
110
|
context 'as method arguments' do
|
@@ -134,7 +132,7 @@ describe Sidekiq::ManagerWorker do
|
|
134
132
|
context 'when the identifier_key is specified' do
|
135
133
|
|
136
134
|
def batch_args(*users)
|
137
|
-
{
|
135
|
+
{class: worker_class, args: users.map{ |user| [user.email] }}
|
138
136
|
end
|
139
137
|
|
140
138
|
let(:identifier_key) { :email }
|
@@ -151,7 +149,7 @@ describe Sidekiq::ManagerWorker do
|
|
151
149
|
around do |example|
|
152
150
|
mock_options(:identifier_key => identifier_key)
|
153
151
|
example.run
|
154
|
-
mock_options(:identifier_key => Sidekiq::ManagerWorker::DEFAULT_IDENTIFIER_KEY)
|
152
|
+
mock_options(:identifier_key => Sidekiq::ActiveRecord::ManagerWorker::DEFAULT_IDENTIFIER_KEY)
|
155
153
|
end
|
156
154
|
|
157
155
|
it 'pushes a bulk of all user emails as the identifier_key' do
|
data/spec/spec_helper.rb
CHANGED
@@ -1,36 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'sidekiq'
|
2
|
+
require 'sidekiq/activerecord'
|
3
3
|
require 'factory_girl'
|
4
4
|
require 'database_cleaner'
|
5
|
-
require 'support'
|
6
5
|
|
7
6
|
RSpec.configure do |config|
|
8
7
|
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
8
|
end
|
36
9
|
|
10
|
+
Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
|
11
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
db_config = {:adapter => 'sqlite3', :database => ':memory:'}
|
2
|
+
ActiveRecord::Base.establish_connection(db_config)
|
3
|
+
connection = ActiveRecord::Base.connection
|
4
|
+
|
5
|
+
connection.create_table :users, force: true do |t|
|
6
|
+
t.string :name
|
7
|
+
t.string :email
|
8
|
+
t.string :status
|
9
|
+
t.timestamps
|
10
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
RSpec.configure do |config|
|
2
|
+
config.before(:suite) do
|
3
|
+
DatabaseCleaner.strategy = :transaction
|
4
|
+
DatabaseCleaner.clean_with(:truncation)
|
5
|
+
end
|
6
|
+
|
7
|
+
config.before(:each) do
|
8
|
+
DatabaseCleaner.start
|
9
|
+
end
|
10
|
+
|
11
|
+
config.after(:each) do
|
12
|
+
DatabaseCleaner.clean
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
RSpec.configure do |config|
|
2
|
+
config.include FactoryGirl::Syntax::Methods # Don't need to write FactoryGirl.create => create
|
3
|
+
end
|
4
|
+
|
5
|
+
FactoryGirl.define do
|
6
|
+
factory :user do
|
7
|
+
|
8
|
+
sequence(:name) { |n| "name-#{n}" }
|
9
|
+
sequence(:email) { |n| "email-#{n}" }
|
10
|
+
|
11
|
+
trait :active do
|
12
|
+
status 'active'
|
13
|
+
end
|
14
|
+
|
15
|
+
trait :banned do
|
16
|
+
status 'banned'
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidekiq-activerecord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-05-
|
12
|
+
date: 2014-05-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sidekiq
|
@@ -18,7 +18,7 @@ dependencies:
|
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 2.16
|
21
|
+
version: '2.16'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -26,45 +26,45 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ! '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 2.16
|
29
|
+
version: '2.16'
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
31
|
name: activerecord
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
33
33
|
none: false
|
34
34
|
requirements:
|
35
|
-
- -
|
35
|
+
- - ! '>='
|
36
36
|
- !ruby/object:Gem::Version
|
37
|
-
version: 4.
|
37
|
+
version: '4.0'
|
38
38
|
type: :runtime
|
39
39
|
prerelease: false
|
40
40
|
version_requirements: !ruby/object:Gem::Requirement
|
41
41
|
none: false
|
42
42
|
requirements:
|
43
|
-
- -
|
43
|
+
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
|
-
version: 4.
|
45
|
+
version: '4.0'
|
46
46
|
- !ruby/object:Gem::Dependency
|
47
47
|
name: rspec
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
49
49
|
none: false
|
50
50
|
requirements:
|
51
|
-
- -
|
51
|
+
- - '='
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
53
|
+
version: 3.0.0.rc1
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
none: false
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - '='
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 3.0.0.rc1
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
63
|
name: database_cleaner
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
65
65
|
none: false
|
66
66
|
requirements:
|
67
|
-
- -
|
67
|
+
- - ! '>='
|
68
68
|
- !ruby/object:Gem::Version
|
69
69
|
version: 1.2.0
|
70
70
|
type: :development
|
@@ -72,7 +72,7 @@ dependencies:
|
|
72
72
|
version_requirements: !ruby/object:Gem::Requirement
|
73
73
|
none: false
|
74
74
|
requirements:
|
75
|
-
- -
|
75
|
+
- - ! '>='
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: 1.2.0
|
78
78
|
- !ruby/object:Gem::Dependency
|
@@ -80,7 +80,7 @@ dependencies:
|
|
80
80
|
requirement: !ruby/object:Gem::Requirement
|
81
81
|
none: false
|
82
82
|
requirements:
|
83
|
-
- -
|
83
|
+
- - ! '>='
|
84
84
|
- !ruby/object:Gem::Version
|
85
85
|
version: 1.3.9
|
86
86
|
type: :development
|
@@ -88,7 +88,7 @@ dependencies:
|
|
88
88
|
version_requirements: !ruby/object:Gem::Requirement
|
89
89
|
none: false
|
90
90
|
requirements:
|
91
|
-
- -
|
91
|
+
- - ! '>='
|
92
92
|
- !ruby/object:Gem::Version
|
93
93
|
version: 1.3.9
|
94
94
|
- !ruby/object:Gem::Dependency
|
@@ -109,27 +109,33 @@ dependencies:
|
|
109
109
|
version: '4.0'
|
110
110
|
description: Encapsulates various interactions between Sidekiq and ActiveRecord
|
111
111
|
email:
|
112
|
-
-
|
112
|
+
- afarhi@ebay.com
|
113
113
|
executables: []
|
114
114
|
extensions: []
|
115
115
|
extra_rdoc_files: []
|
116
116
|
files:
|
117
117
|
- .gitignore
|
118
118
|
- .rspec
|
119
|
+
- .travis.yml
|
119
120
|
- Gemfile
|
120
121
|
- Gemfile.lock
|
121
122
|
- LICENSE.txt
|
122
123
|
- README.md
|
123
124
|
- Rakefile
|
125
|
+
- lib/sidekiq/active_record/manager_worker.rb
|
126
|
+
- lib/sidekiq/active_record/task_worker.rb
|
127
|
+
- lib/sidekiq/active_record/version.rb
|
124
128
|
- lib/sidekiq/activerecord.rb
|
125
|
-
-
|
126
|
-
-
|
127
|
-
- lib/sidekiq/task_worker.rb
|
129
|
+
- pkg/sidekiq-activerecord-0.0.1.gem
|
130
|
+
- pkg/sidekiq-activerecord-0.0.2.gem
|
128
131
|
- sidekiq-activerecord.gemspec
|
129
|
-
- spec/
|
130
|
-
- spec/
|
132
|
+
- spec/sidekiq/active_record/manager_worker_spec.rb
|
133
|
+
- spec/sidekiq/active_record/task_worker_spec.rb
|
131
134
|
- spec/spec_helper.rb
|
132
|
-
- spec/support.rb
|
135
|
+
- spec/support/database.rb
|
136
|
+
- spec/support/database_cleaner.rb
|
137
|
+
- spec/support/factory_girl.rb
|
138
|
+
- spec/support/models.rb
|
133
139
|
homepage: https://github.com/yelled3/sidekiq-activerecord
|
134
140
|
licenses:
|
135
141
|
- MIT
|
@@ -156,8 +162,11 @@ signing_key:
|
|
156
162
|
specification_version: 3
|
157
163
|
summary: Encapsulates various interactions between Sidekiq and ActiveRecord
|
158
164
|
test_files:
|
159
|
-
- spec/
|
160
|
-
- spec/
|
165
|
+
- spec/sidekiq/active_record/manager_worker_spec.rb
|
166
|
+
- spec/sidekiq/active_record/task_worker_spec.rb
|
161
167
|
- spec/spec_helper.rb
|
162
|
-
- spec/support.rb
|
168
|
+
- spec/support/database.rb
|
169
|
+
- spec/support/database_cleaner.rb
|
170
|
+
- spec/support/factory_girl.rb
|
171
|
+
- spec/support/models.rb
|
163
172
|
has_rdoc:
|
@@ -1,144 +0,0 @@
|
|
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
|
data/lib/sidekiq/task_worker.rb
DELETED
@@ -1,117 +0,0 @@
|
|
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
|
data/spec/support.rb
DELETED
@@ -1,39 +0,0 @@
|
|
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
|