sidekiq-killswitch 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>