sidekiq_strategies 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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +4 -0
- data/lib/airbrake/airbrake.rb +7 -0
- data/lib/dependencies.rb +29 -0
- data/lib/models/account.rb +50 -0
- data/lib/models/provider.rb +11 -0
- data/lib/models/transaction.rb +22 -0
- data/lib/rails/rails.rb +19 -0
- data/lib/sidekiq/sidekiq.rb +14 -0
- data/lib/sidekiq_strategies.rb +10 -0
- data/lib/sidekiq_strategies/account_handler.rb +151 -0
- data/lib/sidekiq_strategies/base.rb +19 -0
- data/lib/sidekiq_strategies/beanstalkd_handler.rb +32 -0
- data/lib/sidekiq_strategies/helper.rb +50 -0
- data/lib/sidekiq_strategies/polling_strategy.rb +252 -0
- data/lib/sidekiq_strategies/version.rb +3 -0
- data/lib/spool/publisher.rb +15 -0
- data/lib/spool/subscribers/events_agent_subscriber.rb +11 -0
- data/lib/spool/subscribers/redis_pubsub_subscriber.rb +11 -0
- data/lib/strategy_runner.rb +26 -0
- data/sidekiq_strategies.gemspec +27 -0
- data/spec/spec_helper.rb +66 -0
- data/spec/strategies/account_handler_spec.rb +182 -0
- data/spec/strategies/beanstalkd_handler_spec.rb +50 -0
- data/spec/strategies/helper_spec.rb +61 -0
- data/spec/strategies/polling_strategy_spec.rb +206 -0
- data/spec/support/seed_helpers.rb +83 -0
- data/tasks/rspec.rake +3 -0
- metadata +169 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module SidekiqStrategies
|
2
|
+
|
3
|
+
class BeanstalkdHandler
|
4
|
+
attr_accessor :connection
|
5
|
+
|
6
|
+
TIME_TO_RUN = 600
|
7
|
+
|
8
|
+
def initialize(connection=nil)
|
9
|
+
@connection = connection
|
10
|
+
end
|
11
|
+
|
12
|
+
def obtain_tube(name)
|
13
|
+
conn.tubes[name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_job(id)
|
17
|
+
conn.jobs.find_all(id).shift
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def conn
|
23
|
+
@connection ||= Beaneater::Pool.new(host)
|
24
|
+
end
|
25
|
+
|
26
|
+
def host
|
27
|
+
@host ||= 'localhost:11300'
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module SidekiqStrategies
|
2
|
+
|
3
|
+
class Helper
|
4
|
+
attr_accessor :acc_handler
|
5
|
+
|
6
|
+
def initialize(acc_handler, url_helper)
|
7
|
+
@acc_handler = acc_handler
|
8
|
+
self.class.send(:include, url_helper)
|
9
|
+
end
|
10
|
+
|
11
|
+
def reply_tube_name
|
12
|
+
"#{@acc_handler.account.country.code}-#{@acc_handler.account.id}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def request_tube_name
|
16
|
+
@acc_handler.transaction.provider.queue
|
17
|
+
end
|
18
|
+
|
19
|
+
def queue_message
|
20
|
+
@acc_handler.transaction.provider.queue_options.merge(
|
21
|
+
{
|
22
|
+
action: @acc_handler.action_name,
|
23
|
+
account: @acc_handler.account.id,
|
24
|
+
number: @acc_handler.account.number,
|
25
|
+
service: @acc_handler.account.service.slug,
|
26
|
+
fields: @acc_handler.transaction.fields,
|
27
|
+
retry_count: @acc_handler.transaction.retry_count,
|
28
|
+
request_tube: @acc_handler.transaction.provider.queue,
|
29
|
+
reply_tube: reply_tube_name,
|
30
|
+
account_url: account_url(@acc_handler.account)
|
31
|
+
}
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def try_again?(exception)
|
36
|
+
return false if @acc_handler.transaction.retry_count >= 2
|
37
|
+
return exception[:retry_action] if exception.kind_of?(Hash)
|
38
|
+
return exception.retry_action if exception.respond_to?(:retry_action)
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def account_url(account)
|
45
|
+
v1_account_url cc: account.country_code, slug: account.service.slug, id: account.id
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
module SidekiqStrategies
|
2
|
+
|
3
|
+
class PollingStrategy < Base
|
4
|
+
|
5
|
+
attr_accessor :poll_interval, :poll_max_tries, :poll_tries_before_airbrake
|
6
|
+
|
7
|
+
def initialize(acc_handler, bns_handler, logger, airbrake_notifier, helper)
|
8
|
+
@acc_handler = acc_handler
|
9
|
+
@bns_handler = bns_handler
|
10
|
+
@helper = helper
|
11
|
+
|
12
|
+
@poll_interval = 1 # seconds
|
13
|
+
@poll_max_tries = 600
|
14
|
+
@poll_tries_before_airbrake = 180
|
15
|
+
|
16
|
+
@logger = logger
|
17
|
+
@airbrake_notifier = airbrake_notifier
|
18
|
+
end
|
19
|
+
|
20
|
+
def retries_exhausted(msg)
|
21
|
+
@logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}"
|
22
|
+
@acc_handler.save_exception(XBP::Error::ApiInternalError.new( msg['error_class'] + ': ' + msg['error_message'] ))
|
23
|
+
@acc_handler.error
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform(sidekiq_jid)
|
27
|
+
|
28
|
+
@logger.info "processing transaction #{@acc_handler.transaction.id}"
|
29
|
+
|
30
|
+
begin
|
31
|
+
request_tube = @bns_handler.obtain_tube(@helper.request_tube_name)
|
32
|
+
reply_tube = @bns_handler.obtain_tube(@helper.reply_tube_name)
|
33
|
+
|
34
|
+
# There are previous data?
|
35
|
+
beanstalkd_jid = @acc_handler.transaction.beanstalkd_jid
|
36
|
+
if beanstalkd_jid != nil
|
37
|
+
reply_job = reserve_existing_reply_job(reply_tube)
|
38
|
+
request_job = @bns_handler.find_job(beanstalkd_jid)
|
39
|
+
|
40
|
+
if reply_job != nil
|
41
|
+
# do nothing: there was already a reply job for me
|
42
|
+
else
|
43
|
+
kick_existing_buried_job(beanstalkd_jid)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Save sidekiq job id
|
48
|
+
@acc_handler.save_sidekiq_jid(sidekiq_jid)
|
49
|
+
|
50
|
+
if ( ['reserved', 'awaiting_retry', 'waiting_at_worker'].include?(@acc_handler.account.state) || request_job!=nil || reply_job!=nil )
|
51
|
+
|
52
|
+
if reply_job == nil
|
53
|
+
|
54
|
+
# There are available watchers?
|
55
|
+
validate_available_watchers(request_tube)
|
56
|
+
|
57
|
+
# Is the provider available to process requests?
|
58
|
+
validate_provider_status
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
if ( request_job==nil && reply_job==nil )
|
63
|
+
@logger.info "putting job in #{@helper.request_tube_name} tube"
|
64
|
+
@logger.info "expect response in #{@helper.reply_tube_name} tube"
|
65
|
+
request_job_data = request_tube.put(Oj.dump(@helper.queue_message), pri: @acc_handler.priority, ttr: BeanstalkdHandler::TIME_TO_RUN)
|
66
|
+
request_job = @bns_handler.find_job(request_job_data[:id])
|
67
|
+
@acc_handler.save_beanstalkd_jid(request_job_data[:id])
|
68
|
+
end
|
69
|
+
|
70
|
+
if reply_job == nil
|
71
|
+
reply_job = reserve_reply_job(reply_tube, request_tube, request_job)
|
72
|
+
end
|
73
|
+
|
74
|
+
# At this point all should be ok
|
75
|
+
@logger.info "reply job obtained: #{reply_job}"
|
76
|
+
|
77
|
+
if reply_job != nil
|
78
|
+
process_job_response(reply_job, request_job)
|
79
|
+
elsif reply_job==nil && @acc_handler.account.state!='error'
|
80
|
+
mark_account_with_invalid_state
|
81
|
+
end
|
82
|
+
|
83
|
+
else
|
84
|
+
mark_account_with_invalid_state
|
85
|
+
end
|
86
|
+
|
87
|
+
rescue Beaneater::NotConnected => exception
|
88
|
+
@logger.info "beanstalkd service was unavailable"
|
89
|
+
exception = XBP::Error::MessageQueueNotReachable.new("Beanstalk queue is not reachable")
|
90
|
+
@acc_handler.queue_for_retry
|
91
|
+
raise exception
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def mark_account_with_invalid_state
|
99
|
+
@logger.info "account was in an invalid state: #{@acc_handler.account.state}"
|
100
|
+
exception = XBP::Error::AccountInvalidState.new("The account was in an invalid state: #{@acc_handler.account.state}.")
|
101
|
+
@acc_handler.save_exception(exception)
|
102
|
+
@acc_handler.error
|
103
|
+
end
|
104
|
+
|
105
|
+
def validate_available_watchers(tube)
|
106
|
+
if tube.stats.current_watching == 0
|
107
|
+
exception = XBP::Error::WorkersUnavailable.new("No workers available for queue #{tube.name}.")
|
108
|
+
@acc_handler.queue_for_retry
|
109
|
+
raise exception
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def validate_provider_status
|
114
|
+
# TODO: status == obtain_status
|
115
|
+
# TODO: if status != 'ok'
|
116
|
+
# TODO: exception = XBP::Error::??
|
117
|
+
# TODO: @acc_handler.save_sidekiq_exception(exception)
|
118
|
+
# TODO: @acc_handler.error
|
119
|
+
# TODO: else
|
120
|
+
# TODO: true
|
121
|
+
# TODO: end
|
122
|
+
end
|
123
|
+
|
124
|
+
def reserve_existing_reply_job(reply_tube)
|
125
|
+
reply_job = nil
|
126
|
+
|
127
|
+
if reply_job_data = reply_tube.peek(:ready)
|
128
|
+
reply_job = reply_tube.reserve
|
129
|
+
|
130
|
+
if ( ['reserved', 'awaiting_retry', 'waiting_at_worker'].include?(@acc_handler.account.state) )
|
131
|
+
@acc_handler.start_action
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
reply_job
|
136
|
+
end
|
137
|
+
|
138
|
+
def kick_existing_buried_job(job_id)
|
139
|
+
request_job = @bns_handler.find_job(job_id)
|
140
|
+
if request_job != nil
|
141
|
+
if request_job.stats.state == 'buried'
|
142
|
+
request_job.kick
|
143
|
+
elsif request_job.stats == 'ready'
|
144
|
+
# Do nothing: soon some worker will process this job
|
145
|
+
elsif request_job.stats == 'reserved'
|
146
|
+
# Do nothing: some worker should be processing this job
|
147
|
+
elsif request_job.stats == 'delayed'
|
148
|
+
# Do nothing: this job suddenly will be back on 'ready' queue
|
149
|
+
end
|
150
|
+
else
|
151
|
+
# Do nothing: if request_job doesn't exist it will be created
|
152
|
+
end
|
153
|
+
|
154
|
+
request_job
|
155
|
+
end
|
156
|
+
|
157
|
+
def process_job_response(reply_job, request_job)
|
158
|
+
if @acc_handler.account.state == 'waiting_at_worker'
|
159
|
+
@acc_handler.start_action
|
160
|
+
end
|
161
|
+
|
162
|
+
body = Oj.load(reply_job.body.to_s)
|
163
|
+
|
164
|
+
if exception = body[:exception]
|
165
|
+
if @helper.try_again?(exception)
|
166
|
+
@acc_handler.queue_for_retry
|
167
|
+
|
168
|
+
# Raise exception for retry
|
169
|
+
if exception[:type]
|
170
|
+
if exception[:type].kind_of?(String)
|
171
|
+
raise Kernel.const_get(exception[:type]), exception[:message]
|
172
|
+
else
|
173
|
+
raise exception[:type], exception[:message]
|
174
|
+
end
|
175
|
+
else
|
176
|
+
raise exception, exception[:message]
|
177
|
+
end
|
178
|
+
else
|
179
|
+
@acc_handler.save_exception(exception)
|
180
|
+
@acc_handler.error
|
181
|
+
end
|
182
|
+
else
|
183
|
+
@acc_handler.save_success_response(body)
|
184
|
+
@acc_handler.success
|
185
|
+
end
|
186
|
+
|
187
|
+
reply_job.delete
|
188
|
+
## request_job.delete
|
189
|
+
end
|
190
|
+
|
191
|
+
def reserve_reply_job(reply_tube, request_tube, request_job)
|
192
|
+
job = nil
|
193
|
+
poll_count = 0
|
194
|
+
poll_done = false
|
195
|
+
|
196
|
+
if request_tube.stats.current_waiting == 0
|
197
|
+
@acc_handler.queue_for_wait_at_worker
|
198
|
+
else
|
199
|
+
@acc_handler.start_action
|
200
|
+
end
|
201
|
+
|
202
|
+
while !poll_done
|
203
|
+
|
204
|
+
begin
|
205
|
+
job = reply_tube.reserve(@poll_interval)
|
206
|
+
poll_done = true
|
207
|
+
rescue Beaneater::TimedOutError => ex
|
208
|
+
|
209
|
+
begin
|
210
|
+
if request_job.stats.state == 'ready'
|
211
|
+
# Do nothing: wait another second or less
|
212
|
+
elsif request_job.stats.state == 'reserved'
|
213
|
+
if @acc_handler.account.state == 'waiting_at_worker'
|
214
|
+
@acc_handler.start_action
|
215
|
+
else
|
216
|
+
# Do nothing: wait another second or less
|
217
|
+
end
|
218
|
+
elsif request_job.stats.state == 'buried'
|
219
|
+
# Do nothing: reply tube should contain the response
|
220
|
+
end
|
221
|
+
rescue Beaneater::NotFoundError => ex
|
222
|
+
# Request_job has been deleted, it is ok!
|
223
|
+
# Reply tube should be ready
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
poll_count += 1
|
229
|
+
if poll_count >= @poll_max_tries
|
230
|
+
poll_done = true
|
231
|
+
elsif poll_count == @poll_tries_before_airbrake
|
232
|
+
@airbrake_notifier.notify(:error_message => "Polling is taking more than #{@poll_tries_before_airbrake} seconds.")
|
233
|
+
end
|
234
|
+
|
235
|
+
end # polling
|
236
|
+
|
237
|
+
if job == nil
|
238
|
+
error_message = "Queue #{@helper.reply_tube_name} time out. Waited #{@poll_max_tries} seconds."
|
239
|
+
exception = XBP::Error::MessageQueueTimeout.new(error_message)
|
240
|
+
|
241
|
+
@logger.info error_message
|
242
|
+
@acc_handler.save_exception(exception)
|
243
|
+
@acc_handler.error
|
244
|
+
@airbrake_notifier.notify(:error_message => error_message)
|
245
|
+
end
|
246
|
+
|
247
|
+
job
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Spool
|
2
|
+
module Publisher
|
3
|
+
def add_subscriber(object)
|
4
|
+
@subscribers ||= []
|
5
|
+
@subscribers << object
|
6
|
+
end
|
7
|
+
|
8
|
+
def publish(message, *args)
|
9
|
+
return if !(@subscribers && @subscribers.any?)
|
10
|
+
@subscribers.each do |subscriber|
|
11
|
+
subscriber.send(message, *args) if subscriber.respond_to?(message)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Load to simulate a rails environment
|
2
|
+
require './lib/dependencies'
|
3
|
+
|
4
|
+
module SidekiqStrategies
|
5
|
+
Rails.application.routes.url_helpers
|
6
|
+
|
7
|
+
# Initialize helpers
|
8
|
+
acc_handler = AccountHandler.new(2, 4, :query, 10, Spool::Publisher)
|
9
|
+
bns_handler = BeanstalkdHandler.new
|
10
|
+
helper = Helper.new(acc_handler, Rails.application.routes.url_helpers)
|
11
|
+
|
12
|
+
# Assign watchers
|
13
|
+
acc_handler.add_subscriber(Spool::Subscribers::RedisPubSubSubscriber.new)
|
14
|
+
acc_handler.add_subscriber(Spool::Subscribers::EventsAgentSubscriber.new)
|
15
|
+
|
16
|
+
# Define external dependencies
|
17
|
+
logger = Sidekiq.logger
|
18
|
+
airbrake_notifier = Airbrake
|
19
|
+
|
20
|
+
# Initiailze strategy executor
|
21
|
+
strategy = PollingStrategy.new(acc_handler, bns_handler, logger, airbrake_notifier, helper)
|
22
|
+
|
23
|
+
# Run strategy
|
24
|
+
strategy.perform('56dg4df68g')
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sidekiq_strategies/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "sidekiq_strategies"
|
8
|
+
spec.version = SidekiqStrategies::VERSION
|
9
|
+
spec.authors = ["abrahamb"]
|
10
|
+
spec.email = ["abraham.ordonez@xoom.com"]
|
11
|
+
spec.summary = %q{Sidekiq strategies to manage accounts processing actions.}
|
12
|
+
spec.description = %q{Sidekiq strategies to manage accounts processing actions.}
|
13
|
+
spec.homepage = "https://github.com/blue-kite/sidekiq_strategies"
|
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_development_dependency 'bundler', '~> 1.6'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec'
|
24
|
+
spec.add_dependency 'beaneater', '~> 0.3.2'
|
25
|
+
spec.add_dependency 'oj', '~> 2.9.0'
|
26
|
+
spec.add_dependency 'money'
|
27
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Load to simulate a rails environment
|
2
|
+
require './lib/dependencies'
|
3
|
+
|
4
|
+
Dir["./spec/support/**/*.rb"].sort.each { |f| require f}
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
# The settings below are suggested to provide a good initial experience
|
8
|
+
# with RSpec, but feel free to customize to your heart's content.
|
9
|
+
|
10
|
+
# These two settings work together to allow you to limit a spec run
|
11
|
+
# to individual examples or groups you care about by tagging them with
|
12
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
13
|
+
# get run.
|
14
|
+
config.filter_run :focus
|
15
|
+
config.run_all_when_everything_filtered = true
|
16
|
+
|
17
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
18
|
+
# file, and it's useful to allow more verbose output when running an
|
19
|
+
# individual spec file.
|
20
|
+
if config.files_to_run.one?
|
21
|
+
# Use the documentation formatter for detailed output,
|
22
|
+
# unless a formatter has already been configured
|
23
|
+
# (e.g. via a command-line flag).
|
24
|
+
config.default_formatter = 'doc'
|
25
|
+
end
|
26
|
+
|
27
|
+
# Print the 10 slowest examples and example groups at the
|
28
|
+
# end of the spec run, to help surface which specs are running
|
29
|
+
# particularly slow.
|
30
|
+
config.profile_examples = 10
|
31
|
+
|
32
|
+
# Run specs in random order to surface order dependencies. If you find an
|
33
|
+
# order dependency and want to debug it, you can fix the order by providing
|
34
|
+
# the seed, which is printed after each run.
|
35
|
+
# --seed 1234
|
36
|
+
config.order = :random
|
37
|
+
|
38
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
39
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
40
|
+
# test failures related to randomization by passing the same `--seed` value
|
41
|
+
# as the one that triggered the failure.
|
42
|
+
Kernel.srand config.seed
|
43
|
+
|
44
|
+
# rspec-expectations config goes here. You can use an alternate
|
45
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
46
|
+
# assertions if you prefer.
|
47
|
+
config.expect_with :rspec do |expectations|
|
48
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
49
|
+
# For more details, see:
|
50
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
51
|
+
expectations.syntax = :expect
|
52
|
+
end
|
53
|
+
|
54
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
55
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
56
|
+
config.mock_with :rspec do |mocks|
|
57
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
58
|
+
# For more details, see:
|
59
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
60
|
+
mocks.syntax = :expect
|
61
|
+
|
62
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
63
|
+
# a real object. This is generally recommended.
|
64
|
+
mocks.verify_partial_doubles = true
|
65
|
+
end
|
66
|
+
end
|