sidekiq_strategies 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module SidekiqStrategies
2
+ VERSION = "0.0.1"
3
+ 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,11 @@
1
+ module Spool
2
+ module Subscribers
3
+ class EventsAgentSubscriber
4
+
5
+ def state_changed(account, from, to)
6
+ puts "Spool::Subscribers::EventsAgentSubscriber: [state_changed] - account: #{account}, from: #{from}, to: #{to}"
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Spool
2
+ module Subscribers
3
+ class RedisPubSubSubscriber
4
+
5
+ def state_changed(account, from, to)
6
+ puts "Spool::Subscribers::RedisPubSubSubscriber: [state_changed] - account: #{account}, from: #{from}, to: #{to}"
7
+ end
8
+
9
+ end
10
+ end
11
+ 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
@@ -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