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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4269c4f6da1a1eff0beed38c5ced2f6fca035b83
4
- data.tar.gz: 9d9fb7cb1bf97982dfb489ab5a397b06930c0620
3
+ metadata.gz: c2b88a4d00042d82a27ebd81fa27832f01873618
4
+ data.tar.gz: 010d2676048b457204d403509774c4e013cd39d4
5
5
  SHA512:
6
- metadata.gz: 716ea8ce12527e8653f0bc08c9cc16acc428917340907a9701e2e45d8f3996a157dacc634a96d0db74db3dfa949ba8ffb0377391837942df5d343d5b1870463f
7
- data.tar.gz: 0f04a39f3ac416f231e7b075a34ce827f4c435183e1b2277d50ce1fb4392b66a7ddcef59c60d4c0e1392ac34c572849501624d8be642b3cb76495562ebd85cd5
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
- # Available options
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
- module Sidekiq
6
- # Sidekiq server middleware. Kill worker when the RSS memory exceeds limit
7
- # after a given grace time.
8
- class WorkerKiller
9
- include Sidekiq::Util
10
-
11
- MUTEX = Mutex.new
12
-
13
- def initialize(options = {})
14
- @max_rss = (options[:max_rss] || 0)
15
- @grace_time = (options[:grace_time] || 15 * 60)
16
- @shutdown_wait = (options[:shutdown_wait] || 30)
17
- @kill_signal = (options[:kill_signal] || "SIGKILL")
18
- end
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
- def call(_worker, _job, _queue)
21
- yield
22
- # Skip if the max RSS is not exceeded
23
- return unless @max_rss > 0 && current_rss > @max_rss
24
- GC.start(full_mark: true, immediate_sweep: true)
25
- return unless @max_rss > 0 && current_rss > @max_rss
26
- # Launch the shutdown process
27
- warn "current RSS #{current_rss} of #{identity} exceeds " \
28
- "maximum RSS #{@max_rss}"
29
- request_shutdown
30
- end
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
- private
33
+ private
33
34
 
34
- def request_shutdown
35
- # In another thread to allow undelying job to finish
36
- Thread.new do
37
- # Only if another thread is not already
38
- # shutting down the Sidekiq process
39
- shutdown if MUTEX.try_lock
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
- def shutdown
44
- warn "sending #{quiet_signal} to #{identity}"
45
- signal(quiet_signal, pid)
44
+ def shutdown
45
+ warn "sending #{quiet_signal} to #{identity}"
46
+ signal(quiet_signal, pid)
46
47
 
47
- warn "shutting down #{identity} in #{@grace_time} seconds"
48
- wait_job_finish_in_grace_time
48
+ warn "shutting down #{identity} in #{@grace_time} seconds"
49
+ wait_job_finish_in_grace_time
49
50
 
50
- warn "sending SIGTERM to #{identity}"
51
- signal("SIGTERM", pid)
51
+ warn "sending SIGTERM to #{identity}"
52
+ signal("SIGTERM", pid)
52
53
 
53
- warn "waiting #{@shutdown_wait} seconds before sending " \
54
- "#{@kill_signal} to #{identity}"
55
- sleep(@shutdown_wait)
54
+ warn "waiting #{@shutdown_wait} seconds before sending " \
55
+ "#{@kill_signal} to #{identity}"
56
+ sleep(@shutdown_wait)
56
57
 
57
- warn "sending #{@kill_signal} to #{identity}"
58
- signal(@kill_signal, pid)
59
- end
58
+ warn "sending #{@kill_signal} to #{identity}"
59
+ signal(@kill_signal, pid)
60
+ end
60
61
 
61
- def wait_job_finish_in_grace_time
62
- start = Time.now
63
- loop do
64
- break if start + @grace_time < Time.now || no_jobs_on_quiet_processes?
65
- sleep(1)
66
- end
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
- def no_jobs_on_quiet_processes?
70
- Sidekiq::ProcessSet.new.each do |process|
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
- def current_rss
77
- ::GetProcessMem.new.mb
78
- end
74
+ start + @grace_time < Time.now
75
+ end
79
76
 
80
- def signal(signal, pid)
81
- ::Process.kill(signal, pid)
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
- def pid
85
- ::Process.pid
86
- end
84
+ def current_rss
85
+ ::GetProcessMem.new.mb
86
+ end
87
87
 
88
- def identity
89
- "#{hostname}:#{pid}"
90
- end
88
+ def signal(signal, pid)
89
+ ::Process.kill(signal, pid)
90
+ end
91
91
 
92
- def quiet_signal
93
- if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("5.0")
94
- "TSTP"
95
- else
96
- "USR1"
97
- end
98
- end
92
+ def pid
93
+ ::Process.pid
94
+ end
95
+
96
+ def identity
97
+ "#{hostname}:#{pid}"
98
+ end
99
99
 
100
- def warn(msg)
101
- Sidekiq.logger.warn(msg)
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
@@ -1,5 +1,7 @@
1
+ # rubocop:disable Style/ClassAndModuleChildren
1
2
  module Sidekiq
2
3
  class WorkerKiller
3
- VERSION = "0.3.0".freeze
4
+ VERSION = "0.4.0".freeze
4
5
  end
5
6
  end
7
+ # rubocop:enable Style/ClassAndModuleChildren
@@ -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
- before { allow(subject).to receive(:shutdown){ sleep 0.01 } }
39
- it "should call shutdown" do
40
- expect(subject).to receive(:shutdown)
41
- subject.send(:request_shutdown).join
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
- it "should not call shutdown twice when called concurrently" do
44
- expect(subject).to receive(:shutdown).once
45
- 2.times.map{ subject.send(:request_shutdown) }.each(&:join)
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.3.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: 2018-09-12 00:00:00.000000000 Z
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: rubocop
42
+ name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.49.1
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: 0.49.1
54
+ version: '3.5'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rspec
56
+ name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '3.5'
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: '3.5'
68
+ version: 0.49.1
69
69
  description:
70
70
  email:
71
71
  - dev@klaxit.com