sneakers_retry_handler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9054e59d5824086b0d673bcc5107653a15851aa673e4d71acf810d37c871dd53
4
+ data.tar.gz: 25b8ce8110ff9e371bbbfd1aeed00f7d9fc8567ac297324e081da02c732b7225
5
+ SHA512:
6
+ metadata.gz: 8496d96901358047ff1f29ad7354727ebdc7373cf97e84b6c760093f5f5e837ed03885513232ec6526b05134ac70b8f366c7eb6c96e64e7d8411521cd6d521ee
7
+ data.tar.gz: e0db7b566d7003932d05cf3aba339fb2cfb223258fb27cf9b1851c37be54b6893e10cee6bbb8a276476c7030069bf4fff167e48ec5eb91e5144ee2a7e7de5c21
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /tmp/
5
+ /log/
6
+ build/
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ 0.1.0
2
+ #
3
+ * Added basic functionality
4
+ #
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'http://rubygems.org'
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,56 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sneakers_retry_handler (0.1.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ amq-protocol (2.3.0)
10
+ bunny (2.14.3)
11
+ amq-protocol (~> 2.3, >= 2.3.0)
12
+ coderay (1.1.2)
13
+ concurrent-ruby (1.1.5)
14
+ diff-lcs (1.3)
15
+ json (2.2.0)
16
+ method_source (0.9.2)
17
+ pry (0.12.2)
18
+ coderay (~> 1.1.0)
19
+ method_source (~> 0.9.0)
20
+ rake (13.0.1)
21
+ rspec (3.9.0)
22
+ rspec-core (~> 3.9.0)
23
+ rspec-expectations (~> 3.9.0)
24
+ rspec-mocks (~> 3.9.0)
25
+ rspec-core (3.9.0)
26
+ rspec-support (~> 3.9.0)
27
+ rspec-expectations (3.9.0)
28
+ diff-lcs (>= 1.2.0, < 2.0)
29
+ rspec-support (~> 3.9.0)
30
+ rspec-mocks (3.9.0)
31
+ diff-lcs (>= 1.2.0, < 2.0)
32
+ rspec-support (~> 3.9.0)
33
+ rspec-support (3.9.0)
34
+ serverengine (2.0.7)
35
+ sigdump (~> 0.2.2)
36
+ sigdump (0.2.4)
37
+ sneakers (2.11.0)
38
+ bunny (~> 2.12)
39
+ concurrent-ruby (~> 1.0)
40
+ rake
41
+ serverengine (~> 2.0.5)
42
+ thor
43
+ thor (0.20.3)
44
+
45
+ PLATFORMS
46
+ ruby
47
+
48
+ DEPENDENCIES
49
+ json (~> 2.2)
50
+ pry (~> 0.12)
51
+ rspec (~> 3.9)
52
+ sneakers (~> 2.11)
53
+ sneakers_retry_handler!
54
+
55
+ BUNDLED WITH
56
+ 1.17.3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Restaurant Cheetah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ ## SneakersRetryHandler
2
+
3
+ Run your worker with delayed retrying.
4
+ Define `on_retry` and `on_error` callbacks.
5
+
6
+ ## Install
7
+
8
+ ```ruby
9
+ gem install sneakers_retry_handler
10
+ ```
11
+ ## Usage of the `DelayedRetry`
12
+
13
+ The `DelayedRetry` handler is an extension of the default `MaxRetry` handler.
14
+ It will try to process the message specified number of times.
15
+ When the maximum number of retries is reached it will put the message on an error queue.
16
+
17
+ When defining your worker, you have to define these extra arguments:
18
+
19
+ - `number_of_retries`: Specifies how many times to retry.
20
+
21
+ - `sleep_before_retry`: Retrying delay.
22
+
23
+ - `retriable_errors`: The list of errors. Puts the message on an error queue otherwise.
24
+
25
+ - `x-dead-letter-exchange`: The name of the dead-letter exchange where failed messages will be published to.
26
+
27
+ - `on_retry` and `on_error`: Callbacks.
28
+
29
+
30
+ Here's an example:
31
+
32
+ ```diff
33
+ class BusyWorker
34
+ include Sneakers::Worker
35
+
36
+ from_queue(
37
+ 'busy_worker_queue',
38
+ + exchange: 'retry_exchange',
39
+ + exchange_type: :topic,
40
+ + handler: Sneakers::Handlers::DelayedRetry,
41
+ + arguments: {
42
+ + 'x-dead-letter-exchange': 'retry_exchange'
43
+ + },
44
+ + number_of_retries: 3,
45
+ + sleep_before_retry: 2,
46
+ + retriable_errors: [Faraday::TimeoutError],
47
+ + on_retry: proc do |error, payload, tries|
48
+ + /* put your logic here */
49
+ + end,
50
+ + on_error: proc do |error, payload, tries|
51
+ + /* put your logic here */
52
+ + end
53
+ )
54
+
55
+ def work(*args)
56
+ ack!
57
+ end
58
+ end
59
+ ```
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sneakers
4
+ module Handlers
5
+ class DelayedRetry
6
+ def initialize(channel, queue, opts)
7
+ @worker_queue_name = queue.name
8
+ Sneakers.logger.debug do
9
+ "#{log_prefix} creating handler, opts=#{opts}"
10
+ end
11
+
12
+ @channel = channel
13
+ @opts = opts
14
+
15
+ error_exchange_name = @opts[:error_exchange_name] || 'error_exchange'
16
+ Sneakers.logger.debug do
17
+ "#{log_prefix} creating exchange=#{error_exchange_name}"
18
+ end
19
+ @error_exchange = @channel.exchange(
20
+ error_exchange_name,
21
+ type: 'direct',
22
+ durable: exchange_durable?
23
+ )
24
+
25
+ error_queue_name = @opts[:error_queue_name] || "error.#{@worker_queue_name}"
26
+ Sneakers.logger.debug do
27
+ "#{log_prefix} creating queue=#{error_queue_name}"
28
+ end
29
+ error_queue = @channel.queue(
30
+ error_queue_name,
31
+ durable: queue_durable?
32
+ )
33
+
34
+ error_queue.bind(@error_exchange, routing_key: @worker_queue_name)
35
+
36
+ @max_retries = @opts[:number_of_retries] || 5
37
+ @sleep_before_retry = @opts[:sleep_before_retry] || 0
38
+ @retriable_errors = @opts[:retriable_errors] || []
39
+ @on_retry = @opts[:on_retry] || proc {}
40
+ @on_error = @opts[:on_error] || proc {}
41
+ end
42
+
43
+ def acknowledge(hdr, _props, _msg)
44
+ @channel.acknowledge(hdr.delivery_tag, false)
45
+ end
46
+
47
+ def reject(hdr, props, msg, requeue = false)
48
+ if requeue
49
+ @channel.reject(hdr.delivery_tag, requeue)
50
+ else
51
+ handle_retry(hdr, props, msg, :reject)
52
+ end
53
+ end
54
+
55
+ def error(hdr, props, msg, err)
56
+ handle_retry(hdr, props, msg, err)
57
+ end
58
+
59
+ def noop(hdr, props, msg); end
60
+
61
+ private
62
+
63
+ def handle_retry(hdr, props, msg, reason)
64
+ num_attempts = failure_count(props[:headers]) + 1
65
+
66
+ if (num_attempts <= @max_retries) && retriable_on?(reason)
67
+ Sneakers.logger.info do
68
+ "#{log_prefix} msg=retrying, count=#{num_attempts}, headers=#{props[:headers]}"
69
+ end
70
+
71
+ sleep(@sleep_before_retry)
72
+
73
+ @on_retry.call(reason, msg, num_attempts)
74
+
75
+ @channel.reject(hdr.delivery_tag, false)
76
+ else
77
+ Sneakers.logger.info do
78
+ "#{log_prefix} msg=failing, retry_count=#{num_attempts}, reason=#{reason}"
79
+ end
80
+
81
+ @on_error.call(reason, msg, num_attempts)
82
+
83
+ error_data = {
84
+ error: reason.to_s,
85
+ num_attempts: num_attempts,
86
+ failed_at: Time.now.iso8601,
87
+ properties: props
88
+ }.tap do |hash|
89
+ if reason.is_a?(Exception)
90
+ hash[:error_class] = reason.class.to_s
91
+ hash[:error_message] = reason.to_s
92
+ if reason.backtrace
93
+ hash[:backtrace] = reason.backtrace.take(10).join(', ')
94
+ end
95
+ end
96
+ end
97
+
98
+ data =
99
+ begin
100
+ JSON.parse(msg).merge(error_data: error_data).to_json
101
+ rescue
102
+ error_data.merge(payload: msg).to_json
103
+ end
104
+
105
+ @error_exchange.publish(data, routing_key: hdr.routing_key)
106
+ @channel.acknowledge(hdr.delivery_tag, false)
107
+ end
108
+ end
109
+
110
+ def retriable_on?(exception)
111
+ @retriable_errors.map { |error_class| exception.is_a?(error_class) }.any?
112
+ end
113
+
114
+ def failure_count(headers)
115
+ if headers.nil? || headers['x-death'].nil?
116
+ 0
117
+ else
118
+ x_death_array = headers['x-death'].select do |x_death|
119
+ x_death['queue'] == @worker_queue_name
120
+ end
121
+ if x_death_array.count > 0 && x_death_array.first['count']
122
+ x_death_array.inject(0) { |sum, x_death| sum + x_death['count'] }
123
+ else
124
+ x_death_array.count
125
+ end
126
+ end
127
+ end
128
+
129
+ def log_prefix
130
+ "DelayedRetry handler [queue=#{@worker_queue_name}]"
131
+ end
132
+
133
+ def queue_durable?
134
+ @opts.fetch(:queue_options, {}).fetch(:durable, false)
135
+ end
136
+
137
+ def exchange_durable?
138
+ queue_durable?
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sneakers/handlers/delayed_retry'
4
+
5
+ module SneakersRetryHandler
6
+ VERSION = '0.1.0'
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('../lib/sneakers_retry_handler', __FILE__)
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'sneakers_retry_handler'
7
+ s.version = SneakersRetryHandler::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.licenses = %w[MIT]
10
+ s.authors = ['Ivan Bondarenko', 'Alex Stepanenko']
11
+ s.email = ['bondarenko.dev@gmail.com', 'stepanenko.aleksander@gmail.com']
12
+ s.summary = 'Sneakers handler with delayed retrying'
13
+ s.description = 'Retries in specified time interval. Supports `on_retry` and `on_error` callbacks'
14
+ s.homepage = 'https://github.com/restaurant-cheetah/sneakers_retry_handler'
15
+
16
+ s.add_development_dependency 'json', '~> 2.2'
17
+ s.add_development_dependency 'pry', '~> 0.12'
18
+ s.add_development_dependency 'rspec', '~> 3.9'
19
+ s.add_development_dependency 'sneakers', '~> 2.11'
20
+
21
+ all_files = `git ls-files`.split("\n")
22
+ test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+
24
+ s.files = all_files - test_files
25
+ s.test_files = test_files
26
+ s.require_paths = %w[lib]
27
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Sneakers::Handlers::DelayedRetry do
6
+ describe 'as handler of the worker' do
7
+ class HandledError < StandardError; end
8
+ class UnhandledError < StandardError; end
9
+
10
+ class BusyWorker
11
+ include Sneakers::Worker
12
+
13
+ def work(msg)
14
+ perform(msg)
15
+
16
+ ack!
17
+ end
18
+
19
+ private
20
+
21
+ def perform(msg); end
22
+ end
23
+
24
+ Sneakers.configure(daemonize: true, log: 'log/test.log')
25
+ Sneakers::Worker.configure_logger(Logger.new('/dev/null'))
26
+ Sneakers::Worker.configure_metrics
27
+
28
+ let(:handler) { described_class.new(channel, queue, opts) }
29
+
30
+ let(:channel) do
31
+ double(:channel, exchange: error_exchange, queue: queue, acknowledge: 'ack', reject: 'reject')
32
+ end
33
+
34
+ let(:queue) { double(:queue, name: 'queue_name', bind: nil, opts: {}) }
35
+ let(:error_exchange) { double(:error_exchange, publish: 'publish') }
36
+ let(:delivery_info) do
37
+ double(:delivery_info, routing_key: 'routing.key', delivery_tag: 'delivery.tag', reject: {}, each: [])
38
+ end
39
+ let(:metadata) { { headers: { retry_info: {}.to_json } } }
40
+ let(:test_pool) { Concurrent::ImmediateExecutor }
41
+ let(:msg) { { key: :value }.to_json }
42
+ let(:opts) { { on_retry: on_retry_cb, on_error: on_error_cb } }
43
+ let(:on_retry_cb) { proc {} }
44
+ let(:on_error_cb) { proc {} }
45
+
46
+ subject do
47
+ BusyWorker.new(
48
+ queue,
49
+ test_pool.new
50
+ ).do_work(
51
+ delivery_info,
52
+ metadata,
53
+ msg,
54
+ handler
55
+ )
56
+ end
57
+
58
+ context 'without errors' do
59
+ before do
60
+ allow_any_instance_of(BusyWorker).to receive(:perform).with(msg).and_return(msg)
61
+ end
62
+
63
+ it 'calls acknowledge' do
64
+ expect(handler).to receive(:acknowledge).once
65
+ subject
66
+ end
67
+ end
68
+
69
+ context 'with an unhandlen error' do
70
+ before do
71
+ allow_any_instance_of(BusyWorker).to receive(:perform).with(msg).and_raise(UnhandledError)
72
+ end
73
+
74
+ it 'calls handle_retry method' do
75
+ expect(handler).to receive(:error).once
76
+ subject
77
+ end
78
+
79
+ it 'calls on_error callback' do
80
+ expect(on_error_cb).to receive(:call).once
81
+ subject
82
+ end
83
+ end
84
+
85
+ context 'with a handled error' do
86
+ let(:opts) { super().merge(retriable_errors: [HandledError]) }
87
+
88
+ before do
89
+ allow_any_instance_of(BusyWorker).to receive(:perform).with(msg).and_raise(HandledError)
90
+ end
91
+
92
+ it 'calls handle_retry method' do
93
+ expect(handler).to receive(:error).once
94
+ subject
95
+ end
96
+
97
+ it 'calls on_retry_cb callback' do
98
+ expect(on_retry_cb).to receive(:call).once
99
+ subject
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sneakers'
4
+ require 'json'
5
+ require_relative '../lib/sneakers/handlers/delayed_retry'
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sneakers_retry_handler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ivan Bondarenko
8
+ - Alex Stepanenko
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2019-11-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.2'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '2.2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: pry
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '0.12'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.12'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.9'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.9'
56
+ - !ruby/object:Gem::Dependency
57
+ name: sneakers
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '2.11'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '2.11'
70
+ description: Retries in specified time interval. Supports `on_retry` and `on_error`
71
+ callbacks
72
+ email:
73
+ - bondarenko.dev@gmail.com
74
+ - stepanenko.aleksander@gmail.com
75
+ executables: []
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - ".gitignore"
80
+ - CHANGELOG.md
81
+ - Gemfile
82
+ - Gemfile.lock
83
+ - LICENSE
84
+ - README.md
85
+ - lib/sneakers/handlers/delayed_retry.rb
86
+ - lib/sneakers_retry_handler.rb
87
+ - sneakers_retry_handler.gemspec
88
+ - spec/sneakers/handlers/delayed_retry_spec.rb
89
+ - spec/spec_helper.rb
90
+ homepage: https://github.com/restaurant-cheetah/sneakers_retry_handler
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.0.6
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Sneakers handler with delayed retrying
113
+ test_files:
114
+ - spec/sneakers/handlers/delayed_retry_spec.rb
115
+ - spec/spec_helper.rb