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 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