sidekiq-worker-killer 0.5.0 → 1.0.0
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.
- checksums.yaml +5 -5
- data/README.md +5 -5
- data/lib/sidekiq/worker_killer.rb +49 -36
- data/lib/sidekiq/worker_killer/version.rb +1 -1
- data/spec/sidekiq/worker_killer_spec.rb +22 -23
- metadata +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f40045c5805f35f8792f66c5dac896042c3b50b7b8c42f0f95e14827d4184a5a
|
4
|
+
data.tar.gz: 5721815ad342ec06d6231c005b60cb0bbda5300343a4bfb99b9b138fff8f6627
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4db165333c1a8465b15e514af6b178aec3091bdb90a2ced38b8131b98706922518b25632f45a65fa33b923ac6d2a42c3379dee4e4fde1dde9dd20d9a49aab1e1
|
7
|
+
data.tar.gz: dd3e9e16d4bfbc5d753559e6b94c95b334ef2855864bcc7ef53f1577e40f6bc5122ad7040133c04d2c019ab37d532f012339fb18180e8efe74dc15274d427f49
|
data/README.md
CHANGED
@@ -36,11 +36,11 @@ The following options can be overrided.
|
|
36
36
|
| Option | Defaults | Description |
|
37
37
|
| ------- | ------- | ----------- |
|
38
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
|
41
|
-
| kill_signal | SIGKILL | Signal to use kill Sidekiq process if it doesn't
|
42
|
-
| gc | true | Try to run garbage collection before Sidekiq process
|
43
|
-
| skip_shutdown_if |
|
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. |
|
41
|
+
| kill_signal | SIGKILL | Signal to use to kill Sidekiq process if it doesn't stop. |
|
42
|
+
| gc | true | Try to run garbage collection before Sidekiq process stops in case of exceeded max_rss. |
|
43
|
+
| skip_shutdown_if | proc {false} | Executes a block of code after max_rss exceeds but before requesting shutdown. |
|
44
44
|
|
45
45
|
*skip_shutdown_if* is expected to return anything other than `false` or `nil` to skip shutdown.
|
46
46
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require "get_process_mem"
|
2
2
|
require "sidekiq"
|
3
3
|
require "sidekiq/util"
|
4
|
+
require "sidekiq/api"
|
4
5
|
|
5
6
|
# Sidekiq server middleware. Kill worker when the RSS memory exceeds limit
|
6
7
|
# after a given grace time.
|
@@ -9,15 +10,43 @@ class Sidekiq::WorkerKiller
|
|
9
10
|
|
10
11
|
MUTEX = Mutex.new
|
11
12
|
|
13
|
+
# @param [Hash] options
|
14
|
+
# @option options [Integer] max_rss
|
15
|
+
# Max RSS in MB. Above this, shutdown will be triggered.
|
16
|
+
# (default: `0` (disabled))
|
17
|
+
# @option options [Integer] grace_time
|
18
|
+
# When shutdown is triggered, the Sidekiq process will not accept new job
|
19
|
+
# and wait at most 15 minutes for running jobs to finish.
|
20
|
+
# If Float::INFINITY is specified, will wait forever. (default: `900`)
|
21
|
+
# @option options [Integer] shutdown_wait
|
22
|
+
# when the grace time expires, still running jobs get 30 seconds to
|
23
|
+
# stop. After that, kill signal is triggered. (default: `30`)
|
24
|
+
# @option options [String] kill_signal
|
25
|
+
# Signal to use to kill Sidekiq process if it doesn't stop.
|
26
|
+
# (default: `"SIGKILL"`)
|
27
|
+
# @option options [Boolean] gc
|
28
|
+
# Try to run garbage collection before Sidekiq process stops in case
|
29
|
+
# of exceeded max_rss. (default: `true`)
|
30
|
+
# @option options [Proc] skip_shutdown_if
|
31
|
+
# Executes a block of code after max_rss exceeds but before requesting
|
32
|
+
# shutdown. (default: `proc {false}`)
|
12
33
|
def initialize(options = {})
|
13
34
|
@max_rss = options.fetch(:max_rss, 0)
|
14
35
|
@grace_time = options.fetch(:grace_time, 15 * 60)
|
15
36
|
@shutdown_wait = options.fetch(:shutdown_wait, 30)
|
16
37
|
@kill_signal = options.fetch(:kill_signal, "SIGKILL")
|
17
38
|
@gc = options.fetch(:gc, true)
|
18
|
-
@skip_shutdown = options.fetch(:skip_shutdown_if,
|
39
|
+
@skip_shutdown = options.fetch(:skip_shutdown_if, proc { false })
|
19
40
|
end
|
20
41
|
|
42
|
+
# @param [String, Class] worker_class
|
43
|
+
# the string or class of the worker class being enqueued
|
44
|
+
# @param [Hash] job
|
45
|
+
# the full job payload
|
46
|
+
# @see https://github.com/mperham/sidekiq/wiki/Job-Format
|
47
|
+
# @param [String] queue
|
48
|
+
# the name of the queue the job was pulled from
|
49
|
+
# @yield the next middleware in the chain or the enqueuing of the job
|
21
50
|
def call(worker, job, queue)
|
22
51
|
yield
|
23
52
|
# Skip if the max RSS is not exceeded
|
@@ -25,8 +54,9 @@ class Sidekiq::WorkerKiller
|
|
25
54
|
return unless current_rss > @max_rss
|
26
55
|
GC.start(full_mark: true, immediate_sweep: true) if @gc
|
27
56
|
return unless current_rss > @max_rss
|
28
|
-
if
|
29
|
-
warn "current RSS #{current_rss} exceeds maximum RSS #{@max_rss},
|
57
|
+
if skip_shutdown?(worker, job, queue)
|
58
|
+
warn "current RSS #{current_rss} exceeds maximum RSS #{@max_rss}, " \
|
59
|
+
"however shutdown will be ignored"
|
30
60
|
return
|
31
61
|
end
|
32
62
|
|
@@ -37,6 +67,10 @@ class Sidekiq::WorkerKiller
|
|
37
67
|
|
38
68
|
private
|
39
69
|
|
70
|
+
def skip_shutdown?(worker, job, queue)
|
71
|
+
@skip_shutdown.respond_to?(:call) && @skip_shutdown.call(worker, job, queue)
|
72
|
+
end
|
73
|
+
|
40
74
|
def request_shutdown
|
41
75
|
# In another thread to allow undelying job to finish
|
42
76
|
Thread.new do
|
@@ -47,32 +81,28 @@ class Sidekiq::WorkerKiller
|
|
47
81
|
end
|
48
82
|
|
49
83
|
def shutdown
|
50
|
-
warn "sending
|
51
|
-
|
84
|
+
warn "sending quiet to #{identity}"
|
85
|
+
sidekiq_process.quiet!
|
52
86
|
|
53
87
|
sleep(5) # gives Sidekiq API 5 seconds to update ProcessSet
|
54
88
|
|
55
89
|
warn "shutting down #{identity} in #{@grace_time} seconds"
|
56
90
|
wait_job_finish_in_grace_time
|
57
91
|
|
58
|
-
warn "
|
59
|
-
|
92
|
+
warn "stopping #{identity}"
|
93
|
+
sidekiq_process.stop!
|
60
94
|
|
61
95
|
warn "waiting #{@shutdown_wait} seconds before sending " \
|
62
96
|
"#{@kill_signal} to #{identity}"
|
63
97
|
sleep(@shutdown_wait)
|
64
98
|
|
65
99
|
warn "sending #{@kill_signal} to #{identity}"
|
66
|
-
|
100
|
+
::Process.kill(@kill_signal, ::Process.pid)
|
67
101
|
end
|
68
102
|
|
69
103
|
def wait_job_finish_in_grace_time
|
70
104
|
start = Time.now
|
71
|
-
|
72
|
-
break if grace_time_exceeded?(start)
|
73
|
-
break if no_jobs_on_quiet_processes?
|
74
|
-
sleep(1)
|
75
|
-
end
|
105
|
+
sleep(1) until grace_time_exceeded?(start) || jobs_finished?
|
76
106
|
end
|
77
107
|
|
78
108
|
def grace_time_exceeded?(start)
|
@@ -81,35 +111,18 @@ class Sidekiq::WorkerKiller
|
|
81
111
|
start + @grace_time < Time.now
|
82
112
|
end
|
83
113
|
|
84
|
-
def
|
85
|
-
|
86
|
-
return false if process["busy"] != 0 && process["quiet"] == "true"
|
87
|
-
end
|
88
|
-
true
|
114
|
+
def jobs_finished?
|
115
|
+
sidekiq_process.stopping? && sidekiq_process["busy"] == 0
|
89
116
|
end
|
90
117
|
|
91
118
|
def current_rss
|
92
119
|
::GetProcessMem.new.mb
|
93
120
|
end
|
94
121
|
|
95
|
-
def
|
96
|
-
::
|
97
|
-
|
98
|
-
|
99
|
-
def pid
|
100
|
-
::Process.pid
|
101
|
-
end
|
102
|
-
|
103
|
-
def identity
|
104
|
-
"#{hostname}:#{pid}"
|
105
|
-
end
|
106
|
-
|
107
|
-
def quiet_signal
|
108
|
-
if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("5.0")
|
109
|
-
"TSTP"
|
110
|
-
else
|
111
|
-
"USR1"
|
112
|
-
end
|
122
|
+
def sidekiq_process
|
123
|
+
Sidekiq::ProcessSet.new.find do |process|
|
124
|
+
process["identity"] == identity
|
125
|
+
end || raise("No sidekiq worker with identity #{identity} found")
|
113
126
|
end
|
114
127
|
|
115
128
|
def warn(msg)
|
@@ -1,30 +1,42 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe Sidekiq::WorkerKiller do
|
4
|
+
let(:sidekiq_process_set) { instance_double(Sidekiq::ProcessSet) }
|
5
|
+
let(:sidekiq_process) { instance_double(Sidekiq::Process) }
|
4
6
|
|
5
7
|
before do
|
6
8
|
allow(subject).to receive(:warn) # silence "warn" logs
|
7
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
|
+
allow(sidekiq_process).to receive(:quiet!)
|
13
|
+
allow(sidekiq_process).to receive(:stop!)
|
14
|
+
allow(sidekiq_process).to receive(:stopping?)
|
8
15
|
end
|
9
16
|
|
10
17
|
describe "#call" do
|
11
18
|
let(:worker){ double("worker") }
|
12
19
|
let(:job){ double("job") }
|
13
20
|
let(:queue){ double("queue") }
|
21
|
+
|
14
22
|
it "should yield" do
|
15
23
|
expect { |b|
|
16
24
|
subject.call(worker, job, queue, &b)
|
17
25
|
}.to yield_with_no_args
|
18
26
|
end
|
27
|
+
|
19
28
|
context "when current rss is over max rss" do
|
20
29
|
subject{ described_class.new(max_rss: 2) }
|
30
|
+
|
21
31
|
before do
|
22
32
|
allow(subject).to receive(:current_rss).and_return(3)
|
23
33
|
end
|
34
|
+
|
24
35
|
it "should request shutdown" do
|
25
36
|
expect(subject).to receive(:request_shutdown)
|
26
37
|
subject.call(worker, job, queue){}
|
27
38
|
end
|
39
|
+
|
28
40
|
it "should call garbage collect" do
|
29
41
|
allow(subject).to receive(:request_shutdown)
|
30
42
|
expect(GC).to receive(:start).with(full_mark: true, immediate_sweep: true)
|
@@ -75,6 +87,7 @@ describe Sidekiq::WorkerKiller do
|
|
75
87
|
subject.call(worker, job, queue){}
|
76
88
|
end
|
77
89
|
end
|
90
|
+
|
78
91
|
context "but max rss is 0" do
|
79
92
|
subject{ described_class.new(max_rss: 0) }
|
80
93
|
it "should not request shutdown" do
|
@@ -115,12 +128,13 @@ describe Sidekiq::WorkerKiller do
|
|
115
128
|
original_request_shutdown.call
|
116
129
|
end
|
117
130
|
|
118
|
-
# track when the
|
119
|
-
|
120
|
-
shutdown_time = Time.now
|
131
|
+
# track when the process has been required to stop
|
132
|
+
expect(sidekiq_process).to receive(:stop!) do |*args|
|
133
|
+
shutdown_time = Time.now
|
121
134
|
end
|
122
135
|
|
123
|
-
allow(
|
136
|
+
allow(Process).to receive(:kill)
|
137
|
+
allow(Process).to receive(:pid).and_return(99)
|
124
138
|
|
125
139
|
subject.send(:request_shutdown).join
|
126
140
|
|
@@ -135,29 +149,14 @@ describe Sidekiq::WorkerKiller do
|
|
135
149
|
context "grace time is Float::INFINITY" do
|
136
150
|
subject{ described_class.new(max_rss: 2, grace_time: Float::INFINITY, shutdown_wait: 0) }
|
137
151
|
it "call signal only on jobs" do
|
138
|
-
allow(subject).to receive(:
|
139
|
-
allow(
|
140
|
-
expect(
|
141
|
-
expect(
|
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!)
|
142
156
|
expect(Process).to receive(:kill).with('SIGKILL', 99)
|
143
157
|
|
144
158
|
subject.send(:request_shutdown).join
|
145
159
|
end
|
146
160
|
end
|
147
161
|
end
|
148
|
-
|
149
|
-
describe "#quiet_signal" do
|
150
|
-
it "should give TSTP if Sidekiq version is > 5.0" do
|
151
|
-
stub_const("Sidekiq::VERSION", "5.0")
|
152
|
-
expect(subject.send :quiet_signal).to eq "TSTP"
|
153
|
-
stub_const("Sidekiq::VERSION", "5.2.1")
|
154
|
-
expect(subject.send :quiet_signal).to eq "TSTP"
|
155
|
-
end
|
156
|
-
it "should give USR1 if Sidekiq version is < 5.0" do
|
157
|
-
stub_const("Sidekiq::VERSION", "3.0")
|
158
|
-
expect(subject.send :quiet_signal).to eq "USR1"
|
159
|
-
stub_const("Sidekiq::VERSION", "4.6.7")
|
160
|
-
expect(subject.send :quiet_signal).to eq "USR1"
|
161
|
-
end
|
162
|
-
end
|
163
162
|
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: 0.
|
4
|
+
version: 1.0.0
|
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: 2020-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: get_process_mem
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '5'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '5'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,8 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
96
|
- !ruby/object:Gem::Version
|
97
97
|
version: '0'
|
98
98
|
requirements: []
|
99
|
-
|
100
|
-
rubygems_version: 2.5.2.3
|
99
|
+
rubygems_version: 3.1.0.pre3
|
101
100
|
signing_key:
|
102
101
|
specification_version: 4
|
103
102
|
summary: Sidekiq worker killer
|