sidekiq-activerecord 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|