freekiqs 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
+ SHA1:
3
+ metadata.gz: 638d7e5e400a98fe5dbde8e7c145c8caf279a145
4
+ data.tar.gz: 967ad2c2b650ed7a27f4573dfdaaf3ac61070e0c
5
+ SHA512:
6
+ metadata.gz: 01e4a44ec9eebe9e8328f258c69c18ff42ce1e7c8cc50d0166b958dab857a95e5cf30e5c2bac0fcb1af5bcdd0a37817b14bf9d5df5437927a37446760bdf523e
7
+ data.tar.gz: 65a722fce7bd7eaf1dc575dcf227a3d33efaaf9fa81f05989b70e4349ac5d92eff246d672bf0897fd6c25955bbdc5bc808e368437bbbd2a1fd83bf4adb25ca73
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ tmp
2
+ gemfiles/*.lock
3
+ Gemfile.lock
4
+ .ruby-version
5
+ pkg
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'rake'
6
+
7
+ group :test do
8
+ gem 'rspec', '~> 3.1'
9
+ end
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 BookBub
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Freekiqs
2
+
3
+ Sidekiq middleware that allows capturing exceptions thrown
4
+ by failed jobs and wrapping them with a `FreekiqException` exception class
5
+ that can be filtered by monitoring tools such as New Relic and
6
+ Honeybadger.
7
+
8
+ Configuration example (in config/initializers/sidekiq.rb):
9
+ ``` ruby
10
+ Sidekiq.configure_server do |config|
11
+ config.server_middleware do |chain|
12
+ chain.insert_after Sidekiq::Middleware::Server::RetryJobs, Sidekiq::Middleware::Server::Freekiqs
13
+ end
14
+ end
15
+ ```
16
+
17
+ Worker example:
18
+ ``` ruby
19
+ class MyWorker
20
+ include Sidekiq::Worker
21
+ sidekiq_options freekiqs: 3
22
+ #
23
+ def perform(param)
24
+ ...
25
+ end
26
+ end
27
+ ```
28
+
29
+ Freekiqs is disabled by default. It can be enabled per-worker
30
+ by setting `:freekiqs` on `sidekiq_options`. Or, it can be
31
+ enabled globally by adding `:freekiqs` to the middleware
32
+ registration.
33
+
34
+ Example:
35
+ ``` ruby
36
+ Sidekiq.configure_server do |config|
37
+ config.server_middleware do |chain|
38
+ chain.insert_after Sidekiq::Middleware::Server::RetryJobs, Sidekiq::Middleware::Server::Freekiqs, freekiqs: 3
39
+ end
40
+ end
41
+ ```
42
+
43
+ ## Overview
44
+
45
+ Up to the configured number of "freekiqs", catch exceptions thrown
46
+ from job and wrap them with the `FreekiqException` exception (which is a
47
+ `RuntimeError`). That exception type can be ignored by monitoring
48
+ tools. If job fails more times than configured "freekiqs", thrown
49
+ exception will not be wrapped. That should result in normal operation
50
+ and the exception showing up in monitoring tools.
51
+
52
+ Implementation Details:
53
+ This relies on Sidekiq's built-in retry handling. Specifically, its
54
+ `retry_count` value. When a job first fails, its `retry_count` value
55
+ is nil (because it hasn't actually been retried yet). That exception,
56
+ along with subsequent exceptions from retries, are caught and wrapped
57
+ with the `FreekiqException` exception.
58
+ Cases where Freekiqs does NOT wrap the exception:
59
+ - The `retry` option is false
60
+ - The `freekiqs` option is not set on the worker nor globally
61
+ - The `freekiqs` option is not set on worker and set to `false` globally
62
+ - The `freekiqs` option is set to `false` on the worker
63
+ - The job threw an exception that is not a StandardError (nor a subclass)
64
+ - The number of thrown exceptions is more than specified freekiqs
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ begin
2
+ require 'rspec/core/rake_task'
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ rescue LoadError
5
+ end
6
+
7
+ task default: :spec
data/freekiqs.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'freekiqs'
5
+ gem.version = '0.1.0'
6
+ gem.authors = ['Rob Lewis']
7
+ gem.email = ['rob@bookbub.com']
8
+ gem.summary = 'Sidekiq middleware extending RetryJobs to allow silient errors.'
9
+ gem.description = 'Sidekiq middleware extending RetryJobs to allow configuring ' \
10
+ 'how many exceptions a job can throw and be wrapped by a silenceable exception.'
11
+ gem.homepage = 'https://github.com/BookBub/freekiqs'
12
+ gem.license = 'MIT'
13
+ gem.executables = []
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- spec/*`.split("\n")
16
+ gem.require_paths = ['lib']
17
+ gem.required_ruby_version = '>= 1.9.3'
18
+
19
+ gem.add_dependency 'sidekiq', '>= 1.0.0', '< 4.0.0'
20
+ gem.add_development_dependency 'rspec', '~> 2.14.1'
21
+ end
data/lib/freekiqs.rb ADDED
@@ -0,0 +1 @@
1
+ require 'sidekiq/middleware/server/freekiqs'
@@ -0,0 +1,46 @@
1
+ require 'sidekiq'
2
+ require 'sidekiq/util'
3
+
4
+ module Sidekiq
5
+ class FreekiqException < RuntimeError; end
6
+
7
+ module Middleware
8
+ module Server
9
+ class Freekiqs
10
+ include Sidekiq::Util
11
+
12
+ def initialize(opts={})
13
+ @default_freekiqs = opts[:freekiqs]
14
+ end
15
+
16
+ def call(worker, msg, queue)
17
+ yield
18
+ rescue => ex
19
+ freekiqs = get_freekiqs_if_enabled(worker, msg)
20
+ if freekiqs
21
+ if msg['retry_count'].nil? || msg['retry_count'] < freekiqs-1
22
+ raise FreekiqException, ex.message
23
+ else
24
+ Sidekiq.logger.info { "Out of free kiqs for #{msg['class']} job #{msg['jid']}" }
25
+ end
26
+ end
27
+ raise ex
28
+ end
29
+
30
+ def get_freekiqs_if_enabled(worker, msg)
31
+ freekiqs = nil
32
+ if msg['retry']
33
+ if worker.class.sidekiq_options['freekiqs'] != false
34
+ if worker.class.sidekiq_options['freekiqs']
35
+ freekiqs = worker.class.sidekiq_options['freekiqs'].to_i
36
+ elsif @default_freekiqs
37
+ freekiqs = @default_freekiqs.to_i
38
+ end
39
+ end
40
+ end
41
+ freekiqs
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,168 @@
1
+ require 'spec_helper'
2
+ require 'celluloid' # Getting error without this required. Should remove once this supports Sidekiq 4.x
3
+ require 'sidekiq/cli'
4
+ require 'sidekiq/middleware/server/retry_jobs'
5
+
6
+ describe Sidekiq::Middleware::Server::Freekiqs do
7
+ class DummyWorkerPlain
8
+ include Sidekiq::Worker
9
+ end
10
+ class DummyWorkerWithFreekiqsEnabled
11
+ include Sidekiq::Worker
12
+ sidekiq_options freekiqs: 2
13
+ end
14
+ class DummyWorkerWithFreekiqsDisabled
15
+ include Sidekiq::Worker
16
+ sidekiq_options freekiqs: false
17
+ end
18
+
19
+ def build_handler_chain(freekiq_options={}, retry_options={})
20
+ Sidekiq::Middleware::Chain.new do |chain|
21
+ chain.add Sidekiq::Middleware::Server::RetryJobs, retry_options
22
+ chain.add Sidekiq::Middleware::Server::Freekiqs, freekiq_options
23
+ end
24
+ end
25
+
26
+ def build_job(options={})
27
+ {'class' => 'FreekiqDummyWorker', 'args' => [], 'retry' => true}.merge(options)
28
+ end
29
+
30
+ def cleanup_redis
31
+ Sidekiq::RetrySet.new.select{|job| job.klass == 'FreekiqDummyWorker'}.each(&:delete)
32
+ Sidekiq::DeadSet.new.clear
33
+ end
34
+
35
+ before(:each) do
36
+ cleanup_redis
37
+ end
38
+
39
+ let(:worker_plain) { DummyWorkerPlain.new }
40
+ let(:worker_with_freekiqs_enabled) { DummyWorkerWithFreekiqsEnabled.new }
41
+ let(:worker_with_freekiqs_disabled) { DummyWorkerWithFreekiqsDisabled.new }
42
+
43
+ it 'requires RetryJobs to update retry_count' do
44
+ handler = Sidekiq::Middleware::Server::RetryJobs.new
45
+ worker = worker_plain
46
+ job = build_job
47
+ expect {
48
+ handler.call(worker, job, 'default') do
49
+ raise 'Oops'
50
+ end
51
+ }.to raise_error(RuntimeError)
52
+ expect(job['retry_count']).to eq(0)
53
+ expect {
54
+ handler.call(worker, job, 'default') do
55
+ raise 'Oops'
56
+ end
57
+ }.to raise_error(RuntimeError)
58
+ expect(job['retry_count']).to eq(1)
59
+ end
60
+
61
+ it 'throws Freekiq exception for specified number of free kiqs' do
62
+ handler = build_handler_chain
63
+ worker = worker_with_freekiqs_enabled
64
+ job = build_job
65
+
66
+ expect {
67
+ handler.invoke(worker, job, 'default') do
68
+ raise ArgumentError, 'overlooked'
69
+ end
70
+ }.to raise_error(Sidekiq::FreekiqException, 'overlooked')
71
+ expect(job['retry_count']).to eq(0)
72
+ expect(job['error_class']).to eq('Sidekiq::FreekiqException')
73
+ expect(job['error_message']).to eq('overlooked')
74
+ expect {
75
+ handler.invoke(worker, job, 'default') do
76
+ raise ArgumentError, 'overlooked'
77
+ end
78
+ }.to raise_error(Sidekiq::FreekiqException, 'overlooked')
79
+ expect(job['retry_count']).to eq(1)
80
+ expect(job['error_class']).to eq('Sidekiq::FreekiqException')
81
+ expect(job['error_message']).to eq('overlooked')
82
+ expect {
83
+ handler.invoke(worker, job, 'default') do
84
+ raise ArgumentError, 'not overlooked'
85
+ end
86
+ }.to raise_error(ArgumentError, 'not overlooked')
87
+ expect(job['retry_count']).to eq(2)
88
+ expect(job['error_class']).to eq('ArgumentError')
89
+ expect(job['error_message']).to eq('not overlooked')
90
+ expect(Sidekiq::RetrySet.new.size).to eq(3)
91
+ expect(Sidekiq::DeadSet.new.size).to eq(0)
92
+ end
93
+
94
+ it 'allows a freekiqs option in initializer' do
95
+ handler = build_handler_chain(freekiqs: 1)
96
+ worker = worker_plain
97
+ job = build_job
98
+
99
+ expect {
100
+ handler.invoke(worker, job, 'default') do
101
+ raise ArgumentError, 'overlooked'
102
+ end
103
+ }.to raise_error(Sidekiq::FreekiqException, 'overlooked')
104
+ expect(job['retry_count']).to eq(0)
105
+ expect(job['error_class']).to eq('Sidekiq::FreekiqException')
106
+ expect(job['error_message']).to eq('overlooked')
107
+ expect {
108
+ handler.invoke(worker, job, 'default') do
109
+ raise ArgumentError, 'not overlooked'
110
+ end
111
+ }.to raise_error(ArgumentError, 'not overlooked')
112
+ expect(job['retry_count']).to eq(1)
113
+ expect(job['error_class']).to eq('ArgumentError')
114
+ expect(job['error_message']).to eq('not overlooked')
115
+ expect(Sidekiq::RetrySet.new.size).to eq(2)
116
+ expect(Sidekiq::DeadSet.new.size).to eq(0)
117
+ end
118
+
119
+ it 'allows explicitly disabling freekiqs' do
120
+ handler = build_handler_chain(freekiqs: 3)
121
+ worker = worker_with_freekiqs_disabled
122
+ job = build_job
123
+
124
+ expect {
125
+ handler.invoke(worker, job, 'default') do
126
+ raise ArgumentError, 'not overlooked'
127
+ end
128
+ }.to raise_error(ArgumentError, 'not overlooked')
129
+ expect(job['retry_count']).to eq(0)
130
+ expect(job['error_class']).to eq('ArgumentError')
131
+ expect(job['error_message']).to eq('not overlooked')
132
+ expect(Sidekiq::RetrySet.new.size).to eq(1)
133
+ expect(Sidekiq::DeadSet.new.size).to eq(0)
134
+ end
135
+
136
+ it 'does nothing if not explicitly enabled' do
137
+ handler = build_handler_chain
138
+ worker = worker_plain
139
+ job = build_job
140
+
141
+ expect {
142
+ handler.invoke(worker, job, 'default') do
143
+ raise ArgumentError, 'not overlooked'
144
+ end
145
+ }.to raise_error(ArgumentError, 'not overlooked')
146
+ expect(job['retry_count']).to eq(0)
147
+ expect(job['error_class']).to eq('ArgumentError')
148
+ expect(job['error_message']).to eq('not overlooked')
149
+ end
150
+
151
+ it 'does nothing if retries disabled' do
152
+ handler = build_handler_chain
153
+ worker = worker_with_freekiqs_enabled
154
+ job = build_job('retry' => false)
155
+
156
+ expect {
157
+ handler.invoke(worker, job, 'default') do
158
+ raise ArgumentError, 'not overlooked'
159
+ end
160
+ }.to raise_error(ArgumentError, 'not overlooked')
161
+ expect(job['retry_count']).to be_nil
162
+ expect(job['error_class']).to be_nil
163
+ expect(job['error_message']).to be_nil
164
+ expect(job['error_message']).to be_nil
165
+ expect(Sidekiq::RetrySet.new.size).to eq(0)
166
+ expect(Sidekiq::DeadSet.new.size).to eq(0)
167
+ end
168
+ end
@@ -0,0 +1,11 @@
1
+ require 'freekiqs'
2
+
3
+ RSpec.configure do |config|
4
+ config.expect_with :rspec do |expectations|
5
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
6
+ end
7
+
8
+ config.mock_with :rspec do |mocks|
9
+ mocks.verify_partial_doubles = true
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: freekiqs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rob Lewis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sidekiq
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 4.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 4.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: rspec
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 2.14.1
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 2.14.1
47
+ description: Sidekiq middleware extending RetryJobs to allow configuring how many
48
+ exceptions a job can throw and be wrapped by a silenceable exception.
49
+ email:
50
+ - rob@bookbub.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - ".gitignore"
56
+ - Gemfile
57
+ - MIT-LICENSE
58
+ - README.md
59
+ - Rakefile
60
+ - freekiqs.gemspec
61
+ - lib/freekiqs.rb
62
+ - lib/sidekiq/middleware/server/freekiqs.rb
63
+ - spec/freekiqs_spec.rb
64
+ - spec/spec_helper.rb
65
+ homepage: https://github.com/BookBub/freekiqs
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 1.9.3
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.4.3
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Sidekiq middleware extending RetryJobs to allow silient errors.
89
+ test_files:
90
+ - spec/freekiqs_spec.rb
91
+ - spec/spec_helper.rb