worker_killer 1.0.5.213889 → 1.1.0.214146

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
  SHA256:
3
- metadata.gz: 1d0558051a55e944e97bb26729239a97d7f150fb37885af8761246f78db4e280
4
- data.tar.gz: 417bb8c4f0c4818d4e2a19ad4f2cb8cc5e3ca82621b925c81c2e4b33102803fb
3
+ metadata.gz: eabc2f6b314a0e9a0e81a078ac3954c5355e24017373fad26f4de4fbeea2f00f
4
+ data.tar.gz: deb1ebcf899be03574b4611aee0b3f8e08f4c6f57e5babddeaa2bc5d20f70ed7
5
5
  SHA512:
6
- metadata.gz: 855f6852cb2c7c8115388038f07d234958a6183c008d21b28ad8867a34a53dcfd713f34645c1b0f435fafc67f5f7ad0c1272b7b4f94f2d91b170ca91c0f97133
7
- data.tar.gz: 8699bc63da513186e47cb1ef626f732e61cfe7a603231370223cbcea5ec3b6d8897a314ed5fe8dee9a23765822b4c20aa43ddc6fda7b02bb775f01aa1f838a7e
6
+ metadata.gz: 80da7063d7ab1d3ce70baf42414e7dc6aea1310d85285cc10c95c9a09d980b0b8db98438985ffe422e82b20c4aab73ea05c717924dac47442c4aed1f257a1fae
7
+ data.tar.gz: 0e40fd946bb0a0f5e4cdb36e469738c195c555e4495e379f943a90fb336166f4f56e09a7a60ffafea782a597979db79d21123ded769c4412843d8f05397aa12d
data/README.md CHANGED
@@ -17,7 +17,7 @@ Features:
17
17
 
18
18
  - generic middleware implementation
19
19
  - Phusion Passenger support(through `passenger-config detach-process <PID>`)
20
- - Puma phased-restart support(through `pumactl phased-restart`)
20
+ - Puma support through native plugin
21
21
  - DelayedJob support
22
22
  - custom reaction hook
23
23
 
@@ -82,33 +82,26 @@ Add these lines to your `puma.rb` AND `application.rb`.
82
82
  ```ruby
83
83
  # puma.rb
84
84
 
85
- on_worker_boot do |*args|
86
- require 'worker_killer/killer'
87
- require 'active_support/notifications'
88
-
89
- ::ActiveSupport::Notifications.subscribe 'initialize.custom.rails' do |*eargs|
90
- event = ActiveSupport::Notifications::Event.new(*eargs)
91
- config = event.payload[:config]
92
-
93
- killer = ::WorkerKiller::Killer::Puma.new
94
- config.middleware.insert_before(
95
- ::Rack::Runtime,
96
- ::WorkerKiller::Middleware::RequestsLimiter, killer: killer, min: 3072, max: 4096
97
- )
98
-
99
- config.middleware.insert_before(
100
- ::Rack::Runtime,
101
- ::WorkerKiller::Middleware::OOMLimiter, killer: killer, min: nil, max: 0.5, verbose: true, check_cycle: 16
102
- )
103
- end
104
- end
85
+ require 'worker_killer/puma/plugin/worker_killer'
86
+ plugin('worker_killer')
105
87
 
106
88
  # application.rb
89
+ if defined?(::Puma::Client)
90
+ killer = ::WorkerKiller::PumaPlugin.instance.killer
91
+
92
+ config.middleware.insert_before(
93
+ Rack::Runtime,
94
+ WorkerKiller::Middleware::OOMLimiter, killer: killer, min: nil, max: 0.5, verbose: false, check_cycle: 16
95
+ )
107
96
 
108
- ActiveSupport::Notifications.instrument 'initialize.custom.rails', config: config
97
+ config.middleware.insert_before(
98
+ Rack::Runtime,
99
+ WorkerKiller::Middleware::RequestsLimiter, killer: killer, min: 3000, max: 4000, verbose: false
100
+ )
101
+ end
109
102
  ```
110
103
 
111
- This gem provides two modules: WorkerKiller::CountLimiter and WorkerKiller::MemoryLimiter, some Rack integration and DelayedJob plugin.
104
+ This gem provides two modules: WorkerKiller::CountLimiter and WorkerKiller::MemoryLimiter, some Rack integration, DelayedJob plugin and Puma plugin.
112
105
 
113
106
  ### WorkerKiller::Middleware::RequestsLimiter and WorkerKiller::DelayedJobPlugin::JobsLimiter
114
107
 
@@ -128,6 +121,29 @@ The memory size check is done in every `check_cycle` requests.
128
121
 
129
122
  If `verbose` is set to true, then every memory size check will be shown in your logs. This logging is done at the `info` level.
130
123
 
124
+ ### WorkerKiller::PumaPlugin
125
+
126
+ Works in Puma `Cluster Mode`. Work correctly with `phased-restart` and `fork_workers`, but has maximum effectivity (memory usage) in simple `Cluster Mode` with **preload_app!**.
127
+
128
+ Example production config:
129
+
130
+ ```ruby
131
+ port ENV.fetch('PUMA_PORT', 3000).to_i
132
+
133
+ environment ENV.fetch('RAILS_ENV', 'development')
134
+
135
+ pidfile ENV.fetch('PUMA_PIDFILE', 'tmp/pids/server.pid')
136
+
137
+ threads ENV.fetch('PUMA_THREADS', 4).to_i
138
+
139
+ workers ENV.fetch('PUMA_WORKERS', 3).to_i
140
+
141
+ require 'worker_killer/puma/plugin/worker_killer'
142
+ plugin('worker_killer')
143
+
144
+ preload_app!
145
+ ```
146
+
131
147
  # Special Thanks
132
148
 
133
149
  - [@hotchpotch](http://github.com/hotchpotch/) for the [original idea](https://gist.github.com/hotchpotch/1258681)
@@ -6,13 +6,19 @@ module WorkerKiller
6
6
  def initialize(min: 3072, max: 4096, verbose: false)
7
7
  @min = min
8
8
  @max = max
9
- @limit = @min + WorkerKiller.randomize(@max - @min + 1)
10
- @left = @limit
9
+ @limit = nil
10
+ @left = nil
11
11
  @verbose = verbose
12
12
  end
13
13
 
14
14
  def check
15
- return nil if @limit <= 1
15
+ # initialize limits on first check
16
+ if @limit.nil?
17
+ @limit = min + WorkerKiller.randomize(max - min + 1)
18
+ @left = @limit
19
+ end
20
+
21
+ return nil if @limit < 1
16
22
 
17
23
  @started_at ||= Time.now
18
24
 
@@ -23,10 +23,10 @@ module WorkerKiller
23
23
 
24
24
  def configure_lifecycle(lifecycle)
25
25
  # Count condition after every job
26
- lifecycle.after(:perform) do |worker, *_args|
26
+ lifecycle.after(:perform) do |_worker, *_args|
27
27
  @time_to_burn ||= limiter.check
28
28
  end
29
-
29
+
30
30
  # Stop execution only after whole loop completed
31
31
  lifecycle.after(:loop) do |worker, *_args|
32
32
  @time_to_burn ||= limiter.check
@@ -42,7 +42,7 @@ module WorkerKiller
42
42
  end
43
43
 
44
44
  def self.check_passenger_config! path
45
- if path = check_passenger_config(path)
45
+ if (path = check_passenger_config(path))
46
46
  path
47
47
  else
48
48
  raise "Can't find passenger config at #{path.inspect}"
@@ -1,29 +1,28 @@
1
+ require 'socket'
2
+
1
3
  module ::WorkerKiller
2
4
  module Killer
3
5
  class Puma < ::WorkerKiller::Killer::Base
4
6
 
5
- def initialize **kwrags
6
- super
7
+ attr_accessor :ipc_path, :worker_num
8
+
9
+ def initialize(ipc_path:, worker_num: nil, **kwargs)
10
+ super(**kwargs)
11
+ @ipc_path = ipc_path
12
+ @worker_num = worker_num
7
13
  end
8
14
 
9
15
  def do_kill(sig, pid, alive_sec, **_params)
10
- cmd = 'pumactl phased-restart'
11
-
12
16
  if sig == :KILL
13
- logger.error "#{self} force to kill self (pid: #{pid}) alive: #{alive_sec} sec (trial #{kill_attempts})"
17
+ logger.error "#{self} force to kill self[#{worker_num}] (pid: #{pid}) alive: #{alive_sec} sec (trial #{kill_attempts})"
14
18
  Process.kill sig, pid
15
19
  return
16
20
  end
17
21
 
18
- return if @already_detached
19
-
20
- logger.warn "#{self} run #{cmd.inspect} (pid: #{pid}) alive: #{alive_sec} sec (trial #{kill_attempts})"
21
- @already_detached = true
22
+ logger.warn "#{self} send #{worker_num} to Puma Plugin (pid: #{pid}) alive: #{alive_sec} sec (trial #{kill_attempts})"
22
23
 
23
- Thread.new(cmd) do |command|
24
- unless Kernel.system(command)
25
- logger.warn "#{self} run #{command.inspect} failed: #{$?.inspect}"
26
- end
24
+ Socket.unix(ipc_path) do |sock|
25
+ sock.puts Integer(worker_num).to_s
27
26
  end
28
27
  end
29
28
 
@@ -2,7 +2,7 @@ module WorkerKiller
2
2
  module Killer
3
3
  class Base
4
4
 
5
- attr_accessor :config, :kill_attempts, :logger
5
+ attr_accessor :config, :kill_attempts
6
6
 
7
7
  def initialize(logger: nil, **_kwargs)
8
8
  @logger = logger
@@ -27,7 +27,7 @@ module WorkerKiller
27
27
  raise 'Not Implemented'
28
28
  end
29
29
  # :nocov:
30
-
30
+
31
31
  def logger
32
32
  @logger || WorkerKiller.configuration.logger
33
33
  end
@@ -10,8 +10,6 @@ module WorkerKiller
10
10
  # set static memory limits
11
11
  @min = min
12
12
  @max = max
13
- @limit = @min + WorkerKiller.randomize(@max - @min + 1)
14
- @limit_mb = (@limit / 1024 / 1024).round(1)
15
13
  else
16
14
  # prepare for relative memory limits
17
15
  @max_percent = max
@@ -26,11 +24,19 @@ module WorkerKiller
26
24
  @started_at ||= Time.now
27
25
  @check_count += 1
28
26
 
27
+
29
28
  return nil if (@check_count % @check_cycle) != 0
30
29
 
31
30
  rss = GetProcessMem.new.bytes
31
+
32
32
  # initialize relative memory limits on first check
33
- set_limits(rss, rss + rss * @max_percent) if min.nil?
33
+ if @limit.nil?
34
+ if min.nil?
35
+ set_limits(rss, rss + rss * @max_percent)
36
+ else
37
+ set_limits(min, min + WorkerKiller.randomize(max - min + 1))
38
+ end
39
+ end
34
40
 
35
41
  do_check(rss)
36
42
  end
@@ -52,7 +58,8 @@ module WorkerKiller
52
58
 
53
59
  def set_limits(min, max)
54
60
  @min = min
55
- @limit = @max = max
61
+ @limit = max
62
+ @max ||= max
56
63
  @limit_mb = (@limit / 1024 / 1024).round(1)
57
64
  end
58
65
 
@@ -0,0 +1,13 @@
1
+ require 'worker_killer'
2
+ require 'worker_killer/puma_plugin'
3
+
4
+ Puma::Plugin.create do
5
+ def config(cfg)
6
+ ::WorkerKiller::PumaPlugin.instance.config(cfg)
7
+ end
8
+
9
+ def start(launcher)
10
+ ::WorkerKiller::PumaPlugin.instance.start(launcher)
11
+ end
12
+ end
13
+
@@ -0,0 +1,80 @@
1
+ require 'singleton'
2
+
3
+ require 'worker_killer/memory_limiter'
4
+ require 'worker_killer/count_limiter'
5
+
6
+
7
+ module WorkerKiller
8
+ class PumaPlugin
9
+
10
+ include Singleton
11
+
12
+ attr_accessor :ipc_path, :killer, :thread
13
+
14
+ def initialize
15
+ @ipc_path = File.join('tmp', "puma_worker_killer_#{Process.pid}.socket")
16
+ @killer = ::WorkerKiller::Killer::Puma.new(worker_num: nil, ipc_path: ipc_path)
17
+ log "Initializing IPC: #{@ipc_path}"
18
+ end
19
+
20
+ def config(puma)
21
+ puma.on_worker_boot do |num|
22
+ log "Set worker_num: #{num}"
23
+ @killer.worker_num = num
24
+ end
25
+ end
26
+
27
+ def start(launcher)
28
+ @runner = launcher.instance_variable_get('@runner')
29
+
30
+ launcher.events.on_booted do
31
+ @thread ||= start_ipc_listener
32
+ end
33
+ end
34
+
35
+ def start_ipc_listener
36
+ log 'Start IPC listener'
37
+ Thread.new do
38
+ Socket.unix_server_loop(ipc_path) do |sock, *args|
39
+ if (line = sock.gets)
40
+ worker_num = Integer(line.strip)
41
+ if (worker = find_worker(worker_num))
42
+ log "Killing worker #{worker_num}"
43
+ worker.term!
44
+ end
45
+ end
46
+ rescue StandardError => e
47
+ log("Exception: #{e.inspect}")
48
+ ensure
49
+ sock.close
50
+ end
51
+ end
52
+ end
53
+
54
+ def find_worker(worker_num)
55
+ worker = @runner.worker_at(worker_num)
56
+ unless worker
57
+ log "Unknown worker index: #{worker_num.inspect}. Skipping."
58
+ return nil
59
+ end
60
+
61
+ unless worker.booted?
62
+ log "Worker #{worker_num.inspect} is not booted yet. Skipping."
63
+ return nil
64
+ end
65
+
66
+ if worker.term?
67
+ log "Worker #{worker_num.inspect} already terminating. Skipping."
68
+ return nil
69
+ end
70
+
71
+ worker
72
+ end
73
+
74
+ def log(msg)
75
+ warn("#{self.class}[#{Process.pid}]: #{msg}")
76
+ end
77
+
78
+ end
79
+ end
80
+
@@ -2,7 +2,7 @@
2
2
 
3
3
  module WorkerKiller
4
4
 
5
- VERSION = '1.0.5'
5
+ VERSION = '1.1.0'
6
6
 
7
7
  end
8
8
 
@@ -4,21 +4,30 @@ RSpec.describe WorkerKiller::CountLimiter do
4
4
  let(:max){ min + rand(100) }
5
5
  let(:options){ { min: min, max: max, verbose: true } }
6
6
 
7
- it { is_expected.to have_attributes(min: min, max: max, limit: a_value_between(min, max), left: subject.limit) }
7
+ it { is_expected.to have_attributes(min: min, max: max, limit: nil, left: nil) }
8
8
 
9
- it 'expect not to react while less than limit' do
10
- (subject.limit - 1).times do
11
- expect(subject.check).to be_falsey
12
- end
13
- end
9
+ context 'initialize limits after first check' do
10
+ before { subject.check }
11
+
12
+ it {
13
+ is_expected.to have_attributes(min: min, max: max,
14
+ limit: a_value_between(min, max), left: subject.limit - 1)
15
+ }
14
16
 
15
- it 'expect call reaction when check succeded' do
16
- (subject.limit - 1).times do
17
- expect(subject.check).to be_falsey
17
+ it 'expect not to react while less than limit' do
18
+ (subject.limit - 2).times do
19
+ expect(subject.check).to be_falsey
20
+ end
18
21
  end
19
22
 
20
- expect(subject.check).to be_truthy
21
- expect(subject.check).to be_truthy
23
+ it 'expect call reaction when check succeded' do
24
+ (subject.limit - 2).times do
25
+ expect(subject.check).to be_falsey
26
+ end
27
+
28
+ expect(subject.check).to be_truthy
29
+ expect(subject.check).to be_truthy
30
+ end
22
31
  end
23
32
  end
24
33
 
@@ -6,7 +6,9 @@ RSpec.describe WorkerKiller::Killer::Puma do
6
6
  end
7
7
  end
8
8
 
9
- let(:killer){ described_class.new }
9
+ let(:ipc_path) { '/tmp/test_ipx.sock' }
10
+ let(:killer){ described_class.new(ipc_path: ipc_path, worker_num: 99) }
11
+ let(:buffer) { StringIO.new }
10
12
 
11
13
  describe '#kill' do
12
14
  around do |example|
@@ -18,11 +20,11 @@ RSpec.describe WorkerKiller::Killer::Puma do
18
20
  end
19
21
 
20
22
  it 'expect right signal order' do
21
- expect(Kernel).to receive(:system).with('pumactl phased-restart').and_return(true)
23
+ expect(Socket).to receive(:unix).with(ipc_path).and_yield(buffer).exactly(4).times
22
24
  expect(Process).to receive(:kill).with(:KILL, anything).exactly(5).times
23
25
 
24
- thread = killer.kill(Time.now)
25
- thread.join
26
+ killer.kill(Time.now)
27
+ expect(buffer.string.strip).to eq(99.to_s)
26
28
 
27
29
  1.times { killer.kill(Time.now) } # 1 QUIT
28
30
  2.times { killer.kill(Time.now) } # 1 TERM
@@ -22,7 +22,7 @@ RSpec.describe WorkerKiller::MemoryLimiter do
22
22
  let(:min){ rand(50..100) * mb }
23
23
  let(:max){ min + rand(100) * mb }
24
24
 
25
- it { is_expected.to have_attributes(min: min, max: max, limit: a_value_between(min, max)) }
25
+ it { is_expected.to have_attributes(min: min, max: max, limit: nil) }
26
26
 
27
27
  it 'expect to skip check while less than cycle count' do
28
28
  expect(GetProcessMem).not_to receive(:new)
@@ -30,6 +30,15 @@ RSpec.describe WorkerKiller::MemoryLimiter do
30
30
  skip_cycles(subject, check_cycle)
31
31
  end
32
32
 
33
+ it 'expect to initialize limits after cycle count' do
34
+ expect(memory).to receive(:bytes).and_return(min)
35
+ is_expected.to have_attributes(min: min, max: max, limit: nil)
36
+
37
+ skip_cycles(subject, check_cycle)
38
+ subject.check
39
+ is_expected.to have_attributes(min: min, max: max, limit: a_value_between(min, max))
40
+ end
41
+
33
42
  it 'expect to skip check after cycle count reached' do
34
43
  expect(memory).to receive(:bytes).and_return(min - 1)
35
44
 
@@ -38,8 +47,12 @@ RSpec.describe WorkerKiller::MemoryLimiter do
38
47
  end
39
48
 
40
49
  it 'expect call reaction when check succeded' do
41
- expect(memory).to receive(:bytes).and_return(subject.limit + 1)
50
+ expect(memory).to receive(:bytes).and_return(min)
51
+
52
+ skip_cycles(subject, check_cycle)
53
+ subject.check
42
54
 
55
+ expect(memory).to receive(:bytes).and_return(subject.limit + 1)
43
56
  skip_cycles(subject, check_cycle)
44
57
  expect(subject.check).to be_truthy
45
58
  end
@@ -0,0 +1,34 @@
1
+ RSpec.describe WorkerKiller::PumaPlugin do
2
+ subject(:plugin){ described_class.instance }
3
+ let(:puma) { double }
4
+ let(:runner){ double }
5
+ let(:events) { double }
6
+ let(:launcher){ double('@runner' => runner, 'events' => events) }
7
+ let(:worker){ double('booted?' => true, 'term?' => false) }
8
+
9
+ it {
10
+ is_expected.to have_attributes(ipc_path: /puma_worker_.*socket/,
11
+ killer: ::WorkerKiller::Killer::Puma)
12
+ }
13
+
14
+ context 'Puma initialization' do
15
+ it do
16
+ expect(puma).to receive(:on_worker_boot)
17
+ plugin.config(puma)
18
+ end
19
+
20
+ it do
21
+ launcher.instance_variable_set('@runner', runner)
22
+
23
+ expect(Socket).to receive(:unix_server_loop).with(/puma_worker_.*socket/).and_yield(StringIO.new('99'))
24
+ expect(events).to receive(:on_booted).and_yield
25
+ expect(runner).to receive(:worker_at).with(99).and_return(worker)
26
+ expect(plugin).to receive(:find_worker).with(99).and_call_original
27
+ expect(worker).to receive(:term!)
28
+
29
+ plugin.start(launcher)
30
+ plugin.thread.join
31
+ end
32
+ end
33
+ end
34
+
data/spec/spec_helper.rb CHANGED
@@ -30,6 +30,7 @@ SimpleCov.start
30
30
 
31
31
  require 'worker_killer'
32
32
  require 'worker_killer/delayed_job_plugin'
33
+ require 'worker_killer/puma_plugin'
33
34
 
34
35
  $root = File.join(File.dirname(__dir__), 'spec')
35
36
  Dir[File.join(__dir__, 'support', '**', '*.rb')].sort.each {|f| require f }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: worker_killer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5.213889
4
+ version: 1.1.0.214146
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samoilenko Yuri
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-22 00:00:00.000000000 Z
11
+ date: 2024-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: get_process_mem
@@ -135,6 +135,8 @@ files:
135
135
  - lib/worker_killer/killer/signal.rb
136
136
  - lib/worker_killer/memory_limiter.rb
137
137
  - lib/worker_killer/middleware.rb
138
+ - lib/worker_killer/puma/plugin/worker_killer.rb
139
+ - lib/worker_killer/puma_plugin.rb
138
140
  - lib/worker_killer/version.rb
139
141
  - spec/count_limiter_spec.rb
140
142
  - spec/delayed_job_plugin/jobs_limiter_spec.rb
@@ -146,6 +148,7 @@ files:
146
148
  - spec/killer_spec.rb
147
149
  - spec/memory_limiter_spec.rb
148
150
  - spec/middleware_spec.rb
151
+ - spec/puma_plugin_spec.rb
149
152
  - spec/spec_helper.rb
150
153
  - spec/support/logger.rb
151
154
  - spec/worker_killer_spec.rb
@@ -173,16 +176,17 @@ signing_key:
173
176
  specification_version: 4
174
177
  summary: Kill any workers by memory and request counts or take custom reaction
175
178
  test_files:
176
- - spec/killer/signal_spec.rb
177
- - spec/killer/puma_spec.rb
178
- - spec/killer/delayed_job_spec.rb
179
- - spec/killer/passenger_spec.rb
180
- - spec/killer_spec.rb
181
- - spec/count_limiter_spec.rb
182
- - spec/worker_killer_spec.rb
183
- - spec/support/logger.rb
184
179
  - spec/middleware_spec.rb
180
+ - spec/killer_spec.rb
185
181
  - spec/spec_helper.rb
182
+ - spec/puma_plugin_spec.rb
186
183
  - spec/delayed_job_plugin/oom_limiter_spec.rb
187
184
  - spec/delayed_job_plugin/jobs_limiter_spec.rb
185
+ - spec/killer/passenger_spec.rb
186
+ - spec/killer/puma_spec.rb
187
+ - spec/killer/signal_spec.rb
188
+ - spec/killer/delayed_job_spec.rb
188
189
  - spec/memory_limiter_spec.rb
190
+ - spec/support/logger.rb
191
+ - spec/worker_killer_spec.rb
192
+ - spec/count_limiter_spec.rb