puma_worker_killer 0.0.6 → 0.0.7

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
  SHA1:
3
- metadata.gz: 948c271c454d3a1dc1b2bccaf270bcba98de73f8
4
- data.tar.gz: 14b5ed3e5b5c28b03df31e23dc7a1047d1965a30
3
+ metadata.gz: 20389b0e2ba67ab795071554a0e52b6599cd3612
4
+ data.tar.gz: 0789e6a76e4c12394df935bd9b16b503d6990a62
5
5
  SHA512:
6
- metadata.gz: d47c3c52bca9383936074705a88ef19e62751c183b0f086a1f004970655e5bf3b9bc934a2706013a4af3a69b160ad986371dfa36105b52acee0d6a981481f6a1
7
- data.tar.gz: 683b00d578d35bc61b3450acbfb2276f438208b99f50e7951650c2348d2bdc0fc2570a5bf4f52b446131e80dc61ff21d39821c87c1cd6fe3e2768ff20cd2e58a
6
+ metadata.gz: 62aa47f0f5032df0d0887e8001143059b4107fe1871932b545d4e56820c0012260ea73ffb77aed9be3fe83a1a3ec08b2f23633a05f96d8375885b3cdf1e05454
7
+ data.tar.gz: 30fedd4919738c70432d7de4861089c03212c651805d4f564cd83e16d46f3b1d9bfc60ce44ae03a71cd1dff15b7547fbfb757b6b26d5e894f577ab0765669cad
data/.travis.yml CHANGED
@@ -7,6 +7,8 @@ rvm:
7
7
  - 2.3.0
8
8
  - ruby-head
9
9
  - rbx
10
+ before_install:
11
+ - gem install bundler -v 1.12.5
10
12
 
11
13
  matrix:
12
14
  allow_failures:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.0.7
2
+
3
+ - Logging is configurable #41
4
+
1
5
  ## 0.0.6
2
6
 
3
7
  - Log PID of worker insead of inspecting the worker #33
data/README.md CHANGED
@@ -29,15 +29,68 @@ Then run `$ bundle install`
29
29
 
30
30
  -->
31
31
 
32
- Somewhere in your main process run this code:
32
+ ## Turn on Rolling Restarts
33
+
34
+ A rolling restart will kill each of your workers on a rolling basis. You set the frequency which it conducts the restart. This is a simple way to keep memory down as Ruby web programs generally increase memory usage over time. If you're using Heroku [it is difficult to measure RAM from inside of a container accurately](https://github.com/schneems/get_process_mem/issues/7), so it is recommended to use this feature or use a [log-drain-based worker killer](https://github.com/arches/whacamole). You can enable roling restarts by running:
33
35
 
34
36
  ```ruby
35
- # config/initializers/puma_worker_killer.rb
36
- PumaWorkerKiller.start
37
+ before_fork do
38
+ require 'puma_worker_killer'
39
+
40
+ PumaWorkerKiller.enable_rolling_restart # Default is every 6 hours
41
+ end
42
+
43
+ ```
44
+
45
+ or you can pass in the restart frequency:
46
+
47
+ ```ruby
48
+ PumaWorkerKiller.enable_rolling_restart(12 * 3600) # 12 hours in seconds
49
+ ```
50
+
51
+ Make sure if you do this to not accidentally call `PumaWorkerKiller.start` as well.
52
+
53
+ ## Enable Worker Killing
54
+
55
+ If you're not running on a containerized platform you can try to detect the amount of memory you're using and only kill Puma workers when you're over that limit. It may allow you to go for longer periods of time without killing a worker however it is more error prone than rolling restarts. To enable measurement based worker killing put this in your `config/puma.rb`:
56
+
57
+ ```ruby
58
+ # config/puma.rb
59
+
60
+ before_fork do
61
+ require 'puma_worker_killer'
62
+
63
+ PumaWorkerKiller.start
64
+ end
37
65
  ```
38
66
 
39
67
  That's it. Now on a regular basis the size of all Puma and all of it's forked processes will be evaluated and if they're over the RAM threshold will be killed. Don't worry Puma will notice a process is missing a spawn a fresh copy with a much smaller RAM footprint ASAP.
40
68
 
69
+ ## Troubleshooting
70
+
71
+ When you boot your program locally you should see debug output:
72
+
73
+ ```
74
+ [77773] Puma starting in cluster mode...
75
+ [77773] * Version 3.1.0 (ruby 2.3.1-p112), codename: El Niño Winter Wonderland
76
+ [77773] * Min threads: 0, max threads: 16
77
+ [77773] * Environment: development
78
+ [77773] * Process workers: 2
79
+ [77773] * Phased restart available
80
+ [77773] * Listening on tcp://0.0.0.0:9292
81
+ [77773] Use Ctrl-C to stop
82
+ [77773] PumaWorkerKiller: Consuming 54.34765625 mb with master and 2 workers.
83
+ ```
84
+
85
+ If you don't see any `PumaWorkerKiller` output, make sure that you are running with multiple workers. PWK only functions if you have workers enabled, you should see something like this when Puma boots:
86
+
87
+ ```
88
+ [77773] * Process workers: 2
89
+ ```
90
+
91
+ If you've configured PWK's frequency try reducing it to a very low value
92
+
93
+
41
94
  ## Configure
42
95
 
43
96
  Before calling `start` you can configure `PumaWorkerKiller`. You can do so using a configure block or calling methods directly:
@@ -48,13 +101,17 @@ PumaWorkerKiller.config do |config|
48
101
  config.frequency = 5 # seconds
49
102
  config.percent_usage = 0.98
50
103
  config.rolling_restart_frequency = 12 * 3600 # 12 hours in seconds
104
+ config.reaper_status_logs = true # setting this to false will not log lines like:
105
+ # PumaWorkerKiller: Consuming 54.34765625 mb with master and 2 workers.
51
106
  end
52
107
  PumaWorkerKiller.start
53
108
  ```
54
109
 
55
110
  ## Attention
56
- If you start puma as a daemon, to add puma worker killer config into puma config file, rather than into initializers:
57
- Sample like this: (in puma.rb file)
111
+
112
+ If you start puma as a daemon, to add puma worker killer config into puma config file, rather than into initializers:
113
+ Sample like this: (in `config/puma.rb` file):
114
+
58
115
  ```ruby
59
116
  before_fork do
60
117
  PumaWorkerKiller.config do |config|
@@ -93,27 +150,18 @@ PumaWorkerKiller.rolling_restart_frequency = 12 * 3600 # 12 hours in seconds
93
150
 
94
151
  By default PumaWorkerKiller will perform a rolling restart of all your worker processes every 12 hours. To disable, set to `false`.
95
152
 
96
- ## Only turn on Rolling Restarts
97
-
98
- If you're running on a platform like [Heroku where it is difficult to measure RAM from inside of a container accurately](https://github.com/schneems/get_process_mem/issues/7), you may want to disable the "worker killer" functionality and only use the rolling restart. You can do that by running:
153
+ You may want to hide the following log lines: `PumaWorkerKiller: Consuming 54.34765625 mb with master and 2 workers.`. To do that set:
99
154
 
100
155
  ```ruby
101
- PumaWorkerKiller.enable_rolling_restart # Default is every 6 hours
156
+ PumaWorkerKiller.reaper_status_logs = false
102
157
  ```
103
158
 
104
- or you can pass in the restart frequency
105
-
106
- ```ruby
107
- PumaWorkerKiller.enable_rolling_restart(12 * 3600) # 12 hours in seconds
108
- ```
109
-
110
- Make sure if you do this to not accidentally call `PumaWorkerKiller.start` as well.
159
+ Note: It is `true` by default.
111
160
 
112
161
  ## License
113
162
 
114
163
  MIT
115
164
 
116
-
117
165
  ## Feedback
118
166
 
119
167
  Open up an issue or ping me on twitter [@schneems](http://twitter.com/schneems).
@@ -11,8 +11,8 @@ module PumaWorkerKiller
11
11
 
12
12
  Thread.new do
13
13
  while @running
14
- @reaper.reap
15
14
  sleep @timeout
15
+ @reaper.reap
16
16
  end
17
17
  end
18
18
  end
@@ -18,6 +18,10 @@ module PumaWorkerKiller
18
18
  # rescue Errno::ECHILD
19
19
  end
20
20
 
21
+ def workers_stopped?
22
+ !running?
23
+ end
24
+
21
25
  def running?
22
26
  @master && workers.any?
23
27
  end
@@ -1,8 +1,9 @@
1
1
  module PumaWorkerKiller
2
2
  class Reaper
3
- def initialize(max_ram, master = nil)
3
+ def initialize(max_ram, master = nil, reaper_status_logs = true)
4
4
  @cluster = PumaWorkerKiller::PumaMemory.new(master)
5
5
  @max_ram = max_ram
6
+ @reaper_status_logs = reaper_status_logs
6
7
  end
7
8
 
8
9
  # used for tes
@@ -11,11 +12,11 @@ module PumaWorkerKiller
11
12
  end
12
13
 
13
14
  def reap
14
- return false unless @cluster.running?
15
+ return false if @cluster.workers_stopped?
15
16
  if (total = get_total_memory) > @max_ram
16
17
  @cluster.master.log "PumaWorkerKiller: Out of memory. #{@cluster.workers.count} workers consuming total: #{total} mb out of max: #{@max_ram} mb. Sending TERM to pid #{@cluster.largest_worker.pid} consuming #{@cluster.largest_worker_memory} mb."
17
18
  @cluster.term_largest_worker
18
- else
19
+ elsif @reaper_status_logs
19
20
  @cluster.master.log "PumaWorkerKiller: Consuming #{total} mb with master and #{@cluster.workers.count} workers."
20
21
  end
21
22
  end
@@ -1,3 +1,3 @@
1
1
  module PumaWorkerKiller
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
@@ -3,18 +3,19 @@ require 'get_process_mem'
3
3
  module PumaWorkerKiller
4
4
  extend self
5
5
 
6
- attr_accessor :ram, :frequency, :percent_usage, :rolling_restart_frequency
6
+ attr_accessor :ram, :frequency, :percent_usage, :rolling_restart_frequency, :reaper_status_logs
7
7
  self.ram = 512 # mb
8
8
  self.frequency = 10 # seconds
9
9
  self.percent_usage = 0.99 # percent of RAM to use
10
10
  self.rolling_restart_frequency = 6 * 3600 # 6 hours in seconds
11
+ self.reaper_status_logs = true
11
12
 
12
13
  def config
13
14
  yield self
14
15
  end
15
16
 
16
- def reaper(ram = self.ram, percent = self.percent_usage)
17
- Reaper.new(ram * percent_usage)
17
+ def reaper(ram = self.ram, percent = self.percent_usage, reaper_status_logs = self.reaper_status_logs)
18
+ Reaper.new(ram * percent_usage, nil, reaper_status_logs)
18
19
  end
19
20
 
20
21
  def start(frequency = self.frequency, reaper = self.reaper)
@@ -21,6 +21,7 @@ Gem::Specification.new do |gem|
21
21
  gem.add_dependency "puma", ">= 2.7", "< 4"
22
22
  gem.add_dependency "get_process_mem", "~> 0.2"
23
23
  gem.add_development_dependency "rack", "~> 1.6"
24
+ gem.add_development_dependency "wait_for_it", "~> 0.1"
24
25
  gem.add_development_dependency "rake", "~> 10.1"
25
26
  gem.add_development_dependency "test-unit", ">= 0"
26
27
 
@@ -0,0 +1,10 @@
1
+ load File.expand_path("../fixture_helper.rb", __FILE__)
2
+
3
+ PumaWorkerKiller.start
4
+
5
+ @memory = []
6
+ 10_000.times.each do
7
+ @memory << SecureRandom.hex
8
+ end
9
+
10
+ run HelloWorldApp
@@ -0,0 +1,6 @@
1
+ load File.expand_path("../../fixture_helper.rb", __FILE__)
2
+
3
+ before_fork do
4
+ require 'puma_worker_killer'
5
+ PumaWorkerKiller.start
6
+ end
@@ -0,0 +1,5 @@
1
+ load File.expand_path("../fixture_helper.rb", __FILE__)
2
+
3
+ PumaWorkerKiller.start
4
+
5
+ run HelloWorldApp
@@ -1,3 +1,5 @@
1
+ require 'securerandom'
2
+
1
3
  require 'rack'
2
4
  require 'rack/server'
3
5
 
@@ -7,21 +9,17 @@ PumaWorkerKiller.config do |config|
7
9
  config.ram = Integer(ENV['PUMA_RAM']) if ENV['PUMA_RAM']
8
10
  config.frequency = Integer(ENV['PUMA_FREQUENCY']) if ENV['PUMA_FREQUENCY']
9
11
  end
10
- PumaWorkerKiller.start
11
-
12
12
 
13
- puts "Frequency: #{PumaWorkerKiller.frequency}" if ENV['PUMA_FREQUENCY']
13
+ puts "Frequency: #{ PumaWorkerKiller.frequency }" if ENV['PUMA_FREQUENCY']
14
14
 
15
15
  class HelloWorld
16
- def response
16
+ def response(env)
17
17
  [200, {}, ['Hello World']]
18
18
  end
19
19
  end
20
20
 
21
21
  class HelloWorldApp
22
22
  def self.call(env)
23
- HelloWorld.new.response
23
+ HelloWorld.new.response(env)
24
24
  end
25
25
  end
26
-
27
- run HelloWorldApp
@@ -0,0 +1,5 @@
1
+ load File.expand_path("../fixture_helper.rb", __FILE__)
2
+
3
+ PumaWorkerKiller.enable_rolling_restart(1) # 1 second
4
+
5
+ run HelloWorldApp
@@ -3,75 +3,51 @@ require 'test_helper'
3
3
  class PumaWorkerKillerTest < Test::Unit::TestCase
4
4
 
5
5
  def test_starts
6
- app_path = fixture_path.join("app.ru")
7
6
  port = 0 # http://stackoverflow.com/questions/200484/how-do-you-find-a-free-tcp-server-port-using-ruby
8
- puma_log = Pathname.new "#{ SecureRandom.hex }-puma.log"
9
- pid = Process.spawn("PUMA_FREQUENCY=1 bundle exec puma #{ app_path } -t 1:1 -w 5 --preload --debug -p #{ port } > #{puma_log}")
10
- sleep 5
11
- assert_match "PumaWorkerKiller:", puma_log.read
12
- ensure
13
- puma_log.delete
14
- Process.kill('TERM', pid) if pid
15
- end
16
-
17
- def test_worker_reaped
18
- ram = 1 #mb
19
- cluster = FakeCluster.new
20
- reaper = PumaWorkerKiller::Reaper.new(ram, cluster)
21
- worker_count = 10
22
- worker_count.times { cluster.add_worker }
23
-
24
- assert_equal worker_count, cluster.workers.count
25
- refute cluster.workers.detect {|w| w.is_term? }
7
+ command = "bundle exec puma #{ fixture_path.join("default.ru") } -t 1:1 -w 2 --preload --debug -p #{ port }"
8
+ options = { wait_for: "booted", timeout: 5, env: { "PUMA_FREQUENCY" => 1 } }
26
9
 
27
- reaper.reap
28
- assert_equal 1, cluster.workers.select {|w| w.is_term? }.count
29
-
30
- reaper.reap
31
- assert_equal 2, cluster.workers.select {|w| w.is_term? }.count
32
-
33
- reaper.reap
34
- assert_equal 3, cluster.workers.select {|w| w.is_term? }.count
35
- ensure
36
- cluster.workers.map(&:term)
10
+ WaitForIt.new(command, options) do |spawn|
11
+ assert_contains(spawn, "PumaWorkerKiller")
12
+ end
37
13
  end
38
14
 
39
- def test_kills_memory_leak
40
- ram = rand(75..100) #mb
41
- cluster = FakeCluster.new
42
- reaper = PumaWorkerKiller::Reaper.new(ram, cluster)
43
- while reaper.get_total_memory < (ram * 0.80)
44
- cluster.add_worker
45
- sleep 0.01
15
+ def test_without_preload
16
+ port = 0 # http://stackoverflow.com/questions/200484/how-do-you-find-a-free-tcp-server-port-using-ruby
17
+ command = "bundle exec puma #{ fixture_path.join("default.ru") } -t 1:1 -w 2 --debug -p #{ port } -C #{ fixture_path.join("config/puma_worker_killer_start.rb") }"
18
+ options = { wait_for: "booted", timeout: 10, env: { "PUMA_FREQUENCY" => 1 } }
19
+
20
+ WaitForIt.new(command, options) do |spawn|
21
+ assert_contains(spawn, "PumaWorkerKiller")
46
22
  end
23
+ end
47
24
 
48
- reaper.reap
49
- assert_equal 0, cluster.workers.select {|w| w.is_term? }.count
25
+ def test_kills_large_app
26
+ file = fixture_path.join("big.ru")
27
+ port = 0
28
+ command = "bundle exec puma #{ file } -t 1:1 -w 2 --preload --debug -p #{ port }"
29
+ options = { wait_for: "booted", timeout: 5, env: { "PUMA_FREQUENCY" => 1, 'PUMA_RAM' => 1} }
50
30
 
51
- until reaper.get_total_memory > ram
52
- cluster.add_worker
53
- sleep 0.01
31
+ WaitForIt.new(command, options) do |spawn|
32
+ assert_contains(spawn, "Out of memory")
54
33
  end
55
-
56
- reaper.reap
57
- assert_equal 1, cluster.workers.select {|w| w.is_term? }.count
58
- ensure
59
- cluster.workers.map(&:term)
60
34
  end
61
35
 
36
+ def assert_contains(spawn, string)
37
+ assert spawn.wait(string), "Expected logs to contain '#{string}' but it did not, contents: #{ spawn.log.read }"
38
+ end
62
39
 
63
40
  def test_rolling_restart
64
- ram = rand(75..100) #mb
65
- cluster = FakeCluster.new
66
- cluster.add_worker
67
41
 
68
- worker = cluster.workers.first
69
- reaper = PumaWorkerKiller::RollingRestart.new(cluster)
70
- reaper.reap(1)
42
+ file = fixture_path.join("rolling_restart.ru")
43
+ port = 0
44
+ command = "bundle exec puma #{ file } -t 1:1 -w 2 --preload --debug -p #{ port }"
45
+ puts command.inspect
46
+ options = { wait_for: "booted", timeout: 15, env: { } }
71
47
 
72
- assert_equal 1, cluster.workers.select {|w| w.is_term? }.count
73
- ensure
74
- cluster.workers.map(&:term)
48
+ WaitForIt.new(command, options) do |spawn|
49
+ assert_contains(spawn, "Rolling Restart")
50
+ end
75
51
  end
76
52
  end
77
53
 
data/test/test_helper.rb CHANGED
@@ -2,70 +2,8 @@ Bundler.require
2
2
 
3
3
  require 'puma_worker_killer'
4
4
  require 'test/unit'
5
+ require 'wait_for_it'
5
6
 
6
7
  def fixture_path
7
8
  Pathname.new(File.expand_path("../fixtures", __FILE__))
8
9
  end
9
-
10
- # Mock object stand in for Puma::Cluster
11
- class FakeCluster
12
- def initialize
13
- @workers = []
14
- end
15
-
16
- def wakeup!
17
- end
18
-
19
- class Worker
20
- attr_accessor :pid
21
-
22
- def initialize(pid)
23
- @pid = pid
24
- end
25
-
26
- def memory_leak
27
- while true
28
-
29
- end
30
- end
31
-
32
- # not public interface, used for testing
33
- def is_term?
34
- @first_term_sent
35
- end
36
-
37
- def term
38
- begin
39
- if @first_term_sent && (Time.new - @first_term_sent) > 30
40
- @signal = "KILL"
41
- else
42
- @first_term_sent ||= Time.new
43
- end
44
-
45
- Process.kill "TERM", @pid
46
- Process.wait(@pid)
47
- rescue Errno::ESRCH, Errno::ECHILD
48
- end
49
- end
50
- end
51
-
52
- def log(msg)
53
- puts msg
54
- end
55
-
56
- def do_work
57
- while true
58
- sleep 1
59
- end
60
- end
61
-
62
- # not a public interface, added to make testing easier
63
- def workers
64
- @workers
65
- end
66
-
67
- def add_worker
68
- pid = fork { do_work }
69
- @workers << Worker.new(pid)
70
- end
71
- end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma_worker_killer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Schneeman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-01 00:00:00.000000000 Z
11
+ date: 2016-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: puma
@@ -58,6 +58,20 @@ dependencies:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
60
  version: '1.6'
61
+ - !ruby/object:Gem::Dependency
62
+ name: wait_for_it
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.1'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.1'
61
75
  - !ruby/object:Gem::Dependency
62
76
  name: rake
63
77
  requirement: !ruby/object:Gem::Requirement
@@ -106,7 +120,11 @@ files:
106
120
  - lib/puma_worker_killer/rolling_restart.rb
107
121
  - lib/puma_worker_killer/version.rb
108
122
  - puma_worker_killer.gemspec
109
- - test/fixtures/app.ru
123
+ - test/fixtures/big.ru
124
+ - test/fixtures/config/puma_worker_killer_start.rb
125
+ - test/fixtures/default.ru
126
+ - test/fixtures/fixture_helper.rb
127
+ - test/fixtures/rolling_restart.ru
110
128
  - test/puma_worker_killer_test.rb
111
129
  - test/test_helper.rb
112
130
  homepage: https://github.com/schneems/puma_worker_killer
@@ -129,12 +147,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
147
  version: '0'
130
148
  requirements: []
131
149
  rubyforge_project:
132
- rubygems_version: 2.5.1
150
+ rubygems_version: 2.6.4
133
151
  signing_key:
134
152
  specification_version: 4
135
153
  summary: If you have a memory leak in your web code puma_worker_killer can keep it
136
154
  in check.
137
155
  test_files:
138
- - test/fixtures/app.ru
156
+ - test/fixtures/big.ru
157
+ - test/fixtures/config/puma_worker_killer_start.rb
158
+ - test/fixtures/default.ru
159
+ - test/fixtures/fixture_helper.rb
160
+ - test/fixtures/rolling_restart.ru
139
161
  - test/puma_worker_killer_test.rb
140
162
  - test/test_helper.rb
163
+ has_rdoc: