sidekiq-worker-killer 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +17 -6
- data/lib/sidekiq/worker_killer/version.rb +1 -1
- data/lib/sidekiq/worker_killer.rb +12 -2
- data/spec/sidekiq/worker_killer_spec.rb +166 -109
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8573256c162630662fbb9be660375da63c3c8cbe3fb11d9851e36b1c0456eff
|
4
|
+
data.tar.gz: 63a6bea822d499f22f283f704f80e0954ac006ed442f6cb7632fa204602051f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1fe7ab3d39860cbd742af569a3e5b3a37173bbdffa751ebc76aae1cbf67d09a5745991e8ad9c5b50671705b110abeab3ee4124e69c3ebbccc658579409cbd6d
|
7
|
+
data.tar.gz: f65acd7d78a86152a2d04cd5c4ef0046885366540c362fe0a8c61caa4f1d04d4263f338bd9769c00ea09530151b9a8da48f34cf71377ecfec6517c4c36075d80
|
data/README.md
CHANGED
@@ -5,7 +5,18 @@
|
|
5
5
|
|
6
6
|
[Sidekiq](https://github.com/mperham/sidekiq) is probably the best background processing framework today. At the same time, memory leaks are very hard to tackle in Ruby and we often find ourselves with growing memory consumption. Instead of spending herculean effort fixing leaks, why not kill your processes when they got to be too large?
|
7
7
|
|
8
|
-
Highly inspired by [Gitlab Sidekiq MemoryKiller](https://gitlab.com/gitlab-org/gitlab-
|
8
|
+
Highly inspired by [Gitlab Sidekiq MemoryKiller](https://gitlab.com/gitlab-org/gitlab-foss/-/blob/39c1731a53d1014eab7c876d70632b1abf738712/lib/gitlab/sidekiq_middleware/shutdown.rb) and [Noxa Sidekiq killer](https://github.com/Noxa/sidekiq-killer).
|
9
|
+
|
10
|
+
---
|
11
|
+
**NOTE**
|
12
|
+
|
13
|
+
This gem needs to get the used memory of the Sidekiq process. For
|
14
|
+
this we use [GetProcessGem](https://github.com/schneems/get_process_mem),
|
15
|
+
but be aware that if you are running Sidekiq in Heroku(or any container) the
|
16
|
+
memory usage will
|
17
|
+
[not be accurate](https://github.com/schneems/get_process_mem/issues/7).
|
18
|
+
|
19
|
+
---
|
9
20
|
|
10
21
|
quick-refs: [install](#install) | [usage](#usage) | [available options](#available-options) | [development](#development)
|
11
22
|
|
@@ -31,13 +42,13 @@ end
|
|
31
42
|
|
32
43
|
## Available options
|
33
44
|
|
34
|
-
The following options can be
|
45
|
+
The following options can be overridden.
|
35
46
|
|
36
47
|
| Option | Defaults | Description |
|
37
48
|
| ------- | ------- | ----------- |
|
38
|
-
| max_rss | 0 MB (disabled) |
|
39
|
-
| grace_time | 900 seconds |
|
40
|
-
| shutdown_wait | 30 seconds |
|
49
|
+
| max_rss | 0 MB (disabled) | Max RSS in megabytes used by the Sidekiq process. Above this, shutdown will be triggered. |
|
50
|
+
| grace_time | 900 seconds | When shutdown is triggered, the Sidekiq process will not accept new job and wait at most 15 minutes for running jobs to finish. If Float::INFINITY specified, will wait forever. |
|
51
|
+
| shutdown_wait | 30 seconds | When the grace time expires, still running jobs get 30 seconds to stop. After that, kill signal is triggered. |
|
41
52
|
| kill_signal | SIGKILL | Signal to use to kill Sidekiq process if it doesn't stop. |
|
42
53
|
| gc | true | Try to run garbage collection before Sidekiq process stops in case of exceeded max_rss. |
|
43
54
|
| skip_shutdown_if | proc {false} | Executes a block of code after max_rss exceeds but before requesting shutdown. |
|
@@ -75,4 +86,4 @@ See the list of [contributors](https://github.com/klaxit/sidekiq-worker-killer/c
|
|
75
86
|
|
76
87
|
## License
|
77
88
|
|
78
|
-
Please see LICENSE
|
89
|
+
Please see [LICENSE](https://github.com/klaxit/sidekiq-worker-killer/blob/master/LICENSE)
|
@@ -1,12 +1,18 @@
|
|
1
1
|
require "get_process_mem"
|
2
2
|
require "sidekiq"
|
3
|
-
|
3
|
+
begin
|
4
|
+
require "sidekiq/util"
|
5
|
+
SidekiqComponent = Sidekiq::Util
|
6
|
+
rescue LoadError
|
7
|
+
require "sidekiq/middleware/modules"
|
8
|
+
SidekiqComponent = Sidekiq::ServerMiddleware
|
9
|
+
end
|
4
10
|
require "sidekiq/api"
|
5
11
|
|
6
12
|
# Sidekiq server middleware. Kill worker when the RSS memory exceeds limit
|
7
13
|
# after a given grace time.
|
8
14
|
class Sidekiq::WorkerKiller
|
9
|
-
include
|
15
|
+
include SidekiqComponent
|
10
16
|
|
11
17
|
MUTEX = Mutex.new
|
12
18
|
|
@@ -125,6 +131,10 @@ class Sidekiq::WorkerKiller
|
|
125
131
|
end || raise("No sidekiq worker with identity #{identity} found")
|
126
132
|
end
|
127
133
|
|
134
|
+
def identity
|
135
|
+
config[:identity] || config["identity"]
|
136
|
+
end unless method_defined?(:identity)
|
137
|
+
|
128
138
|
def warn(msg)
|
129
139
|
Sidekiq.logger.warn(msg)
|
130
140
|
end
|
@@ -7,156 +7,213 @@ describe Sidekiq::WorkerKiller do
|
|
7
7
|
before do
|
8
8
|
allow(subject).to receive(:warn) # silence "warn" logs
|
9
9
|
allow(subject).to receive(:sleep) # reduces tests running time
|
10
|
-
allow(Sidekiq::ProcessSet).to receive(:new) { sidekiq_process_set }
|
11
|
-
allow(sidekiq_process_set).to receive(:find) { sidekiq_process }
|
12
10
|
allow(sidekiq_process).to receive(:quiet!)
|
13
11
|
allow(sidekiq_process).to receive(:stop!)
|
14
12
|
allow(sidekiq_process).to receive(:stopping?)
|
15
13
|
end
|
16
14
|
|
17
|
-
describe "#
|
18
|
-
let(:
|
19
|
-
|
20
|
-
|
15
|
+
describe "#sidekiq_process" do
|
16
|
+
let(:identity) do
|
17
|
+
"foobar"
|
18
|
+
end
|
19
|
+
let(:mock_processes) do
|
20
|
+
[{
|
21
|
+
"identity" => identity,
|
22
|
+
"tag" => "baz",
|
23
|
+
"started_at" => Time.now,
|
24
|
+
"concurrency" => 5,
|
25
|
+
"busy" => 2,
|
26
|
+
"queues" => %w[low medium high]
|
27
|
+
}]
|
28
|
+
end
|
29
|
+
it "finds the process by identity" do
|
30
|
+
if subject.respond_to?(:config=)
|
31
|
+
rspec_info "variant 6.5+: Sidekiq::ServerMiddleware"
|
32
|
+
subject.config = mock_processes.first
|
33
|
+
else
|
34
|
+
rspec_info "variant pre 6.5: Sidekiq::Util"
|
35
|
+
allow(subject).to receive(:identity).and_return(identity)
|
36
|
+
end
|
37
|
+
allow(Sidekiq).to receive(:redis)
|
38
|
+
sidekiq_process_set = Sidekiq::ProcessSet.new
|
39
|
+
sidekiq_mock_processes = mock_processes.map {|mock_process|
|
40
|
+
Sidekiq::Process.new(mock_process)
|
41
|
+
}
|
42
|
+
allow(sidekiq_process_set).to receive(:each) {|&block|
|
43
|
+
sidekiq_mock_processes.each(&block)
|
44
|
+
}
|
45
|
+
expect(sidekiq_process_set.to_a.map(&:identity)).to eq([identity])
|
46
|
+
allow(Sidekiq::ProcessSet).to receive(:new).and_return(sidekiq_process_set)
|
47
|
+
expect(subject.send(:identity)).to eq(identity)
|
48
|
+
expect(subject.send(:sidekiq_process)).to be_a(Sidekiq::Process)
|
49
|
+
expect(subject.send(:sidekiq_process).identity).to eq(identity)
|
50
|
+
end
|
51
|
+
end
|
21
52
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
53
|
+
context "with stubbed sidekiq_process" do
|
54
|
+
before do
|
55
|
+
allow(Sidekiq::ProcessSet).to receive(:new) { sidekiq_process_set }
|
56
|
+
allow(sidekiq_process_set).to receive(:find) { sidekiq_process }
|
57
|
+
identity = "some-identity"
|
58
|
+
if subject.respond_to?(:config=)
|
59
|
+
rspec_info "variant 6.5+: Sidekiq::ServerMiddleware"
|
60
|
+
subject.config = { "identity" => identity }
|
61
|
+
else
|
62
|
+
rspec_info "variant pre 6.5: Sidekiq::Util"
|
63
|
+
allow(subject).to receive(:identity).and_return(identity)
|
64
|
+
end
|
26
65
|
end
|
27
66
|
|
28
|
-
|
29
|
-
|
67
|
+
describe "#call" do
|
68
|
+
let(:worker){ double("worker") }
|
69
|
+
let(:job){ double("job") }
|
70
|
+
let(:queue){ double("queue") }
|
30
71
|
|
31
|
-
|
32
|
-
|
72
|
+
it "should yield" do
|
73
|
+
expect { |b|
|
74
|
+
subject.call(worker, job, queue, &b)
|
75
|
+
}.to yield_with_no_args
|
33
76
|
end
|
34
77
|
|
35
|
-
|
36
|
-
|
37
|
-
subject.call(worker, job, queue){}
|
38
|
-
end
|
78
|
+
context "when current rss is over max rss" do
|
79
|
+
subject{ described_class.new(max_rss: 2) }
|
39
80
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
81
|
+
before do
|
82
|
+
allow(subject).to receive(:current_rss).and_return(3)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should request shutdown" do
|
86
|
+
expect(subject).to receive(:request_shutdown)
|
87
|
+
subject.call(worker, job, queue){}
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should call garbage collect" do
|
91
|
+
allow(subject).to receive(:request_shutdown)
|
92
|
+
expect(GC).to receive(:start).with(full_mark: true, immediate_sweep: true)
|
93
|
+
subject.call(worker, job, queue){}
|
94
|
+
end
|
45
95
|
|
46
|
-
|
47
|
-
|
96
|
+
context "and skip_shutdown_if is given" do
|
97
|
+
subject{ described_class.new(max_rss: 2, skip_shutdown_if: skip_shutdown_proc) }
|
48
98
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
99
|
+
context "and skip_shutdown_if is a proc" do
|
100
|
+
let(:skip_shutdown_proc) { proc { |worker| true } }
|
101
|
+
it "should NOT request shutdown" do
|
102
|
+
expect(subject).not_to receive(:request_shutdown)
|
103
|
+
subject.call(worker, job, queue){}
|
104
|
+
end
|
54
105
|
end
|
55
|
-
end
|
56
106
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
107
|
+
context "and skip_shutdown_if is a lambda" do
|
108
|
+
let(:skip_shutdown_proc) { ->(worker, job, queue) { true } }
|
109
|
+
it "should NOT request shutdown" do
|
110
|
+
expect(subject).not_to receive(:request_shutdown)
|
111
|
+
subject.call(worker, job, queue){}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "and skip_shutdown_if returns false" do
|
116
|
+
let(:skip_shutdown_proc) { proc { |worker, job, queue| false } }
|
117
|
+
it "should still request shutdown" do
|
118
|
+
expect(subject).to receive(:request_shutdown)
|
119
|
+
subject.call(worker, job, queue){}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "and skip_shutdown_if returns nil" do
|
124
|
+
let(:skip_shutdown_proc) { proc { |worker, job, queue| nil } }
|
125
|
+
it "should still request shutdown" do
|
126
|
+
expect(subject).to receive(:request_shutdown)
|
127
|
+
subject.call(worker, job, queue){}
|
128
|
+
end
|
62
129
|
end
|
63
130
|
end
|
64
131
|
|
65
|
-
context "
|
66
|
-
|
67
|
-
it "should
|
68
|
-
|
132
|
+
context "when gc is false" do
|
133
|
+
subject{ described_class.new(max_rss: 2, gc: false) }
|
134
|
+
it "should not call garbage collect" do
|
135
|
+
allow(subject).to receive(:request_shutdown)
|
136
|
+
expect(GC).not_to receive(:start)
|
69
137
|
subject.call(worker, job, queue){}
|
70
138
|
end
|
71
139
|
end
|
72
140
|
|
73
|
-
context "
|
74
|
-
|
75
|
-
it "should
|
76
|
-
expect(subject).
|
141
|
+
context "but max rss is 0" do
|
142
|
+
subject{ described_class.new(max_rss: 0) }
|
143
|
+
it "should not request shutdown" do
|
144
|
+
expect(subject).to_not receive(:request_shutdown)
|
77
145
|
subject.call(worker, job, queue){}
|
78
146
|
end
|
79
147
|
end
|
80
148
|
end
|
149
|
+
end
|
81
150
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
expect(
|
87
|
-
subject.
|
151
|
+
describe "#request_shutdown" do
|
152
|
+
context "grace time is default" do
|
153
|
+
before { allow(subject).to receive(:shutdown){ sleep 0.01 } }
|
154
|
+
it "should call shutdown" do
|
155
|
+
expect(subject).to receive(:shutdown)
|
156
|
+
subject.send(:request_shutdown).join
|
88
157
|
end
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
subject{ described_class.new(max_rss: 0) }
|
93
|
-
it "should not request shutdown" do
|
94
|
-
expect(subject).to_not receive(:request_shutdown)
|
95
|
-
subject.call(worker, job, queue){}
|
158
|
+
it "should not call shutdown twice when called concurrently" do
|
159
|
+
expect(subject).to receive(:shutdown).once
|
160
|
+
2.times.map{ subject.send(:request_shutdown) }.each(&:join)
|
96
161
|
end
|
97
162
|
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
describe "#request_shutdown" do
|
102
|
-
context "grace time is default" do
|
103
|
-
before { allow(subject).to receive(:shutdown){ sleep 0.01 } }
|
104
|
-
it "should call shutdown" do
|
105
|
-
expect(subject).to receive(:shutdown)
|
106
|
-
subject.send(:request_shutdown).join
|
107
|
-
end
|
108
|
-
it "should not call shutdown twice when called concurrently" do
|
109
|
-
expect(subject).to receive(:shutdown).once
|
110
|
-
2.times.map{ subject.send(:request_shutdown) }.each(&:join)
|
111
|
-
end
|
112
|
-
end
|
113
163
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
164
|
+
context "grace time is 5 seconds" do
|
165
|
+
subject{ described_class.new(max_rss: 2, grace_time: 5.0, shutdown_wait: 0) }
|
166
|
+
it "should wait the specified grace time before calling shutdown" do
|
167
|
+
# there are busy jobs that will not terminate within the grace time
|
168
|
+
allow(subject).to receive(:no_jobs_on_quiet_processes?).and_return(false)
|
169
|
+
|
170
|
+
shutdown_request_time = nil
|
171
|
+
shutdown_time = nil
|
172
|
+
|
173
|
+
# replace the original #request_shutdown to track
|
174
|
+
# when the shutdown is requested
|
175
|
+
original_request_shutdown = subject.method(:request_shutdown)
|
176
|
+
allow(subject).to receive(:request_shutdown) do
|
177
|
+
shutdown_request_time= Time.now
|
178
|
+
original_request_shutdown.call
|
179
|
+
end
|
130
180
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
181
|
+
# track when the process has been required to stop
|
182
|
+
expect(sidekiq_process).to receive(:stop!) do |*args|
|
183
|
+
shutdown_time = Time.now
|
184
|
+
end
|
135
185
|
|
136
|
-
|
137
|
-
|
186
|
+
allow(Process).to receive(:kill)
|
187
|
+
allow(Process).to receive(:pid).and_return(99)
|
138
188
|
|
139
|
-
|
189
|
+
subject.send(:request_shutdown).join
|
140
190
|
|
141
|
-
|
191
|
+
elapsed_time = shutdown_time - shutdown_request_time
|
142
192
|
|
143
|
-
|
144
|
-
|
145
|
-
|
193
|
+
# the elapsed time between shutdown request and the actual
|
194
|
+
# shutdown signal should be greater than the specified grace_time
|
195
|
+
expect(elapsed_time).to be >= 5.0
|
196
|
+
end
|
146
197
|
end
|
147
|
-
end
|
148
198
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
199
|
+
context "grace time is Float::INFINITY" do
|
200
|
+
subject{ described_class.new(max_rss: 2, grace_time: Float::INFINITY, shutdown_wait: 0) }
|
201
|
+
it "call signal only on jobs" do
|
202
|
+
allow(subject).to receive(:jobs_finished?).and_return(true)
|
203
|
+
allow(Process).to receive(:pid).and_return(99)
|
204
|
+
expect(sidekiq_process).to receive(:quiet!)
|
205
|
+
expect(sidekiq_process).to receive(:stop!)
|
206
|
+
expect(Process).to receive(:kill).with('SIGKILL', 99)
|
157
207
|
|
158
|
-
|
208
|
+
subject.send(:request_shutdown).join
|
209
|
+
end
|
159
210
|
end
|
160
211
|
end
|
161
212
|
end
|
213
|
+
|
214
|
+
def rspec_info(msg)
|
215
|
+
is_printing_progress = RSpec.configuration.formatters.any? { |f| /ProgressFormatter/.match?(f.class.name) }
|
216
|
+
return if is_printing_progress
|
217
|
+
RSpec.configuration.reporter.message msg
|
218
|
+
end
|
162
219
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidekiq-worker-killer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyrille Courtiere
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: get_process_mem
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 0.49.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: appraisal
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
description:
|
70
84
|
email:
|
71
85
|
- dev@klaxit.com
|
@@ -96,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
110
|
- !ruby/object:Gem::Version
|
97
111
|
version: '0'
|
98
112
|
requirements: []
|
99
|
-
rubygems_version: 3.
|
113
|
+
rubygems_version: 3.0.3
|
100
114
|
signing_key:
|
101
115
|
specification_version: 4
|
102
116
|
summary: Sidekiq worker killer
|