sidekiq-worker-killer 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +23 -5
- data/lib/sidekiq/worker_killer.rb +85 -78
- data/lib/sidekiq/worker_killer/version.rb +3 -1
- data/spec/sidekiq/worker_killer_spec.rb +69 -7
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c2b88a4d00042d82a27ebd81fa27832f01873618
|
4
|
+
data.tar.gz: 010d2676048b457204d403509774c4e013cd39d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9aef7ae408f33ed04a2cbe81ba673887d877762c803482da841a25c0878b2c91e96e6eb8b4eabfddcbcfb42b8bf7ce56e218463df867af44fadad7afb4a6a0c9
|
7
|
+
data.tar.gz: 4c9cfa47bdb45445f6647ca0297e3118f4f37cc675759a3e292a96e701577c385c6f7bcce832cd8c5082f59acce4449aefad7e92160e59f8049eefa5499d95c7
|
data/README.md
CHANGED
@@ -3,13 +3,15 @@
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/sidekiq-worker-killer.svg)](https://badge.fury.io/rb/sidekiq-worker-killer)
|
4
4
|
[![CircleCI](https://circleci.com/gh/klaxit/sidekiq-worker-killer.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/klaxit/sidekiq-worker-killer)
|
5
5
|
|
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.
|
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
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).
|
9
9
|
|
10
|
+
quick-refs: [install](#install) | [usage](#usage) | [available options](#available-options) | [development](#development)
|
11
|
+
|
10
12
|
## Install
|
11
13
|
Use [Bundler](http://bundler.io/)
|
12
|
-
```
|
14
|
+
```ruby
|
13
15
|
gem "sidekiq-worker-killer"
|
14
16
|
```
|
15
17
|
|
@@ -17,7 +19,9 @@ gem "sidekiq-worker-killer"
|
|
17
19
|
|
18
20
|
Add this to your Sidekiq configuration.
|
19
21
|
|
20
|
-
```
|
22
|
+
```ruby
|
23
|
+
require 'sidekiq/worker_killer'
|
24
|
+
|
21
25
|
Sidekiq.configure_server do |config|
|
22
26
|
config.server_middleware do |chain|
|
23
27
|
chain.add Sidekiq::WorkerKiller, max_rss: 480
|
@@ -25,16 +29,30 @@ Sidekiq.configure_server do |config|
|
|
25
29
|
end
|
26
30
|
```
|
27
31
|
|
28
|
-
|
32
|
+
## Available options
|
29
33
|
|
30
34
|
The following options can be overrided.
|
31
35
|
|
32
36
|
| Option | Defaults | Description |
|
33
37
|
| ------- | ------- | ----------- |
|
34
38
|
| max_rss | 0 MB (disabled) | max RSS in megabytes. Above this, shutdown will be triggered. |
|
35
|
-
| 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. |
|
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 |
|
36
40
|
| shutdown_wait | 30 seconds | when the grace time expires, still running jobs get 30 seconds to terminate. After that, kill signal is triggered. |
|
37
41
|
| kill_signal | SIGKILL | Signal to use kill Sidekiq process if it doesn't terminate. |
|
42
|
+
| gc | true | Try to run garbage collection before Sidekiq process terminate in case of max_rss exceeded |
|
43
|
+
|
44
|
+
## Development
|
45
|
+
|
46
|
+
Pull Requests are very welcome!
|
47
|
+
|
48
|
+
There are tasks that may help you along the way in a makefile:
|
49
|
+
|
50
|
+
```bash
|
51
|
+
make console # Loads the whole stack in an IRB session.
|
52
|
+
make test # Run tests.
|
53
|
+
make lint # Run rubocop linter.
|
54
|
+
```
|
55
|
+
Please make sure that you have tested your code carefully before opening a PR, and make sure as well that you have no style issues.
|
38
56
|
|
39
57
|
## Authors
|
40
58
|
|
@@ -2,103 +2,110 @@ require "get_process_mem"
|
|
2
2
|
require "sidekiq"
|
3
3
|
require "sidekiq/util"
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
5
|
+
# Sidekiq server middleware. Kill worker when the RSS memory exceeds limit
|
6
|
+
# after a given grace time.
|
7
|
+
class Sidekiq::WorkerKiller
|
8
|
+
include Sidekiq::Util
|
9
|
+
|
10
|
+
MUTEX = Mutex.new
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@max_rss = options.fetch(:max_rss, 0)
|
14
|
+
@grace_time = options.fetch(:grace_time, 15 * 60)
|
15
|
+
@shutdown_wait = options.fetch(:shutdown_wait, 30)
|
16
|
+
@kill_signal = options.fetch(:kill_signal, "SIGKILL")
|
17
|
+
@gc = options.fetch(:gc, true)
|
18
|
+
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
20
|
+
def call(_worker, _job, _queue)
|
21
|
+
yield
|
22
|
+
# Skip if the max RSS is not exceeded
|
23
|
+
return unless @max_rss > 0
|
24
|
+
return unless current_rss > @max_rss
|
25
|
+
GC.start(full_mark: true, immediate_sweep: true) if @gc
|
26
|
+
return unless current_rss > @max_rss
|
27
|
+
# Launch the shutdown process
|
28
|
+
warn "current RSS #{current_rss} of #{identity} exceeds " \
|
29
|
+
"maximum RSS #{@max_rss}"
|
30
|
+
request_shutdown
|
31
|
+
end
|
31
32
|
|
32
|
-
|
33
|
+
private
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
end
|
35
|
+
def request_shutdown
|
36
|
+
# In another thread to allow undelying job to finish
|
37
|
+
Thread.new do
|
38
|
+
# Only if another thread is not already
|
39
|
+
# shutting down the Sidekiq process
|
40
|
+
shutdown if MUTEX.try_lock
|
41
41
|
end
|
42
|
+
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
def shutdown
|
45
|
+
warn "sending #{quiet_signal} to #{identity}"
|
46
|
+
signal(quiet_signal, pid)
|
46
47
|
|
47
|
-
|
48
|
-
|
48
|
+
warn "shutting down #{identity} in #{@grace_time} seconds"
|
49
|
+
wait_job_finish_in_grace_time
|
49
50
|
|
50
|
-
|
51
|
-
|
51
|
+
warn "sending SIGTERM to #{identity}"
|
52
|
+
signal("SIGTERM", pid)
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
54
|
+
warn "waiting #{@shutdown_wait} seconds before sending " \
|
55
|
+
"#{@kill_signal} to #{identity}"
|
56
|
+
sleep(@shutdown_wait)
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
warn "sending #{@kill_signal} to #{identity}"
|
59
|
+
signal(@kill_signal, pid)
|
60
|
+
end
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
62
|
+
def wait_job_finish_in_grace_time
|
63
|
+
start = Time.now
|
64
|
+
loop do
|
65
|
+
break if grace_time_exceeded?(start)
|
66
|
+
break if no_jobs_on_quiet_processes?
|
67
|
+
sleep(1)
|
67
68
|
end
|
69
|
+
end
|
68
70
|
|
69
|
-
|
70
|
-
|
71
|
-
return false if !process["busy"].zero? && process["quiet"]
|
72
|
-
end
|
73
|
-
true
|
74
|
-
end
|
71
|
+
def grace_time_exceeded?(start)
|
72
|
+
return false if @grace_time == Float::INFINITY
|
75
73
|
|
76
|
-
|
77
|
-
|
78
|
-
end
|
74
|
+
start + @grace_time < Time.now
|
75
|
+
end
|
79
76
|
|
80
|
-
|
81
|
-
|
77
|
+
def no_jobs_on_quiet_processes?
|
78
|
+
Sidekiq::ProcessSet.new.each do |process|
|
79
|
+
return false if process["busy"] != 0 && process["quiet"] == "true"
|
82
80
|
end
|
81
|
+
true
|
82
|
+
end
|
83
83
|
|
84
|
-
|
85
|
-
|
86
|
-
|
84
|
+
def current_rss
|
85
|
+
::GetProcessMem.new.mb
|
86
|
+
end
|
87
87
|
|
88
|
-
|
89
|
-
|
90
|
-
|
88
|
+
def signal(signal, pid)
|
89
|
+
::Process.kill(signal, pid)
|
90
|
+
end
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
92
|
+
def pid
|
93
|
+
::Process.pid
|
94
|
+
end
|
95
|
+
|
96
|
+
def identity
|
97
|
+
"#{hostname}:#{pid}"
|
98
|
+
end
|
99
99
|
|
100
|
-
|
101
|
-
|
100
|
+
def quiet_signal
|
101
|
+
if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("5.0")
|
102
|
+
"TSTP"
|
103
|
+
else
|
104
|
+
"USR1"
|
102
105
|
end
|
103
106
|
end
|
107
|
+
|
108
|
+
def warn(msg)
|
109
|
+
Sidekiq.logger.warn(msg)
|
110
|
+
end
|
104
111
|
end
|
@@ -24,6 +24,19 @@ describe Sidekiq::WorkerKiller do
|
|
24
24
|
expect(subject).to receive(:request_shutdown)
|
25
25
|
subject.call(worker, job, queue){}
|
26
26
|
end
|
27
|
+
it "should call garbage collect" do
|
28
|
+
allow(subject).to receive(:request_shutdown)
|
29
|
+
expect(GC).to receive(:start).with(full_mark: true, immediate_sweep: true)
|
30
|
+
subject.call(worker, job, queue){}
|
31
|
+
end
|
32
|
+
context "when gc is false" do
|
33
|
+
subject{ described_class.new(max_rss: 2, gc: false) }
|
34
|
+
it "should not call garbage collect" do
|
35
|
+
allow(subject).to receive(:request_shutdown)
|
36
|
+
expect(GC).not_to receive(:start)
|
37
|
+
subject.call(worker, job, queue){}
|
38
|
+
end
|
39
|
+
end
|
27
40
|
context "but max rss is 0" do
|
28
41
|
subject{ described_class.new(max_rss: 0) }
|
29
42
|
it "should not request shutdown" do
|
@@ -35,14 +48,63 @@ describe Sidekiq::WorkerKiller do
|
|
35
48
|
end
|
36
49
|
|
37
50
|
describe "#request_shutdown" do
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
51
|
+
context "grace time is default" do
|
52
|
+
before { allow(subject).to receive(:shutdown){ sleep 0.01 } }
|
53
|
+
it "should call shutdown" do
|
54
|
+
expect(subject).to receive(:shutdown)
|
55
|
+
subject.send(:request_shutdown).join
|
56
|
+
end
|
57
|
+
it "should not call shutdown twice when called concurrently" do
|
58
|
+
expect(subject).to receive(:shutdown).once
|
59
|
+
2.times.map{ subject.send(:request_shutdown) }.each(&:join)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "grace time is 5 seconds" do
|
64
|
+
subject{ described_class.new(max_rss: 2, grace_time: 5.0, shutdown_wait: 0) }
|
65
|
+
it "should wait the specified grace time before calling shutdown" do
|
66
|
+
# there are busy jobs that will not terminate within the grace time
|
67
|
+
allow(subject).to receive(:no_jobs_on_quiet_processes?).and_return(false)
|
68
|
+
|
69
|
+
shutdown_request_time = nil
|
70
|
+
shutdown_time = nil
|
71
|
+
|
72
|
+
# replace the original #request_shutdown to track
|
73
|
+
# when the shutdown is requested
|
74
|
+
original_request_shutdown = subject.method(:request_shutdown)
|
75
|
+
allow(subject).to receive(:request_shutdown) do
|
76
|
+
shutdown_request_time= Time.now
|
77
|
+
original_request_shutdown.call
|
78
|
+
end
|
79
|
+
|
80
|
+
# track when the SIGTERM signal is sent
|
81
|
+
allow(Process).to receive(:kill) do |*args|
|
82
|
+
shutdown_time = Time.now if args[0] == 'SIGTERM'
|
83
|
+
end
|
84
|
+
|
85
|
+
allow(subject).to receive(:pid).and_return(99)
|
86
|
+
|
87
|
+
subject.send(:request_shutdown).join
|
88
|
+
|
89
|
+
elapsed_time = shutdown_time - shutdown_request_time
|
90
|
+
|
91
|
+
# the elapsed time beetween shutdown request and the actual
|
92
|
+
# shutdown signal should be greater than the specificed grace_time
|
93
|
+
expect(elapsed_time).to be >= 5.0
|
94
|
+
end
|
42
95
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
96
|
+
|
97
|
+
context "grace time is Float::INFINITY" do
|
98
|
+
subject{ described_class.new(max_rss: 2, grace_time: Float::INFINITY, shutdown_wait: 0) }
|
99
|
+
it "call signal only on jobs" do
|
100
|
+
allow(subject).to receive(:no_jobs_on_quiet_processes?).and_return(true)
|
101
|
+
allow(subject).to receive(:pid).and_return(99)
|
102
|
+
expect(Process).to receive(:kill).with('TSTP', 99)
|
103
|
+
expect(Process).to receive(:kill).with('SIGTERM', 99)
|
104
|
+
expect(Process).to receive(:kill).with('SIGKILL', 99)
|
105
|
+
|
106
|
+
subject.send(:request_shutdown).join
|
107
|
+
end
|
46
108
|
end
|
47
109
|
end
|
48
110
|
|
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: 0.4.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: 2019-05-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: get_process_mem
|
@@ -39,33 +39,33 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: '3.5'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: '3.5'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: rubocop
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 0.49.1
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 0.49.1
|
69
69
|
description:
|
70
70
|
email:
|
71
71
|
- dev@klaxit.com
|