sidekiq-worker-killer 0.5.0 → 1.0.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
- SHA1:
3
- metadata.gz: bfc7e3a75eba6f95fd48ee2c0fa604cddad8328f
4
- data.tar.gz: 367eb4d8380b612dad3300829962cdcad40509fd
2
+ SHA256:
3
+ metadata.gz: f40045c5805f35f8792f66c5dac896042c3b50b7b8c42f0f95e14827d4184a5a
4
+ data.tar.gz: 5721815ad342ec06d6231c005b60cb0bbda5300343a4bfb99b9b138fff8f6627
5
5
  SHA512:
6
- metadata.gz: e26eae8384c0a43e2f57b792f094abb12b419ef58a47cfda9d2e42285ebfacf4ade437148cd20e742c5a6aaea9c1dc392e5272749aff2c1ad607e860325724a1
7
- data.tar.gz: c347ad5878953ff5383508f0871d1beb12c1442970b2c2f6b4ff8b86a869002f75b928221506c176b395cf69b2419376e002340d70ecdfe82fb33f25c90c2d17
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 terminate. After that, kill signal is triggered. |
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
- | skip_shutdown_if | Proc.new {false} | Executes a block of code after max_rss exceeds but before requesting shutdown. |
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, Proc.new { false })
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 @skip_shutdown.respond_to?(:call) && @skip_shutdown.call(worker, job, queue)
29
- warn "current RSS #{current_rss} exceeds maximum RSS #{@max_rss}, however shutdown will be ignored"
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 #{quiet_signal} to #{identity}"
51
- signal(quiet_signal, pid)
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 "sending SIGTERM to #{identity}"
59
- signal("SIGTERM", pid)
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
- signal(@kill_signal, pid)
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
- loop do
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 no_jobs_on_quiet_processes?
85
- Sidekiq::ProcessSet.new.each do |process|
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 signal(signal, pid)
96
- ::Process.kill(signal, pid)
97
- end
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,7 +1,7 @@
1
1
  # rubocop:disable Style/ClassAndModuleChildren
2
2
  module Sidekiq
3
3
  class WorkerKiller
4
- VERSION = "0.5.0".freeze
4
+ VERSION = "1.0.0".freeze
5
5
  end
6
6
  end
7
7
  # rubocop:enable Style/ClassAndModuleChildren
@@ -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 SIGTERM signal is sent
119
- allow(Process).to receive(:kill) do |*args|
120
- shutdown_time = Time.now if args[0] == 'SIGTERM'
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(subject).to receive(:pid).and_return(99)
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(:no_jobs_on_quiet_processes?).and_return(true)
139
- allow(subject).to receive(:pid).and_return(99)
140
- expect(Process).to receive(:kill).with('TSTP', 99)
141
- expect(Process).to receive(:kill).with('SIGTERM', 99)
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.5.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: 2019-10-29 00:00:00.000000000 Z
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: '3'
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: '3'
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
- rubyforge_project:
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