sidekiq-killswitch 1.0.0.pre1

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,25 @@
1
+ # frozen_string_literal: true
2
+ require File.expand_path('../lib/sidekiq/killswitch/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'sidekiq-killswitch'
6
+ gem.version = Sidekiq::Killswitch::VERSION
7
+ gem.summary = 'Cross-host Sidekiq worker killswitches'
8
+ gem.authors = ['Yuriy Naidyon']
9
+ gem.email = 'yurokle@gmail.com'
10
+ gem.license = 'Apache-2.0'
11
+ gem.homepage = 'https://github.com/square/sidekiq-killswitch'
12
+
13
+ gem.files = `git ls-files`.split($RS)
14
+ gem.test_files = gem.files.grep(%r{spec/})
15
+ gem.require_paths = ['lib']
16
+ gem.required_ruby_version = '>= 2.2.2'
17
+ gem.metadata['allowed_push_host'] = 'https://rubygems.org'
18
+
19
+ gem.add_runtime_dependency 'sidekiq', '>= 3'
20
+
21
+ gem.add_development_dependency 'rspec', '~> 3.6'
22
+ gem.add_development_dependency 'rack-test'
23
+ gem.add_development_dependency 'rspec-html-matchers'
24
+ gem.add_development_dependency 'rubocop', '~> 0.49.1'
25
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe Sidekiq::Killswitch::Config do
5
+ let(:config) { Sidekiq::Killswitch::Config.new }
6
+
7
+ describe '#web_ui_worker_validator' do
8
+ it 'should to string presence validation by default' do
9
+ expect(config.web_ui_worker_validator.call(nil)).to be_falsey
10
+ expect(config.web_ui_worker_validator.call('')).to be_falsey
11
+ expect(config.web_ui_worker_validator.call('MyWorker')).to be_truthy
12
+ end
13
+ end
14
+
15
+ describe '#validate_worker_class_in_web' do
16
+ it 'should set Sidekiq::Worker module inclusion check as validator' do
17
+ stub_const('GoodWorker', Class.new {
18
+ include Sidekiq::Worker
19
+ })
20
+ stub_const('AlsoGoodWorker', Class.new(GoodWorker) {})
21
+ stub_const('BadWorker', Class.new {})
22
+
23
+ config.validate_worker_class_in_web
24
+
25
+ expect(config.web_ui_worker_validator.call('GoodWorker')).to be_truthy
26
+ expect(config.web_ui_worker_validator.call('AlsoGoodWorker')).to be_truthy
27
+
28
+ expect(config.web_ui_worker_validator.call('BadWorker')).to be_falsey
29
+ expect(config.web_ui_worker_validator.call('')).to be_falsey
30
+ expect(config.web_ui_worker_validator.call('Sidekiq::Worker')).to be_falsey
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe Sidekiq::DeadSet do
5
+ let(:dead_set) { Sidekiq::DeadSet.new }
6
+
7
+ describe '#kill' do
8
+ it 'should put passed serialized job to the "dead" sorted set' do
9
+ serialized_job = Sidekiq.dump_json(jid: '123123', class: 'SomeWorker', args: [])
10
+ dead_set.kill(serialized_job)
11
+
12
+ expect(dead_set.find_job('123123').value).to eq(serialized_job)
13
+ end
14
+
15
+ it 'should remove dead jobs older than Sidekiq::DeadSet.timeout' do
16
+ allow(Sidekiq::DeadSet).to receive(:timeout).and_return(10)
17
+ time_now = Time.now
18
+
19
+ stub_time_now(time_now - 11)
20
+ dead_set.kill(Sidekiq.dump_json({jid: '000103', class: 'MyWorker3', args: []})) # the oldest
21
+
22
+ stub_time_now(time_now - 9)
23
+ dead_set.kill(Sidekiq.dump_json({jid: '000102', class: 'MyWorker2', args: []}))
24
+
25
+ stub_time_now(time_now)
26
+ dead_set.kill(Sidekiq.dump_json({jid: '000101', class: 'MyWorker1', args: []}))
27
+
28
+ stub_time_now(time_now)
29
+
30
+ expect(dead_set.find_job('000103')).to be_falsey
31
+ expect(dead_set.find_job('000102')).to be_truthy
32
+ expect(dead_set.find_job('000101')).to be_truthy
33
+ end
34
+
35
+ it 'should remove all but last Sidekiq::DeadSet.max_jobs-1 jobs' do
36
+ allow(Sidekiq::DeadSet).to receive(:max_jobs).and_return(3)
37
+
38
+ dead_set.kill(Sidekiq.dump_json({jid: '000101', class: 'MyWorker1', args: []}))
39
+ dead_set.kill(Sidekiq.dump_json({jid: '000102', class: 'MyWorker2', args: []}))
40
+ dead_set.kill(Sidekiq.dump_json({jid: '000103', class: 'MyWorker3', args: []}))
41
+
42
+ expect(dead_set.find_job('000101')).to be_falsey
43
+ expect(dead_set.find_job('000102')).to be_truthy
44
+ expect(dead_set.find_job('000103')).to be_truthy
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe Sidekiq::Killswitch do
5
+ # !!!!! HERE BE DRAGONS !!!!!
6
+ # Make sure to return test config to the working state after testing ...
7
+ # ... consider using Rspec.around wrapper:
8
+ #
9
+ # around do |example|
10
+ # original_test_logger = Sidekiq::Killswitch.logger
11
+ # example.run
12
+ # Sidekiq::Killswitch.logger = original_test_logger
13
+ # end
14
+
15
+ around do |example|
16
+ original_test_logger = Sidekiq::Killswitch.config.logger
17
+
18
+ example.run
19
+
20
+ Sidekiq::Killswitch.config.logger = original_test_logger
21
+ end
22
+
23
+ let(:worker_name) { 'SomeWorker' }
24
+
25
+ describe '.configure' do
26
+ describe '.logger= ' do
27
+ it 'should allow to set the logger' do
28
+ logger = double
29
+
30
+ Sidekiq::Killswitch.configure do |config|
31
+ config.logger = logger
32
+ end
33
+
34
+ expect(Sidekiq::Killswitch.logger).to eq(logger)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '.blackhole_add_worker' do
40
+ it 'should mark a worker as blackholed in Redis' do
41
+ time_now = stub_time_now
42
+
43
+ Sidekiq::Killswitch.blackhole_add_worker(worker_name)
44
+
45
+ Sidekiq::Killswitch.redis_pool do |redis|
46
+ expect(redis.hget(Sidekiq::Killswitch::BLACKHOLE_WORKERS_KEY_NAME, worker_name)).to eq(time_now.to_s)
47
+ end
48
+ end
49
+
50
+ it 'should accept class object as a parameter' do
51
+ time_now = stub_time_now
52
+ stub_const('AnotherWorker', Class.new {})
53
+
54
+ Sidekiq::Killswitch.blackhole_add_worker(AnotherWorker)
55
+
56
+ Sidekiq::Killswitch.redis_pool do |redis|
57
+ expect(redis.hget(Sidekiq::Killswitch::BLACKHOLE_WORKERS_KEY_NAME, 'AnotherWorker')).to eq(time_now.to_s)
58
+ end
59
+ end
60
+ end
61
+
62
+ describe '.blackhole_remove_worker' do
63
+ it 'should remove worker from the list of blackholed workers' do
64
+ Sidekiq::Killswitch.blackhole_add_worker(worker_name)
65
+ Sidekiq::Killswitch.blackhole_remove_worker(worker_name)
66
+
67
+ Sidekiq::Killswitch.redis_pool do |redis|
68
+ expect(redis.hexists(Sidekiq::Killswitch::BLACKHOLE_WORKERS_KEY_NAME, worker_name)).to be_falsey
69
+ end
70
+ end
71
+ end
72
+
73
+ describe '.blackhole_worker?' do
74
+ it 'should return true for blackholed workers' do
75
+ Sidekiq::Killswitch.blackhole_add_worker(worker_name)
76
+ expect(Sidekiq::Killswitch.blackhole_worker?(worker_name)).to be_truthy
77
+ end
78
+
79
+ it 'should return false for non-blackholed workers' do
80
+ expect(Sidekiq::Killswitch.blackhole_worker?(worker_name)).to be_falsey
81
+ end
82
+ end
83
+
84
+ describe '.blackhole_workers' do
85
+ it 'should return a list of all blackholed workers with "added at" timestamps' do
86
+ first_worker_added_at = stub_time_now
87
+ Sidekiq::Killswitch.blackhole_add_worker('FirstWorker')
88
+ second_worker_added_at = stub_time_now(first_worker_added_at + 1)
89
+ Sidekiq::Killswitch.blackhole_add_worker('SecondWorker')
90
+
91
+ expect(Sidekiq::Killswitch.blackhole_workers).to eq({
92
+ 'FirstWorker' => first_worker_added_at.to_s,
93
+ 'SecondWorker' => second_worker_added_at.to_s
94
+ })
95
+ end
96
+ end
97
+
98
+ describe '.dead_queue_add_worker' do
99
+ it 'should mark a worker as a "dead queue worker" in Redis' do
100
+ time_now = stub_time_now
101
+
102
+ Sidekiq::Killswitch.dead_queue_add_worker(worker_name)
103
+
104
+ Sidekiq::Killswitch.redis_pool do |redis|
105
+ expect(redis.hget(Sidekiq::Killswitch::DEAD_QUEUE_WORKERS_KEY_NAME, worker_name)).to eq(time_now.to_s)
106
+ end
107
+ end
108
+ end
109
+
110
+ describe '.dead_queue_remove_worker' do
111
+ it 'should remove worker from the list of dead queue workers' do
112
+ Sidekiq::Killswitch.dead_queue_add_worker(worker_name)
113
+ Sidekiq::Killswitch.dead_queue_remove_worker(worker_name)
114
+
115
+ Sidekiq::Killswitch.redis_pool do |redis|
116
+ expect(redis.hexists(Sidekiq::Killswitch::DEAD_QUEUE_WORKERS_KEY_NAME, worker_name)).to be_falsey
117
+ end
118
+ end
119
+ end
120
+
121
+ describe '.dead_queue_worker?' do
122
+ it 'should return true for dead queue workers' do
123
+ Sidekiq::Killswitch.dead_queue_add_worker(worker_name)
124
+ expect(Sidekiq::Killswitch.dead_queue_worker?(worker_name)).to be_truthy
125
+ end
126
+
127
+ it 'should return false for non-dead-queue workers' do
128
+ expect(Sidekiq::Killswitch.dead_queue_worker?(worker_name)).to be_falsey
129
+ end
130
+ end
131
+
132
+ describe '.dead_queue_workers' do
133
+ it 'should return a list of all dead queue workers with "added at" timestamps' do
134
+ first_worker_added_at = stub_time_now
135
+ Sidekiq::Killswitch.dead_queue_add_worker('FirstWorker')
136
+ second_worker_added_at = stub_time_now(first_worker_added_at + 1)
137
+ Sidekiq::Killswitch.dead_queue_add_worker('SecondWorker')
138
+
139
+ expect(Sidekiq::Killswitch.dead_queue_workers).to eq({
140
+ 'FirstWorker' => first_worker_added_at.to_s,
141
+ 'SecondWorker' => second_worker_added_at.to_s
142
+ })
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe Sidekiq::Killswitch::Middleware::Client do
5
+ let(:client_middleware) { Sidekiq::Killswitch::Middleware::Client.new }
6
+
7
+ describe '#call' do
8
+ it 'should return false for blackholed workers' do
9
+ stub_const('DisabledWorker', Class.new {})
10
+
11
+ Sidekiq::Killswitch.blackhole_add_worker(DisabledWorker)
12
+ Sidekiq::Killswitch.blackhole_add_worker('MyWorker')
13
+
14
+ expect(client_middleware.call(DisabledWorker, {}, nil, nil) { 123 }).to eq(false)
15
+ expect(client_middleware.call('MyWorker', {}, nil, nil) { 123 }).to eq(false)
16
+ end
17
+
18
+ it 'should return block result for not blockholed workers' do
19
+ stub_const('EnabledWorker', Class.new {})
20
+
21
+ expect(client_middleware.call(EnabledWorker, {}, nil, nil) { 'result' }).to eq('result')
22
+ expect(client_middleware.call('EnabledWorker', {}, nil, nil) { 123 }).to eq(123)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe Sidekiq::Killswitch::Middleware::Server do
5
+ let(:server_middleware) { Sidekiq::Killswitch::Middleware::Server.new }
6
+
7
+ before do
8
+ stub_const('MyWorker', Class.new {
9
+ include Sidekiq::Worker
10
+ def perform; end
11
+ })
12
+ end
13
+
14
+ describe '#call' do
15
+ context 'for blackholed workers' do
16
+ it 'should not run block' do
17
+ Sidekiq::Killswitch.blackhole_add_worker(MyWorker)
18
+
19
+ expect do
20
+ server_middleware.call(MyWorker.new, {}, nil) { raise 'Should not run' }
21
+ end.to_not raise_error
22
+ end
23
+ end
24
+
25
+ context 'for dead-queued workers' do
26
+ it 'should send a job to the morgue' do
27
+ Sidekiq::Killswitch.dead_queue_add_worker(MyWorker)
28
+
29
+ job_data = double
30
+ expect_any_instance_of(Sidekiq::DeadSet).to receive(:kill).with(job_data)
31
+
32
+ expect do
33
+ server_middleware.call(MyWorker.new, job_data, nil) { raise 'Should not run' }
34
+ end.to_not raise_error
35
+ end
36
+ end
37
+
38
+ context 'for non-marked workers' do
39
+ it 'should run passed block' do
40
+ checkpoint = double
41
+ expect(checkpoint).to receive(:check)
42
+ server_middleware.call(MyWorker.new, {}, nil) { checkpoint.check }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ require 'rspec'
3
+ require 'logger'
4
+ require 'sidekiq/testing'
5
+ require 'sidekiq/killswitch'
6
+
7
+ ENV['RACK_ENV'] = 'test' # Disable CSRF protection for Sidekiq Web app
8
+
9
+ Sidekiq::Killswitch.configure do |config|
10
+ config.logger = Logger.new('/dev/null')
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+ config.disable_monkey_patching!
15
+ config.before do
16
+ Sidekiq::Killswitch.redis_pool { |c| c.flushdb }
17
+ end
18
+ end
19
+
20
+ def stub_time_now(time_now = Time.now)
21
+ allow(Time).to receive(:now).and_return(time_now)
22
+ time_now
23
+ end
data/spec/web_spec.rb ADDED
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'rack/test'
4
+ require 'rspec-html-matchers'
5
+ require 'sidekiq/killswitch/web'
6
+
7
+ RSpec.describe Sidekiq::Killswitch::Web do
8
+ include Rack::Test::Methods
9
+ include RSpecHtmlMatchers
10
+
11
+ let(:app) { Sidekiq::Web }
12
+
13
+ def expect_redirect_to_root_page(response)
14
+ expect(response.status).to be(302)
15
+ expect(response.headers['Location']).to eq("http://#{rack_mock_session.default_host}/kill-switches")
16
+ end
17
+
18
+ describe 'GET /kill-switches' do
19
+ it 'should list blackholed workers' do
20
+ Sidekiq::Killswitch.blackhole_add_worker('WorkerOne')
21
+ Sidekiq::Killswitch.blackhole_add_worker('WorkerTwo')
22
+
23
+ response = get('/kill-switches')
24
+ expect(response.status).to be(200)
25
+
26
+ expect(response.body).to have_tag('.blackhole-workers') do
27
+ with_text('WorkerOne')
28
+ with_text('WorkerTwo')
29
+ end
30
+ end
31
+
32
+ it 'should list dead-queued workers' do
33
+ Sidekiq::Killswitch.dead_queue_add_worker('DeadWorkerOne')
34
+ Sidekiq::Killswitch.dead_queue_add_worker('DeadWorkerTwo')
35
+
36
+ response = get('/kill-switches')
37
+
38
+ expect(response.body).to have_tag('.dead-queue-workers') do
39
+ with_text('DeadWorkerOne')
40
+ with_text('DeadWorkerTwo')
41
+ end
42
+ end
43
+ end
44
+
45
+ describe 'POST /kill-switches/blackhole_add' do
46
+ it 'should blackhole passed worker' do
47
+ response = post('/kill-switches/blackhole_add', worker_name: 'BlackholedWorker')
48
+
49
+ expect_redirect_to_root_page(response)
50
+ expect(Sidekiq::Killswitch.blackhole_worker?('BlackholedWorker')).to be_truthy
51
+ end
52
+
53
+ describe 'validation' do
54
+ around do |example|
55
+ default_validator = Sidekiq::Killswitch.config.web_ui_worker_validator
56
+ example.run
57
+ Sidekiq::Killswitch.config.web_ui_worker_validator = default_validator
58
+ end
59
+
60
+ it 'should perform basic default validation' do
61
+ post('/kill-switches/blackhole_add', worker_name: '')
62
+ follow_redirect!
63
+
64
+ expect(last_response.body).to have_tag('.error-message', text: 'Error: Invalid worker name!')
65
+ end
66
+
67
+ it 'should perform custom validation' do
68
+ Sidekiq::Killswitch.config.web_ui_worker_validator = ->(name) { name.end_with?('BlockedWorker') }
69
+
70
+ post('/kill-switches/blackhole_add', worker_name: 'BadWorker')
71
+ follow_redirect!
72
+ expect(last_response.body).to have_tag('.error-message', text: 'Error: Invalid worker name!')
73
+
74
+ post('/kill-switches/blackhole_add', worker_name: 'MyBlockedWorker')
75
+ expect(Sidekiq::Killswitch.blackhole_worker?('MyBlockedWorker')).to be_truthy
76
+ end
77
+ end
78
+ end
79
+
80
+ describe 'POST /kill-switches/blackhole_remove' do
81
+ it 'should remove worker from blackhole' do
82
+ Sidekiq::Killswitch.blackhole_add_worker('BlakcholedWorker')
83
+
84
+ response = post('/kill-switches/blackhole_remove', worker_name: 'BlackholedWorker')
85
+
86
+ expect_redirect_to_root_page(response)
87
+ expect(Sidekiq::Killswitch.blackhole_worker?('BlackholedWorker')).to be_falsey
88
+ end
89
+ end
90
+
91
+ describe 'POST /kill-switches/dead_queue_add' do
92
+ it 'should add passed worker to dead queue' do
93
+ response = post('/kill-switches/dead_queue_add', worker_name: 'DeadWorker')
94
+
95
+ expect_redirect_to_root_page(response)
96
+ expect(Sidekiq::Killswitch.dead_queue_worker?('DeadWorker')).to be_truthy
97
+ end
98
+ end
99
+
100
+ describe 'POST /kill-switches/dead_queue_remove' do
101
+ it 'should remove worker from dead queue' do
102
+ Sidekiq::Killswitch.dead_queue_add_worker('DeadWorker')
103
+
104
+ response = post('/kill-switches/dead_queue_remove', worker_name: 'DeadWorker')
105
+
106
+ expect_redirect_to_root_page(response)
107
+ expect(Sidekiq::Killswitch.dead_queue_worker?('DeadWorker')).to be_falsey
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,15 @@
1
+ en:
2
+ kill_switches: Kill Switches
3
+ blackholed_workers: Blackholed Workers
4
+ dead_queue_workers: Dead Queue Workers
5
+ are_you_sure: Are you sure?
6
+ blackhole: Blackhole
7
+ disabled_at: Disabled at
8
+ dead_queued_at: Dead-queued at
9
+ blackholed_at: Blackholed at
10
+ send_to_dead_queue: Send to dead queue
11
+ class: Class
12
+ kill: Kill
13
+ resurrect: Resurrect
14
+ resurrect_worker: Resurrect %{worker}?
15
+ invalid_worker_error: "Error: Invalid worker name!"
@@ -0,0 +1,75 @@
1
+ <% if @worker_name_invalid %>
2
+ <h4 class="error-message alert alert-danger"><%= t('invalid_worker_error')%></h4>
3
+ <% end %>
4
+
5
+ <h3><%= t('blackholed_workers')%></h3>
6
+
7
+ <form action="<%= root_path %>kill-switches/blackhole_add" method="post" style="margin-bottom: 4px;">
8
+ <%= csrf_tag %>
9
+ <input name="worker_name" />
10
+ <input class="btn btn-danger btn-xs" type="submit" value="<%= t('blackhole')%>" data-confirm="<%= t('are_you_sure')%>" />
11
+ </form>
12
+
13
+ <div class="table_container">
14
+ <table class="blackhole-workers table table-hover table-bordered table-striped table-white">
15
+ <thead>
16
+ <tr>
17
+ <th><%= t('class')%></th>
18
+ <th style="width: 100%;"><%= t('blackholed_at')%></th>
19
+ <th></th>
20
+ </tr>
21
+ </thead>
22
+
23
+ <tbody>
24
+ <% @blackhole_workers.each do |worker_name, disabled_at| %>
25
+ <tr>
26
+ <td><%= worker_name %></td>
27
+ <td><%= disabled_at %></td>
28
+ <td class="text-center">
29
+ <form action="<%= root_path %>kill-switches/blackhole_remove" method="post">
30
+ <%= csrf_tag %>
31
+ <input type="hidden" name="worker_name" value="<%= worker_name %>" />
32
+ <input class="btn btn-primary btn-xs pull-right" type="submit" value="<%= t('restore')%>" data-confirm="<%= t('restore_worker', worker: worker_name) %>" />
33
+ </form>
34
+ </td>
35
+ </tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+ </div>
40
+
41
+ <h3><%= t('dead_queue_workers') %></h3>
42
+
43
+ <form action="<%= root_path %>kill-switches/dead_queue_add" method="post" style="margin-bottom: 4px;">
44
+ <%= csrf_tag %>
45
+ <input name="worker_name" />
46
+ <input class="btn btn-danger btn-xs" type="submit" value="<%= t('send_to_dead_queue') %>" data-confirm="<%= t('are_you_sure')%>" />
47
+ </form>
48
+
49
+ <div class="table_container">
50
+ <table class="dead-queue-workers table table-hover table-bordered table-striped table-white">
51
+ <thead>
52
+ <tr>
53
+ <th><%= t('class')%></th>
54
+ <th style="width: 100%;"><%= t('dead_queued_at')%></th>
55
+ <th></th>
56
+ </tr>
57
+ </thead>
58
+
59
+ <tbody>
60
+ <% @dead_queue_workers.each do |worker_name, disabled_at| %>
61
+ <tr>
62
+ <td><%= worker_name %></td>
63
+ <td><%= disabled_at %></td>
64
+ <td class="text-center">
65
+ <form action="<%= root_path %>kill-switches/dead_queue_remove" method="post">
66
+ <%= csrf_tag %>
67
+ <input type="hidden" name="worker_name" value="<%= worker_name %>" />
68
+ <input class="btn btn-primary btn-xs pull-right" type="submit" value="<%= t('resurrect')%>" data-confirm="<%= t('resurrect_worker', worker: worker_name) %>" />
69
+ </form>
70
+ </td>
71
+ </tr>
72
+ <% end %>
73
+ </tbody>
74
+ </table>
75
+ </div>