puma_doctor 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 71aabad56a6b33023c08b80b5a51e3abd7019aac
4
+ data.tar.gz: 0dbe0a44f19e7dbbdf7d587c8b6ec2562a9da3f0
5
+ SHA512:
6
+ metadata.gz: 7a575219806300a993916df39ae762cbfd35a013cf93fc2dcf452169a28e09f77e6201d35ebd652e91a2d327f44d0477d7a693c5cd7924a37ae776cbbc95bada
7
+ data.tar.gz: acb853b60daaedcff2f915e013879b5d8815c7ab1e7742977289e1266f8c0d6cc250d384eb664c2ca833d54a1f505d2f2c353b6bad7b36a3f5e3f95fb5bd79dc
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in puma_doctor.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Alex Krasynskyi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # PumaDoctor
2
+
3
+ Inspired by ( https://github.com/schneems/puma_worker_killer ). Idea is to run
4
+ separate process as a daemon to measure puma memory and restart worker when memory
5
+ threshold reached.
6
+
7
+ ## Usage
8
+
9
+ ### Running from ruby code.
10
+ To run from your code:
11
+
12
+ PumaDoctor.start(frequency: 60, memory_threshold: 2000, puma_pid: 99999)
13
+
14
+ This is not very useful in production since it blocks execution, but you can play
15
+ around with options locally. Available options with defaults are:
16
+
17
+ frequency: 60 # Interval in seconds
18
+ puma_pid_file: 'puma.pid' # Location of puma pid file
19
+ memory_threshold: 4000 # Amount in MB
20
+ log_file: 'puma_doctor.log' # Name and location of log file
21
+
22
+ ### Running as a daemon.
23
+
24
+ To run as daemon you can create file with content below(Ex.: doctor.rb)
25
+
26
+ require 'puma_doctor'
27
+ require 'daemons'
28
+
29
+ pid_dir = '../' # Path to directory to store pid.
30
+ Daemons.run_proc('puma_doctor', { dir: pid_dir }) do
31
+ PumaDoctor.start(frequency: 60, memory_threshold: 1000)
32
+ end
33
+
34
+ Then control it with(for more details visit https://github.com/thuehlinger/daemons):
35
+
36
+ bundle exec ruby doctor.rb start
37
+ bundle exec ruby doctor.rb stop
38
+ bundle exec ruby doctor.rb restart
39
+
40
+ ### Using with capistrano.
41
+
42
+ Probably the easiest way to run `puma_doctor` in production is to use `capistrano`. Require script in `Capfile`:
43
+
44
+ require 'puma_doctor/capistrano'
45
+
46
+ This will add hook to start/restart daemon on `after deploy:finished`. If you want to start/stop from capistrano manually - this tasks are available:
47
+
48
+ cap puma_doctor:check # Check if config file exixts on server
49
+ cap puma_doctor:config # Config daemon
50
+ cap puma_doctor:restart # Restart daemon
51
+ cap puma_doctor:start # Start daemon
52
+ cap puma_doctor:stop # Stop daemon
53
+
54
+ Available options with defaults:
55
+
56
+ set :puma_doctor_pid, -> { File.join(shared_path, 'tmp', 'pids', 'puma_doctor.pid') }
57
+ set :puma_doctor_frequency, 30 #seconds
58
+ set :puma_doctor_memory_threshold, 4000 #mb
59
+ set :puma_doctor_daemon_file, -> { File.join(shared_path, 'puma_doctor_daemon.rb') }
60
+ set :puma_doctor_log_file, -> { File.join(shared_path, 'log', 'puma_doctor.log') }
61
+ set :puma_pid, -> { File.join(shared_path, 'tmp', 'pids', 'puma.pid') }
62
+
63
+
64
+ ### Logging
65
+
66
+ You can always see what `puma_doctor` is doing by reading logs.
67
+
68
+
69
+ ## Installation
70
+
71
+ Add this line to your application's Gemfile:
72
+
73
+ gem 'puma_doctor'
74
+
75
+ And then execute:
76
+
77
+ $ bundle
78
+
79
+ Or install it yourself as:
80
+
81
+ $ gem install puma_doctor
82
+
83
+ ## TODO
84
+
85
+ Test
86
+
87
+ ## Contributing
88
+
89
+ 1. Fork it ( http://github.com/spilin/puma_doctor/fork )
90
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
91
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
92
+ 4. Push to the branch (`git push origin my-new-feature`)
93
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,62 @@
1
+ namespace :load do
2
+ task :defaults do
3
+ set :puma_doctor_pid, -> { File.join(shared_path, 'tmp', 'pids', 'puma_doctor.pid') }
4
+ set :puma_doctor_frequency, 30 #seconds
5
+ set :puma_doctor_memory_threshold, 4000 #mb
6
+ set :puma_doctor_daemon_file, -> { File.join(shared_path, 'puma_doctor_daemon.rb') }
7
+ set :puma_doctor_log_file, -> { File.join(shared_path, 'log', 'puma_doctor.log') }
8
+ set :puma_pid, -> { File.join(shared_path, 'tmp', 'pids', 'puma.pid') }
9
+ end
10
+ end
11
+
12
+ namespace :puma_doctor do
13
+ desc 'Config daemon. Generate and send puma_doctor.rb'
14
+ task :config do
15
+ on roles(:app), in: :sequence, wait: 5 do
16
+ path = File.expand_path("../daemon_template.rb.erb", __FILE__)
17
+ if File.file?(path)
18
+ erb = File.read(path)
19
+ upload! StringIO.new(ERB.new(erb).result(binding)), fetch(:puma_doctor_daemon_file)
20
+ end
21
+ end
22
+ end
23
+
24
+ desc 'Start daemon'
25
+ task :start do
26
+ on roles(:app), in: :sequence, wait: 5 do
27
+ within release_path do
28
+ invoke 'puma_doctor:check'
29
+ execute :bundle, :exec, :ruby, fetch(:puma_doctor_daemon_file), 'start'
30
+ end
31
+ end
32
+ end
33
+
34
+ desc 'Stop daemon'
35
+ task :stop do
36
+ on roles(:app), in: :sequence, wait: 5 do
37
+ within release_path do
38
+ invoke 'puma_doctor:check'
39
+ execute :bundle, :exec, :ruby, fetch(:puma_doctor_daemon_file), 'stop'
40
+ end
41
+ end
42
+ end
43
+
44
+ desc 'Restart daemon'
45
+ task :restart do
46
+ on roles(:app), in: :sequence, wait: 5 do
47
+ within release_path do
48
+ invoke 'puma_doctor:check'
49
+ execute :bundle, :exec, :ruby, fetch(:puma_doctor_daemon_file), 'restart'
50
+ end
51
+ end
52
+ end
53
+
54
+ desc 'Check if config file exixts on server. If not - create and upload one.'
55
+ task :check do
56
+ on roles(:app), in: :sequence, wait: 5 do
57
+ invoke 'puma_doctor:config' unless test "[ -f #{fetch(:puma_doctor_daemon_file)} ]"
58
+ end
59
+ end
60
+
61
+ after 'deploy:finished', 'puma_doctor:restart'
62
+ end
@@ -0,0 +1,12 @@
1
+ require 'puma_doctor'
2
+ require 'daemons'
3
+
4
+ Daemons.run_proc('puma_doctor', { dir: '<%= File.dirname(fetch(:puma_doctor_pid)) %>' }) do
5
+ options = {
6
+ frequency: <%= fetch(:puma_doctor_frequency) %>,
7
+ memory_threshold: <%= fetch(:puma_doctor_memory_threshold) %>,
8
+ puma_pid_file: '<%= fetch(:puma_pid) %>',
9
+ log_file: '<%= fetch(:puma_doctor_log_file) %>'
10
+ }
11
+ PumaDoctor.start(options)
12
+ end
@@ -0,0 +1,67 @@
1
+ module PumaDoctor
2
+ class Doctor
3
+ def initialize(options = {})
4
+ @memory_threshold = options[:memory_threshold]
5
+ @puma_pid_file = options[:puma_pid_file]
6
+ @puma_pid = options[:puma_pid] && options[:puma_pid].to_i
7
+ @logger = options[:logger]
8
+ end
9
+
10
+ def examine
11
+ @master_pid = get_master_pid(@master_pid)
12
+ return if @master_pid.nil?
13
+ workers = get_workers(@master_pid) # worker pids with size, last one is the largest one
14
+ used_memory = workers.inject(0) {|memo, v| memo += v.last } + GetProcessMem.new(@master_pid).mb
15
+ logger.info "[Puma Doctor] Total memory used: #{used_memory} mb. Workers online: #{workers.size}"
16
+ if used_memory > @memory_threshold
17
+ kill_largest_worker(workers)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def get_master_pid(current_puma_pid)
24
+ if current_puma_pid && process_is_running?(current_puma_pid)
25
+ current_puma_pid
26
+ elsif current_puma_pid && (@puma_pid_file.nil? || !File.exists?(@puma_pid_file))
27
+ logger.warn "[Puma Doctor] Master pid is no longer represents running process.
28
+ Reload failed because pid file is not set or invalid(File: '#{@puma_pid_file}')"
29
+ nil
30
+ elsif current_puma_pid && (current_puma_pid = File.read(@puma_pid_file).to_i) && process_is_running?(current_puma_pid)
31
+ logger.warn "[Puma Doctor] Master pid is no longer represents running process. Successfully Reloaded pid file."
32
+ current_puma_pid
33
+ elsif @puma_pid && process_is_running?(@puma_pid)
34
+ current_puma_pid = @puma_pid
35
+ elsif @puma_pid_file && File.exists?(@puma_pid_file) && process_is_running?(current_puma_pid = File.read(@puma_pid_file).to_i)
36
+ current_puma_pid
37
+ else
38
+ logger.warn "[Puma Doctor] Puma master pidfile is not found"
39
+ nil
40
+ end
41
+ end
42
+
43
+ def get_workers(puma_pid)
44
+ `pgrep -P #{puma_pid} -d ','`.split(',').compact.map do |pid|
45
+ [pid.to_i, GetProcessMem.new(pid).mb]
46
+ end
47
+ end
48
+
49
+ def kill_largest_worker(workers)
50
+ pid, memory_used = workers.max_by {|a| a[1]}
51
+ Process.kill('TERM', pid)
52
+ logger.info "[Puma Doctor] Doctor killed worker(#{pid}).It was using #{memory_used} mb. Workers online: #{workers.size - 1}"
53
+ end
54
+
55
+ def process_is_running?(pid)
56
+ Process.getpgid(pid)
57
+ true
58
+ rescue Errno::ESRCH
59
+ false
60
+ end
61
+
62
+ def logger
63
+ @logger
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,23 @@
1
+ require 'logger'
2
+
3
+ module PumaDoctor
4
+ class Logger
5
+ def initialize(options = {})
6
+ @logger = ::Logger.new(options[:log_file])
7
+ @logger.level = options[:log_level] || ::Logger::INFO
8
+ end
9
+
10
+ def info(text)
11
+ @logger.info(text)
12
+ end
13
+
14
+ def warn(text)
15
+ @logger.warn(text)
16
+ end
17
+
18
+ def log_start
19
+ @logger.info "[Puma Doctor] Starting..."
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module PumaDoctor
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,36 @@
1
+ require "puma_doctor/version"
2
+ require "get_process_mem"
3
+
4
+ require 'puma_doctor/doctor'
5
+ require 'puma_doctor/logger'
6
+
7
+ module PumaDoctor
8
+ extend self
9
+
10
+ attr_accessor :frequency, :pid_file, :puma_pid, :puma_pid_file, :memory_threshold, :log_file
11
+ attr_reader :logger
12
+ self.frequency = 60 # seconds
13
+ self.pid_file = 'puma_doctor.pid'
14
+ self.puma_pid_file = 'puma.pid'
15
+ self.memory_threshold = 4000 # mb
16
+ self.log_file = 'puma_doctor.log'
17
+
18
+ def start(options = {})
19
+ @logger = ::PumaDoctor::Logger.new(log_file: options[:log_file] || self.log_file, log_level: options[:log_level])
20
+ @logger.log_start
21
+ doctor = Doctor.new(default_options.merge(options).merge(logger: @logger))
22
+ loop do
23
+ doctor.examine
24
+ sleep(options[:frequency] || self.frequency)
25
+ end
26
+ end
27
+
28
+ def default_options
29
+ {
30
+ memory_threshold: self.memory_threshold,
31
+ puma_pid_file: self.puma_pid_file,
32
+ puma_pid: self.puma_pid
33
+ }
34
+ end
35
+
36
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'puma_doctor/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "puma_doctor"
8
+ spec.version = PumaDoctor::VERSION
9
+ spec.authors = ["Alex Krasynskyi"]
10
+ spec.email = ["lyoshakr@gmail.com"]
11
+ spec.summary = %q{Process to keep your puma workers healthy.}
12
+ spec.description = %q{Kills largest worker. Runs seperate daemon, managed with sidekiq.}
13
+ spec.homepage = "https://github.com/spilin/puma_doctor"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = []
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "daemons"
22
+ spec.add_dependency "get_process_mem"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.5"
25
+ spec.add_development_dependency "rake"
26
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: puma_doctor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alex Krasynskyi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: daemons
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
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: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Kills largest worker. Runs seperate daemon, managed with sidekiq.
70
+ email:
71
+ - lyoshakr@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - lib/puma_doctor.rb
82
+ - lib/puma_doctor/capistrano.rb
83
+ - lib/puma_doctor/daemon_template.rb.erb
84
+ - lib/puma_doctor/doctor.rb
85
+ - lib/puma_doctor/logger.rb
86
+ - lib/puma_doctor/version.rb
87
+ - puma_doctor.gemspec
88
+ homepage: https://github.com/spilin/puma_doctor
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.2.0
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Process to keep your puma workers healthy.
112
+ test_files: []