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 +5 -5
- data/README.md +40 -7
- data/lib/sidekiq/worker_killer.rb +108 -64
- data/lib/sidekiq/worker_killer/version.rb +3 -1
- data/spec/sidekiq/worker_killer_spec.rb +119 -20
- metadata +11 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f40045c5805f35f8792f66c5dac896042c3b50b7b8c42f0f95e14827d4184a5a
|
4
|
+
data.tar.gz: 5721815ad342ec06d6231c005b60cb0bbda5300343a4bfb99b9b138fff8f6627
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
36
|
-
| shutdown_wait | 30 seconds | when the grace time expires, still running jobs get 30 seconds to
|
37
|
-
| kill_signal | SIGKILL | Signal to use kill Sidekiq process if it doesn't
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
68
|
+
private
|
12
69
|
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
#
|
25
|
-
|
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
|
-
|
83
|
+
def shutdown
|
84
|
+
warn "sending quiet to #{identity}"
|
85
|
+
sidekiq_process.quiet!
|
31
86
|
|
32
|
-
|
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
|
-
|
42
|
-
|
43
|
-
signal(quiet_signal, pid)
|
89
|
+
warn "shutting down #{identity} in #{@grace_time} seconds"
|
90
|
+
wait_job_finish_in_grace_time
|
44
91
|
|
45
|
-
|
46
|
-
|
92
|
+
warn "stopping #{identity}"
|
93
|
+
sidekiq_process.stop!
|
47
94
|
|
48
|
-
|
49
|
-
|
95
|
+
warn "waiting #{@shutdown_wait} seconds before sending " \
|
96
|
+
"#{@kill_signal} to #{identity}"
|
97
|
+
sleep(@shutdown_wait)
|
50
98
|
|
51
|
-
|
52
|
-
|
53
|
-
|
99
|
+
warn "sending #{@kill_signal} to #{identity}"
|
100
|
+
::Process.kill(@kill_signal, ::Process.pid)
|
101
|
+
end
|
54
102
|
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
60
|
-
|
61
|
-
end
|
108
|
+
def grace_time_exceeded?(start)
|
109
|
+
return false if @grace_time == Float::INFINITY
|
62
110
|
|
63
|
-
|
64
|
-
|
65
|
-
end
|
111
|
+
start + @grace_time < Time.now
|
112
|
+
end
|
66
113
|
|
67
|
-
|
68
|
-
|
69
|
-
|
114
|
+
def jobs_finished?
|
115
|
+
sidekiq_process.stopping? && sidekiq_process["busy"] == 0
|
116
|
+
end
|
70
117
|
|
71
|
-
|
72
|
-
|
73
|
-
|
118
|
+
def current_rss
|
119
|
+
::GetProcessMem.new.mb
|
120
|
+
end
|
74
121
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
84
|
-
|
85
|
-
end
|
128
|
+
def warn(msg)
|
129
|
+
Sidekiq.logger.warn(msg)
|
86
130
|
end
|
87
131
|
end
|
@@ -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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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.
|
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:
|
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: '
|
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: '
|
40
|
+
version: '5'
|
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
|
@@ -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
|
-
|
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
|