sidekiq-worker-killer 0.2.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: 643766850e2b525cc9458efaeb259b2525654235
4
- data.tar.gz: 25cc9a461b70258f9ce09ab71fa4d59f48150744
2
+ SHA256:
3
+ metadata.gz: f40045c5805f35f8792f66c5dac896042c3b50b7b8c42f0f95e14827d4184a5a
4
+ data.tar.gz: 5721815ad342ec06d6231c005b60cb0bbda5300343a4bfb99b9b138fff8f6627
5
5
  SHA512:
6
- metadata.gz: 13e54d178b6eaf6de4c18bff3cb7bf5a0f583a2a511880514cb0b50c4158a8acf93dad622f3d906304e6b4a581c1d4a9b34a9e100354e0d3984891fb09ce34ec
7
- data.tar.gz: e538edc1f1d17a2ffcba3121551bde5234375eb1648e98c1790efd973e1f8934131ad3ec9ecc8ace4101e2ec0ca97c62908e04b62b7b3243c6676a3511458d70
6
+ metadata.gz: 4db165333c1a8465b15e514af6b178aec3091bdb90a2ced38b8131b98706922518b25632f45a65fa33b923ac6d2a42c3379dee4e4fde1dde9dd20d9a49aab1e1
7
+ data.tar.gz: dd3e9e16d4bfbc5d753559e6b94c95b334ef2855864bcc7ef53f1577e40f6bc5122ad7040133c04d2c019ab37d532f012339fb18180e8efe74dc15274d427f49
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,45 @@ 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 but wait 15 minutes for running jobs to finish. |
36
- | shutdown_wait | 30 seconds | when the grace time expires, still running jobs get 30 seconds to terminate. After that, kill signal is triggered. |
37
- | kill_signal | SIGKILL | Signal to use kill Sidekiq process if it doesn't terminate. |
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
+
45
+ *skip_shutdown_if* is expected to return anything other than `false` or `nil` to skip shutdown.
46
+
47
+ ```ruby
48
+ require 'sidekiq/worker_killer'
49
+
50
+ Sidekiq.configure_server do |config|
51
+ config.server_middleware do |chain|
52
+ chain.add Sidekiq::WorkerKiller, max_rss: 480, skip_shutdown_if: ->(worker, job, queue) do
53
+ worker.to_s == 'LongWorker'
54
+ end
55
+ end
56
+ end
57
+ ```
58
+
59
+ ## Development
60
+
61
+ Pull Requests are very welcome!
62
+
63
+ There are tasks that may help you along the way in a makefile:
64
+
65
+ ```bash
66
+ make console # Loads the whole stack in an IRB session.
67
+ make test # Run tests.
68
+ make lint # Run rubocop linter.
69
+ ```
70
+ 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
71
 
39
72
  ## Authors
40
73
 
@@ -1,87 +1,131 @@
1
1
  require "get_process_mem"
2
2
  require "sidekiq"
3
3
  require "sidekiq/util"
4
+ require "sidekiq/api"
5
+
6
+ # Sidekiq server middleware. Kill worker when the RSS memory exceeds limit
7
+ # after a given grace time.
8
+ class Sidekiq::WorkerKiller
9
+ include Sidekiq::Util
10
+
11
+ MUTEX = Mutex.new
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}`)
33
+ def initialize(options = {})
34
+ @max_rss = options.fetch(:max_rss, 0)
35
+ @grace_time = options.fetch(:grace_time, 15 * 60)
36
+ @shutdown_wait = options.fetch(:shutdown_wait, 30)
37
+ @kill_signal = options.fetch(:kill_signal, "SIGKILL")
38
+ @gc = options.fetch(:gc, true)
39
+ @skip_shutdown = options.fetch(:skip_shutdown_if, proc { false })
40
+ end
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
50
+ def call(worker, job, queue)
51
+ yield
52
+ # Skip if the max RSS is not exceeded
53
+ return unless @max_rss > 0
54
+ return unless current_rss > @max_rss
55
+ GC.start(full_mark: true, immediate_sweep: true) if @gc
56
+ return unless current_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"
60
+ return
61
+ end
4
62
 
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
63
+ warn "current RSS #{current_rss} of #{identity} exceeds " \
64
+ "maximum RSS #{@max_rss}"
65
+ request_shutdown
66
+ end
10
67
 
11
- MUTEX = Mutex.new
68
+ private
12
69
 
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
70
+ def skip_shutdown?(worker, job, queue)
71
+ @skip_shutdown.respond_to?(:call) && @skip_shutdown.call(worker, job, queue)
72
+ end
19
73
 
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
- # Launch the shutdown process
25
- warn "current RSS #{current_rss} of #{identity} exceeds " \
26
- "maximum RSS #{@max_rss}"
27
- request_shutdown
74
+ def request_shutdown
75
+ # In another thread to allow undelying job to finish
76
+ Thread.new do
77
+ # Only if another thread is not already
78
+ # shutting down the Sidekiq process
79
+ shutdown if MUTEX.try_lock
28
80
  end
81
+ end
29
82
 
30
- private
83
+ def shutdown
84
+ warn "sending quiet to #{identity}"
85
+ sidekiq_process.quiet!
31
86
 
32
- def request_shutdown
33
- # In another thread to allow undelying job to finish
34
- Thread.new do
35
- # Only if another thread is not already
36
- # shutting down the Sidekiq process
37
- shutdown if MUTEX.try_lock
38
- end
39
- end
87
+ sleep(5) # gives Sidekiq API 5 seconds to update ProcessSet
40
88
 
41
- def shutdown
42
- warn "sending #{quiet_signal} to #{identity}"
43
- signal(quiet_signal, pid)
89
+ warn "shutting down #{identity} in #{@grace_time} seconds"
90
+ wait_job_finish_in_grace_time
44
91
 
45
- warn "shutting down #{identity} in #{@grace_time} seconds"
46
- sleep(@grace_time)
92
+ warn "stopping #{identity}"
93
+ sidekiq_process.stop!
47
94
 
48
- warn "sending SIGTERM to #{identity}"
49
- signal("SIGTERM", pid)
95
+ warn "waiting #{@shutdown_wait} seconds before sending " \
96
+ "#{@kill_signal} to #{identity}"
97
+ sleep(@shutdown_wait)
50
98
 
51
- warn "waiting #{@shutdown_wait} seconds before sending " \
52
- "#{@kill_signal} to #{identity}"
53
- sleep(@kill_signal)
99
+ warn "sending #{@kill_signal} to #{identity}"
100
+ ::Process.kill(@kill_signal, ::Process.pid)
101
+ end
54
102
 
55
- warn "sending #{@kill_signal} to #{identity}"
56
- signal(@kill_signal, pid)
57
- end
103
+ def wait_job_finish_in_grace_time
104
+ start = Time.now
105
+ sleep(1) until grace_time_exceeded?(start) || jobs_finished?
106
+ end
58
107
 
59
- def current_rss
60
- ::GetProcessMem.new.mb
61
- end
108
+ def grace_time_exceeded?(start)
109
+ return false if @grace_time == Float::INFINITY
62
110
 
63
- def signal(signal, pid)
64
- ::Process.kill(signal, pid)
65
- end
111
+ start + @grace_time < Time.now
112
+ end
66
113
 
67
- def pid
68
- ::Process.pid
69
- end
114
+ def jobs_finished?
115
+ sidekiq_process.stopping? && sidekiq_process["busy"] == 0
116
+ end
70
117
 
71
- def identity
72
- "#{hostname}:#{pid}"
73
- end
118
+ def current_rss
119
+ ::GetProcessMem.new.mb
120
+ end
74
121
 
75
- def quiet_signal
76
- if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("5.0")
77
- "TSTP"
78
- else
79
- "USR1"
80
- end
81
- 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")
126
+ end
82
127
 
83
- def warn(msg)
84
- Sidekiq.logger.warn(msg)
85
- end
128
+ def warn(msg)
129
+ Sidekiq.logger.warn(msg)
86
130
  end
87
131
  end
@@ -1,5 +1,7 @@
1
+ # rubocop:disable Style/ClassAndModuleChildren
1
2
  module Sidekiq
2
3
  class WorkerKiller
3
- VERSION = "0.2.0".freeze
4
+ VERSION = "1.0.0".freeze
4
5
  end
5
6
  end
7
+ # rubocop:enable Style/ClassAndModuleChildren
@@ -1,29 +1,93 @@
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
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?)
7
15
  end
8
16
 
9
17
  describe "#call" do
10
18
  let(:worker){ double("worker") }
11
19
  let(:job){ double("job") }
12
20
  let(:queue){ double("queue") }
21
+
13
22
  it "should yield" do
14
23
  expect { |b|
15
24
  subject.call(worker, job, queue, &b)
16
25
  }.to yield_with_no_args
17
26
  end
27
+
18
28
  context "when current rss is over max rss" do
19
29
  subject{ described_class.new(max_rss: 2) }
30
+
20
31
  before do
21
32
  allow(subject).to receive(:current_rss).and_return(3)
22
33
  end
34
+
23
35
  it "should request shutdown" do
24
36
  expect(subject).to receive(:request_shutdown)
25
37
  subject.call(worker, job, queue){}
26
38
  end
39
+
40
+ it "should call garbage collect" do
41
+ allow(subject).to receive(:request_shutdown)
42
+ expect(GC).to receive(:start).with(full_mark: true, immediate_sweep: true)
43
+ subject.call(worker, job, queue){}
44
+ end
45
+
46
+ context "and skip_shutdown_if is given" do
47
+ subject{ described_class.new(max_rss: 2, skip_shutdown_if: skip_shutdown_proc) }
48
+
49
+ context "and skip_shutdown_if is a proc" do
50
+ let(:skip_shutdown_proc) { proc { |worker| true } }
51
+ it "should NOT request shutdown" do
52
+ expect(subject).not_to receive(:request_shutdown)
53
+ subject.call(worker, job, queue){}
54
+ end
55
+ end
56
+
57
+ context "and skip_shutdown_if is a lambda" do
58
+ let(:skip_shutdown_proc) { ->(worker, job, queue) { true } }
59
+ it "should NOT request shutdown" do
60
+ expect(subject).not_to receive(:request_shutdown)
61
+ subject.call(worker, job, queue){}
62
+ end
63
+ end
64
+
65
+ context "and skip_shutdown_if returns false" do
66
+ let(:skip_shutdown_proc) { proc { |worker, job, queue| false } }
67
+ it "should still request shutdown" do
68
+ expect(subject).to receive(:request_shutdown)
69
+ subject.call(worker, job, queue){}
70
+ end
71
+ end
72
+
73
+ context "and skip_shutdown_if returns nil" do
74
+ let(:skip_shutdown_proc) { proc { |worker, job, queue| nil } }
75
+ it "should still request shutdown" do
76
+ expect(subject).to receive(:request_shutdown)
77
+ subject.call(worker, job, queue){}
78
+ end
79
+ end
80
+ end
81
+
82
+ context "when gc is false" do
83
+ subject{ described_class.new(max_rss: 2, gc: false) }
84
+ it "should not call garbage collect" do
85
+ allow(subject).to receive(:request_shutdown)
86
+ expect(GC).not_to receive(:start)
87
+ subject.call(worker, job, queue){}
88
+ end
89
+ end
90
+
27
91
  context "but max rss is 0" do
28
92
  subject{ described_class.new(max_rss: 0) }
29
93
  it "should not request shutdown" do
@@ -35,29 +99,64 @@ describe Sidekiq::WorkerKiller do
35
99
  end
36
100
 
37
101
  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
42
- 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)
102
+ context "grace time is default" do
103
+ before { allow(subject).to receive(:shutdown){ sleep 0.01 } }
104
+ it "should call shutdown" do
105
+ expect(subject).to receive(:shutdown)
106
+ subject.send(:request_shutdown).join
107
+ end
108
+ it "should not call shutdown twice when called concurrently" do
109
+ expect(subject).to receive(:shutdown).once
110
+ 2.times.map{ subject.send(:request_shutdown) }.each(&:join)
111
+ end
46
112
  end
47
- end
48
113
 
49
- describe "#quiet_signal" do
50
- it "should give TSTP if Sidekiq version is > 5.0" do
51
- stub_const("Sidekiq::VERSION", "5.0")
52
- expect(subject.send :quiet_signal).to eq "TSTP"
53
- stub_const("Sidekiq::VERSION", "5.2.1")
54
- expect(subject.send :quiet_signal).to eq "TSTP"
114
+ context "grace time is 5 seconds" do
115
+ subject{ described_class.new(max_rss: 2, grace_time: 5.0, shutdown_wait: 0) }
116
+ it "should wait the specified grace time before calling shutdown" do
117
+ # there are busy jobs that will not terminate within the grace time
118
+ allow(subject).to receive(:no_jobs_on_quiet_processes?).and_return(false)
119
+
120
+ shutdown_request_time = nil
121
+ shutdown_time = nil
122
+
123
+ # replace the original #request_shutdown to track
124
+ # when the shutdown is requested
125
+ original_request_shutdown = subject.method(:request_shutdown)
126
+ allow(subject).to receive(:request_shutdown) do
127
+ shutdown_request_time= Time.now
128
+ original_request_shutdown.call
129
+ end
130
+
131
+ # track when the process has been required to stop
132
+ expect(sidekiq_process).to receive(:stop!) do |*args|
133
+ shutdown_time = Time.now
134
+ end
135
+
136
+ allow(Process).to receive(:kill)
137
+ allow(Process).to receive(:pid).and_return(99)
138
+
139
+ subject.send(:request_shutdown).join
140
+
141
+ elapsed_time = shutdown_time - shutdown_request_time
142
+
143
+ # the elapsed time beetween shutdown request and the actual
144
+ # shutdown signal should be greater than the specificed grace_time
145
+ expect(elapsed_time).to be >= 5.0
146
+ end
55
147
  end
56
- it "should give USR1 if Sidekiq version is < 5.0" do
57
- stub_const("Sidekiq::VERSION", "3.0")
58
- expect(subject.send :quiet_signal).to eq "USR1"
59
- stub_const("Sidekiq::VERSION", "4.6.7")
60
- expect(subject.send :quiet_signal).to eq "USR1"
148
+
149
+ context "grace time is Float::INFINITY" do
150
+ subject{ described_class.new(max_rss: 2, grace_time: Float::INFINITY, shutdown_wait: 0) }
151
+ it "call signal only on jobs" do
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!)
156
+ expect(Process).to receive(:kill).with('SIGKILL', 99)
157
+
158
+ subject.send(:request_shutdown).join
159
+ end
61
160
  end
62
161
  end
63
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.2.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: 2018-04-18 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,42 +30,42 @@ 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
- 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
@@ -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.6.14
99
+ rubygems_version: 3.1.0.pre3
101
100
  signing_key:
102
101
  specification_version: 4
103
102
  summary: Sidekiq worker killer