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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f40045c5805f35f8792f66c5dac896042c3b50b7b8c42f0f95e14827d4184a5a
4
- data.tar.gz: 5721815ad342ec06d6231c005b60cb0bbda5300343a4bfb99b9b138fff8f6627
3
+ metadata.gz: f8573256c162630662fbb9be660375da63c3c8cbe3fb11d9851e36b1c0456eff
4
+ data.tar.gz: 63a6bea822d499f22f283f704f80e0954ac006ed442f6cb7632fa204602051f3
5
5
  SHA512:
6
- metadata.gz: 4db165333c1a8465b15e514af6b178aec3091bdb90a2ced38b8131b98706922518b25632f45a65fa33b923ac6d2a42c3379dee4e4fde1dde9dd20d9a49aab1e1
7
- data.tar.gz: dd3e9e16d4bfbc5d753559e6b94c95b334ef2855864bcc7ef53f1577e40f6bc5122ad7040133c04d2c019ab37d532f012339fb18180e8efe74dc15274d427f49
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-ce/blob/master/lib/gitlab/sidekiq_middleware/shutdown.rb) and [Noxa Sidekiq killer](https://github.com/Noxa/sidekiq-killer).
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 overrided.
45
+ The following options can be overridden.
35
46
 
36
47
  | Option | Defaults | Description |
37
48
  | ------- | ------- | ----------- |
38
- | max_rss | 0 MB (disabled) | max RSS in megabytes. Above this, shutdown will be triggered. |
39
- | 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. |
40
- | shutdown_wait | 30 seconds | when the grace time expires, still running jobs get 30 seconds to stop. After that, kill signal is triggered. |
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,7 +1,7 @@
1
1
  # rubocop:disable Style/ClassAndModuleChildren
2
2
  module Sidekiq
3
3
  class WorkerKiller
4
- VERSION = "1.0.0".freeze
4
+ VERSION = "1.0.1".freeze
5
5
  end
6
6
  end
7
7
  # rubocop:enable Style/ClassAndModuleChildren
@@ -1,12 +1,18 @@
1
1
  require "get_process_mem"
2
2
  require "sidekiq"
3
- require "sidekiq/util"
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 Sidekiq::Util
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 "#call" do
18
- let(:worker){ double("worker") }
19
- let(:job){ double("job") }
20
- let(:queue){ double("queue") }
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
- it "should yield" do
23
- expect { |b|
24
- subject.call(worker, job, queue, &b)
25
- }.to yield_with_no_args
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
- context "when current rss is over max rss" do
29
- subject{ described_class.new(max_rss: 2) }
67
+ describe "#call" do
68
+ let(:worker){ double("worker") }
69
+ let(:job){ double("job") }
70
+ let(:queue){ double("queue") }
30
71
 
31
- before do
32
- allow(subject).to receive(:current_rss).and_return(3)
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
- it "should request shutdown" do
36
- expect(subject).to receive(:request_shutdown)
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
- it "should call garbage collect" do
41
- allow(subject).to receive(:request_shutdown)
42
- expect(GC).to receive(:start).with(full_mark: true, immediate_sweep: true)
43
- subject.call(worker, job, queue){}
44
- end
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
- context "and skip_shutdown_if is given" do
47
- subject{ described_class.new(max_rss: 2, skip_shutdown_if: skip_shutdown_proc) }
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
- context "and skip_shutdown_if is a proc" do
50
- let(:skip_shutdown_proc) { proc { |worker| true } }
51
- it "should NOT request shutdown" do
52
- expect(subject).not_to receive(:request_shutdown)
53
- subject.call(worker, job, queue){}
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
- context "and skip_shutdown_if is a lambda" do
58
- let(:skip_shutdown_proc) { ->(worker, job, queue) { true } }
59
- it "should NOT request shutdown" do
60
- expect(subject).not_to receive(:request_shutdown)
61
- subject.call(worker, job, queue){}
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 "and skip_shutdown_if returns false" do
66
- let(:skip_shutdown_proc) { proc { |worker, job, queue| false } }
67
- it "should still request shutdown" do
68
- expect(subject).to receive(:request_shutdown)
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 "and skip_shutdown_if returns nil" do
74
- let(:skip_shutdown_proc) { proc { |worker, job, queue| nil } }
75
- it "should still request shutdown" do
76
- expect(subject).to receive(:request_shutdown)
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
- context "when gc is false" do
83
- subject{ described_class.new(max_rss: 2, gc: false) }
84
- it "should not call garbage collect" do
85
- allow(subject).to receive(:request_shutdown)
86
- expect(GC).not_to receive(:start)
87
- subject.call(worker, job, queue){}
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
- end
90
-
91
- context "but max rss is 0" do
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
- context "grace time is 5 seconds" do
115
- subject{ described_class.new(max_rss: 2, grace_time: 5.0, shutdown_wait: 0) }
116
- it "should wait the specified grace time before calling shutdown" do
117
- # there are busy jobs that will not terminate within the grace time
118
- allow(subject).to receive(:no_jobs_on_quiet_processes?).and_return(false)
119
-
120
- shutdown_request_time = nil
121
- shutdown_time = nil
122
-
123
- # replace the original #request_shutdown to track
124
- # when the shutdown is requested
125
- original_request_shutdown = subject.method(:request_shutdown)
126
- allow(subject).to receive(:request_shutdown) do
127
- shutdown_request_time= Time.now
128
- original_request_shutdown.call
129
- end
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
- # track when the process has been required to stop
132
- expect(sidekiq_process).to receive(:stop!) do |*args|
133
- shutdown_time = Time.now
134
- end
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
- allow(Process).to receive(:kill)
137
- allow(Process).to receive(:pid).and_return(99)
186
+ allow(Process).to receive(:kill)
187
+ allow(Process).to receive(:pid).and_return(99)
138
188
 
139
- subject.send(:request_shutdown).join
189
+ subject.send(:request_shutdown).join
140
190
 
141
- elapsed_time = shutdown_time - shutdown_request_time
191
+ elapsed_time = shutdown_time - shutdown_request_time
142
192
 
143
- # the elapsed time beetween shutdown request and the actual
144
- # shutdown signal should be greater than the specificed grace_time
145
- expect(elapsed_time).to be >= 5.0
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
- context "grace time is Float::INFINITY" do
150
- subject{ described_class.new(max_rss: 2, grace_time: Float::INFINITY, shutdown_wait: 0) }
151
- it "call signal only on jobs" do
152
- allow(subject).to receive(:jobs_finished?).and_return(true)
153
- allow(Process).to receive(:pid).and_return(99)
154
- expect(sidekiq_process).to receive(:quiet!)
155
- expect(sidekiq_process).to receive(:stop!)
156
- expect(Process).to receive(:kill).with('SIGKILL', 99)
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
- subject.send(:request_shutdown).join
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.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: 2020-05-28 00:00:00.000000000 Z
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.1.0.pre3
113
+ rubygems_version: 3.0.3
100
114
  signing_key:
101
115
  specification_version: 4
102
116
  summary: Sidekiq worker killer