puma_worker_killer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ce372755dae3f6e9abb64192488e0709ecf7af12
4
+ data.tar.gz: 8c51e18768753c3716487379f217a431f4ae6f1e
5
+ SHA512:
6
+ metadata.gz: 535cc32a6414aae221b2842a85bc5205d7043cc37699a3b1d488af5922b89688aebacce10276e025a9af9c0190f98c19f3ed55521ffc49c14fafcd532ac9b12e
7
+ data.tar.gz: 0c025d47e64ddfe8589ee3145178cb9205adcc1248d2b494631e15b1fd5186178662c716cec2f8a2a126ae86addc757fc598b8e6c415a8cb0bfa795d14a6f4b0
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ *.gem
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - ruby-head
7
+ - jruby-19mode
8
+ - rbx-19mode
9
+
10
+ matrix:
11
+ allow_failures:
12
+ - rvm: ruby-head
13
+ - rvm: rbx-19mode
14
+ - rvm: jruby-19mode
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Puma Worker Killer
2
+
3
+ [![Build Status](https://travis-ci.org/schneems/puma_worker_killer.png?branch=master)](https://travis-ci.org/schneems/puma_worker_killer)
4
+
5
+
6
+ ## What
7
+
8
+ If you have a memory leak in your code, finding and plugging it can be a herculean effort. Instead what if you just killed your processes when they got to be too large? The Puma Worker Killer does just that. Similar to [Unicorn Worker Killer](https://github.com/kzk/unicorn-worker-killer) but for the Puma web server. Note that Puma can uses threads or processes to add concurrency, the Puma Worker Killer only helps if you are using clustered mode (concurrency via process).
9
+
10
+
11
+ ## Install
12
+
13
+ In your Gemfile add:
14
+
15
+ ```ruby
16
+ gem 'puma_worker_killer'
17
+ ```
18
+
19
+ Then run `$ bundle install`
20
+
21
+ ## Use
22
+
23
+ Somewhere in your main process run this code:
24
+
25
+ ```ruby
26
+ PumaWorkerKiller.start
27
+ ```
28
+
29
+ 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.
30
+
31
+ ## Configure
32
+
33
+ Before calling `start` you can configure `PumaWorkerKiller`. You can do so using a configure block or calling methods directly:
34
+
35
+ ```ruby
36
+ PumaWorkerKiller.config do |config|
37
+ config.ram = 1024 # mb
38
+ config.frequency = 5 # seconds
39
+ config.percent_usage = 0.98
40
+ end
41
+ ```
42
+
43
+ It is important that you tell your code how much RAM is available on your system. The default is 512 mb (the same size as a Heroku 1x dyno). You can change this value like this:
44
+
45
+ ```ruby
46
+ PumaWorkerKiller.ram = 1024 # mb
47
+ ```
48
+
49
+ By default it is assumed that you do not want to hit 100% utilization, that is if your code is actually using 512 mb out of 512 mb it would be bad (this is dangerously close to swapping memory and slowing down your programs). So by default processes will be killed when they are at 99 % utilization of the value specified in `PumaWorkerKiller.ram`. You can change that value to 98 % like this:
50
+
51
+ ```ruby
52
+ PumaWorkerKiller.percent_usage = 0.98
53
+ ```
54
+
55
+ You may want to tune the worker killer to run more or less often. You can adjust frequency:
56
+
57
+ ```ruby
58
+ PumaWorkerKiller.frequency = 20 # seconds
59
+ ```
60
+
61
+ ## License
62
+
63
+ MIT
64
+
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ require 'rake'
6
+ require 'rake/testtask'
7
+
8
+ task :default => [:test]
9
+
10
+ test_task = Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.libs << 'test'
13
+ t.pattern = 'test/**/*_test.rb'
14
+ t.verbose = false
15
+ end
@@ -0,0 +1,26 @@
1
+ require 'get_process_mem'
2
+
3
+ module PumaWorkerKiller
4
+ extend self
5
+
6
+ attr_accessor :ram, :frequency, :percent_usage
7
+ self.ram = 512 # mb
8
+ self.frequency = 10 # seconds
9
+ self.percent_usage = 0.99 # percent of RAM to use
10
+
11
+ def config
12
+ yield self
13
+ end
14
+
15
+ def reaper(ram = self.ram, percent = self.percent_usage)
16
+ Reaper.new(ram * percent_usage)
17
+ end
18
+
19
+ def start(frequency = self.frequency, reaper = self.reaper)
20
+ AutoReap.new(frequency, reaper).start
21
+ end
22
+ end
23
+
24
+ require 'puma_worker_killer/reaper'
25
+ require 'puma_worker_killer/auto_reap'
26
+ require 'puma_worker_killer/version'
@@ -0,0 +1,21 @@
1
+ module PumaWorkerKiller
2
+ class AutoReap
3
+ def initialize(timeout, reaper = Reaper.new)
4
+ @timeout = timeout # seconds
5
+ @reaper = reaper
6
+ @running = false
7
+ end
8
+
9
+ def start
10
+ @running = true
11
+
12
+ Thread.new do
13
+ while @running
14
+ @reaper.reap
15
+ sleep @timeout
16
+ end
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ module PumaWorkerKiller
2
+ class Reaper
3
+ def initialize(max_ram, master = self.get_master)
4
+ @max_ram = max_ram
5
+ @master = master
6
+ end
7
+
8
+ def get_master
9
+ ObjectSpace.each_object(Puma::Cluster).map { |obj| obj }.first
10
+ end
11
+
12
+ def get_memory(pid)
13
+ GetProcessMem.new(pid).mb
14
+ end
15
+
16
+ def get_workers
17
+ workers = {}
18
+ @master.instance_variable_get("@workers").each { |worker| workers[worker] = get_memory(worker.pid) }
19
+ workers
20
+ end
21
+
22
+ def get_total_memory(workers = self.get_workers)
23
+ master_memory = get_memory(Process.pid)
24
+ worker_memory = workers.map {|_, mem| mem }.inject(&:+) || 0
25
+ worker_memory + master_memory
26
+ end
27
+
28
+ def wait(pid)
29
+ Process.wait(pid)
30
+ rescue Errno::ECHILD
31
+ end
32
+
33
+ def reap
34
+ return false unless @master
35
+ workers = get_workers
36
+ total_memory = get_total_memory(workers)
37
+ if workers.any? && total_memory > @max_ram
38
+ biggest_worker, memory_used = workers.sort_by {|_, mem| mem }.last
39
+ biggest_worker.term
40
+ @master.log "PumaWorkerKiller: Out of memory. #{workers.count} workers consuming total: #{total_memory} mb out of max: #{@max_ram} mb. Sending TERM to #{biggest_worker.inspect} consuming #{memory_used} mb."
41
+ wait(biggest_worker.pid)
42
+ else
43
+ @master.log "PumaWorkerKiller: Consuming #{total_memory} mb with master and #{workers.count} workers"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ module PumaWorkerKiller
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'puma_worker_killer/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "puma_worker_killer"
8
+ gem.version = PumaWorkerKiller::VERSION
9
+ gem.authors = ["Richard Schneeman"]
10
+ gem.email = ["richard.schneeman+rubygems@gmail.com"]
11
+ gem.description = %q{ }
12
+ gem.summary = %q{ }
13
+ gem.homepage = "https://github.com/schneems/puma_worker_killer"
14
+ gem.license = "MIT"
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency "puma", "~> 2"
22
+ gem.add_dependency "get_process_mem", "~> 0"
23
+ gem.add_development_dependency "rake", "~> 10.1"
24
+ end
@@ -0,0 +1,48 @@
1
+ require 'test_helper'
2
+
3
+ class PumaWorkerKillerTest < Test::Unit::TestCase
4
+
5
+ def test_worker_reaped
6
+ ram = 1 #mb
7
+ cluster = FakeCluster.new
8
+ reaper = PumaWorkerKiller::Reaper.new(ram, cluster)
9
+ worker_count = 10
10
+ worker_count.times { cluster.add_worker }
11
+
12
+ assert_equal worker_count, cluster.workers.count
13
+ refute cluster.workers.detect {|w| w.is_term? }
14
+
15
+ reaper.reap
16
+ assert_equal 1, cluster.workers.select {|w| w.is_term? }.count
17
+
18
+ reaper.reap
19
+ assert_equal 2, cluster.workers.select {|w| w.is_term? }.count
20
+
21
+ reaper.reap
22
+ assert_equal 3, cluster.workers.select {|w| w.is_term? }.count
23
+ ensure
24
+ cluster.workers.map(&:term)
25
+ end
26
+
27
+ def test_kills_memory_leak
28
+ ram = 75 #mb
29
+ cluster = FakeCluster.new
30
+ reaper = PumaWorkerKiller::Reaper.new(ram, cluster)
31
+ while reaper.get_total_memory < (ram * 0.80)
32
+ cluster.add_worker
33
+ end
34
+
35
+ reaper.reap
36
+ assert_equal 0, cluster.workers.select {|w| w.is_term? }.count
37
+
38
+ while reaper.get_total_memory < ram
39
+ cluster.add_worker
40
+ end
41
+
42
+ reaper.reap
43
+ assert_equal 1, cluster.workers.select {|w| w.is_term? }.count
44
+ ensure
45
+ cluster.workers.map(&:term)
46
+ end
47
+
48
+ end
@@ -0,0 +1,65 @@
1
+ Bundler.require
2
+
3
+ require 'puma_worker_killer'
4
+ require 'test/unit'
5
+
6
+
7
+ # Mock object stand in for Puma::Cluster
8
+ class FakeCluster
9
+ def initialize
10
+ @workers = []
11
+ end
12
+
13
+ class Worker
14
+ attr_accessor :pid
15
+
16
+ def initialize(pid)
17
+ @pid = pid
18
+ end
19
+
20
+ def memory_leak
21
+ while true
22
+
23
+ end
24
+ end
25
+
26
+ # not public interface, used for testing
27
+ def is_term?
28
+ @first_term_sent
29
+ end
30
+
31
+ def term
32
+ begin
33
+ if @first_term_sent && (Time.new - @first_term_sent) > 30
34
+ @signal = "KILL"
35
+ else
36
+ @first_term_sent ||= Time.new
37
+ end
38
+
39
+ Process.kill "TERM", @pid
40
+ Process.wait(@pid)
41
+ rescue Errno::ESRCH, Errno::ECHILD
42
+ end
43
+ end
44
+ end
45
+
46
+ def log(msg)
47
+ puts msg
48
+ end
49
+
50
+ def do_work
51
+ while true
52
+ sleep 1
53
+ end
54
+ end
55
+
56
+ # not a public interface, added to make testing easier
57
+ def workers
58
+ @workers
59
+ end
60
+
61
+ def add_worker
62
+ pid = fork { do_work }
63
+ @workers << Worker.new(pid)
64
+ end
65
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: puma_worker_killer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Richard Schneeman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: puma
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: get_process_mem
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.1'
55
+ description: " "
56
+ email:
57
+ - richard.schneeman+rubygems@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - README.md
66
+ - Rakefile
67
+ - lib/puma_worker_killer.rb
68
+ - lib/puma_worker_killer/auto_reap.rb
69
+ - lib/puma_worker_killer/reaper.rb
70
+ - lib/puma_worker_killer/version.rb
71
+ - puma_worker_killer.gemspec
72
+ - test/puma_worker_killer_test.rb
73
+ - test/test_helper.rb
74
+ homepage: https://github.com/schneems/puma_worker_killer
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.2.0
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: ''
98
+ test_files:
99
+ - test/puma_worker_killer_test.rb
100
+ - test/test_helper.rb
101
+ has_rdoc: