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.
@@ -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