puma_worker_killer-0.3.1/0000755000004100000410000000000013771343061015445 5ustar www-datawww-datapuma_worker_killer-0.3.1/.travis.yml0000644000004100000410000000044613771343061017562 0ustar www-datawww-datalanguage: ruby rvm: - 2.3.8 - 2.4.10 - 2.5.8 - 2.6.6 - 2.7.1 - ruby-head - rbx before_install: - gem install bundler -v 1.12.5 matrix: allow_failures: - rvm: ruby-head - rvm: rbx install: - bundle install script: - bundle exec rubocop - bundle exec rake test puma_worker_killer-0.3.1/test/0000755000004100000410000000000013771343061016424 5ustar www-datawww-datapuma_worker_killer-0.3.1/test/puma_worker_killer_test.rb0000644000004100000410000000753413771343061023716 0ustar www-datawww-data# frozen_string_literal: true require 'test_helper' class PumaWorkerKillerTest < Test::Unit::TestCase def test_starts port = 0 # http://stackoverflow.com/questions/200484/how-do-you-find-a-free-tcp-server-port-using-ruby command = "bundle exec puma #{fixture_path.join("default.ru")} -t 1:1 -w 2 --preload --debug -p #{port}" options = { wait_for: 'booted', timeout: 5, env: { 'PUMA_FREQUENCY' => 1 } } WaitForIt.new(command, options) do |spawn| assert_contains(spawn, 'PumaWorkerKiller') end end def test_without_preload port = 0 # http://stackoverflow.com/questions/200484/how-do-you-find-a-free-tcp-server-port-using-ruby 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")}" options = { wait_for: 'booted', timeout: 10, env: { 'PUMA_FREQUENCY' => 1 } } WaitForIt.new(command, options) do |spawn| assert_contains(spawn, 'PumaWorkerKiller') end end def test_kills_large_app file = fixture_path.join('big.ru') port = 0 command = "bundle exec puma #{file} -t 1:1 -w 2 --preload --debug -p #{port}" options = { wait_for: 'booted', timeout: 5, env: { 'PUMA_FREQUENCY' => 1, 'PUMA_RAM' => 1 } } WaitForIt.new(command, options) do |spawn| assert_contains(spawn, 'Out of memory') end end def test_pre_term file = fixture_path.join('pre_term.ru') port = 0 command = "bundle exec puma #{file} -t 1:1 -w 2 --preload --debug -p #{port}" options = { wait_for: 'booted', timeout: 5, env: { 'PUMA_FREQUENCY' => 1, 'PUMA_RAM' => 1 } } WaitForIt.new(command, options) do |spawn| assert_contains(spawn, 'Out of memory') assert_contains(spawn, 'About to terminate worker:') # defined in pre_term.ru end end def test_on_calculation file = fixture_path.join('on_calculation.ru') port = 0 command = "bundle exec puma #{file} -t 1:1 -w 2 --preload --debug -p #{port}" options = { wait_for: 'booted', timeout: 5, env: { 'PUMA_FREQUENCY' => 1, 'PUMA_RAM' => 1 } } WaitForIt.new(command, options) do |spawn| assert_contains(spawn, 'Out of memory') assert_contains(spawn, 'Current memory footprint:') # defined in on_calculate.ru end end def assert_contains(spawn, string) assert spawn.wait(string), "Expected logs to contain '#{string}' but it did not, contents: #{spawn.log.read}" end def test_rolling_restart file = fixture_path.join('rolling_restart.ru') port = 0 command = "bundle exec puma #{file} -t 1:1 -w 2 --preload --debug -p #{port}" puts command.inspect options = { wait_for: 'booted', timeout: 15, env: {} } WaitForIt.new(command, options) do |spawn| assert_contains(spawn, 'Rolling Restart') end end def test_rolling_restart_worker_kill_check file = fixture_path.join('rolling_restart.ru') port = 0 command = "bundle exec puma #{file} -t 1:1 -w 1 --preload --debug -p #{port}" puts command.inspect options = { wait_for: 'booted', timeout: 120, env: {} } WaitForIt.new(command, options) do |spawn| # at least 2 matches for TERM (so we set a timeout value longer - 120sec) spawn.wait!(/TERM.*TERM/m) term_ids = spawn.log.read.scan(/TERM to pid (\d*)/) assert term_ids.sort == term_ids.uniq.sort end end def test_rolling_pre_term file = fixture_path.join('rolling_pre_term.ru') port = 0 command = "bundle exec puma #{file} -t 1:1 -w 2 --preload --debug -p #{port}" puts command.inspect options = { wait_for: 'booted', timeout: 15, env: {} } WaitForIt.new(command, options) do |spawn| assert_contains(spawn, 'Rolling Restart') assert_contains(spawn, 'About to terminate (rolling) worker:') # defined in rolling_pre_term.ru end end end puma_worker_killer-0.3.1/test/fixtures/0000755000004100000410000000000013771343061020275 5ustar www-datawww-datapuma_worker_killer-0.3.1/test/fixtures/pre_term.ru0000644000004100000410000000037613771343061022470 0ustar www-datawww-data# frozen_string_literal: true load File.expand_path('fixture_helper.rb', __dir__) PumaWorkerKiller.config do |config| config.pre_term = ->(worker) { puts("About to terminate worker: #{worker.inspect}") } end PumaWorkerKiller.start run HelloWorldApp puma_worker_killer-0.3.1/test/fixtures/rolling_pre_term.ru0000644000004100000410000000050613771343061024211 0ustar www-datawww-data# frozen_string_literal: true load File.expand_path('fixture_helper.rb', __dir__) PumaWorkerKiller.config do |config| config.rolling_pre_term = ->(worker) { puts("About to terminate (rolling) worker: #{worker.pid}") } end PumaWorkerKiller.enable_rolling_restart(1, 0..5.0) # 1 second, short 1-5s splay. run HelloWorldApp puma_worker_killer-0.3.1/test/fixtures/big.ru0000644000004100000410000000030313771343061021402 0ustar www-datawww-data# frozen_string_literal: true load File.expand_path('fixture_helper.rb', __dir__) PumaWorkerKiller.start @memory = [] 10_000.times.each do @memory << SecureRandom.hex end run HelloWorldApp puma_worker_killer-0.3.1/test/fixtures/fixture_helper.rb0000644000004100000410000000104613771343061023650 0ustar www-datawww-data# frozen_string_literal: true require 'securerandom' require 'rack' require 'rack/server' require 'puma_worker_killer' PumaWorkerKiller.config do |config| config.ram = Integer(ENV['PUMA_RAM']) if ENV['PUMA_RAM'] config.frequency = Integer(ENV['PUMA_FREQUENCY']) if ENV['PUMA_FREQUENCY'] end puts "Frequency: #{PumaWorkerKiller.frequency}" if ENV['PUMA_FREQUENCY'] class HelloWorld def response(_env) [200, {}, ['Hello World']] end end class HelloWorldApp def self.call(env) HelloWorld.new.response(env) end end puma_worker_killer-0.3.1/test/fixtures/config/0000755000004100000410000000000013771343061021542 5ustar www-datawww-datapuma_worker_killer-0.3.1/test/fixtures/config/puma_worker_killer_start.rb0000644000004100000410000000024213771343061027177 0ustar www-datawww-data# frozen_string_literal: true load File.expand_path('../fixture_helper.rb', __dir__) before_fork do require 'puma_worker_killer' PumaWorkerKiller.start end puma_worker_killer-0.3.1/test/fixtures/on_calculation.ru0000644000004100000410000000037413771343061023643 0ustar www-datawww-data# frozen_string_literal: true load File.expand_path('fixture_helper.rb', __dir__) PumaWorkerKiller.config do |config| config.on_calculation = ->(usage) { puts("Current memory footprint: #{usage} mb") } end PumaWorkerKiller.start run HelloWorldApp puma_worker_killer-0.3.1/test/fixtures/rolling_restart.ru0000644000004100000410000000027013771343061024056 0ustar www-datawww-data# frozen_string_literal: true load File.expand_path('fixture_helper.rb', __dir__) PumaWorkerKiller.enable_rolling_restart(1, 0..5.0) # 1 second, short 1-5s splay. run HelloWorldApp puma_worker_killer-0.3.1/test/fixtures/default.ru0000644000004100000410000000017613771343061022275 0ustar www-datawww-data# frozen_string_literal: true load File.expand_path('fixture_helper.rb', __dir__) PumaWorkerKiller.start run HelloWorldApp puma_worker_killer-0.3.1/test/test_helper.rb0000644000004100000410000000030313771343061021263 0ustar www-datawww-data# frozen_string_literal: true Bundler.require require 'puma_worker_killer' require 'test/unit' require 'wait_for_it' def fixture_path Pathname.new(File.expand_path('fixtures', __dir__)) end puma_worker_killer-0.3.1/README.md0000644000004100000410000002231413771343061016726 0ustar www-datawww-data# Puma Worker Killer [![Build Status](https://travis-ci.org/schneems/puma_worker_killer.png?branch=master)](https://travis-ci.org/schneems/puma_worker_killer) [![Help Contribute to Open Source](https://www.codetriage.com/schneems/puma_worker_killer/badges/users.svg)](https://www.codetriage.com/schneems/puma_worker_killer) ## !!!!!!!!!!!!!!!! STOP !!!!!!!!!!!!!!!! Before you use this gem, know that it is dangerous. If you have a memory issue, you need to fix the issue. The original idea behind this gem is that it would act as a temporary band-aid to buy you time to allow you to fix your issues. If you turn this on and don't fix the underlying memory problems, then things will only get worse over time. This gem can also make your performance WORSE. When a worker is killed, and comes back it takes CPU cycles and time. If you are frequently restarting your workers then you're killing your performance. Here are some places to start improving your understanding of memory behvior in Ruby: - [Complete Guide to Rails Performance (Book)](https://www.railsspeed.com) - [How Ruby uses Memory](https://www.sitepoint.com/ruby-uses-memory/) - [Ruby Memory Use (Heroku Devcenter article I maintain)](https://devcenter.heroku.com/articles/ruby-memory-use) - [Jumping off the Ruby Memory Cliff](https://www.schneems.com/2017/04/12/jumping-off-the-memory-cliff/) - [How Ruby uses memory (Talk)](https://www.schneems.com/2015/05/11/how-ruby-uses-memory.html) (you can skip the first story in the video, the rest are about memory) - [Debugging a memory leak on Heroku](https://blog.codeship.com/debugging-a-memory-leak-on-heroku/) If you still need this gem, proceed with caution. ## What 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. Puma worker killer can only function if you have enabled cluster mode or hybrid mode (threads + worker cluster). If you are only using threads (and not workers) then puma worker killer cannot help keep your memory in control. BTW restarting your processes to control memory is like putting a bandaid on a gunshot wound, try figuring out the reason you're seeing so much memory bloat [derailed benchmarks](https://github.com/schneems/derailed_benchmarks) can help. ## Install In your Gemfile add: ```ruby gem 'puma_worker_killer' ``` Then run `$ bundle install` ## Turn on Rolling Restarts - Heroku Mode 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: ```ruby # config/puma.rb before_fork do require 'puma_worker_killer' PumaWorkerKiller.enable_rolling_restart # Default is every 6 hours end ``` or you can pass in the restart frequency: ```ruby PumaWorkerKiller.enable_rolling_restart(12 * 3600) # 12 hours in seconds ``` Make sure if you do this to not accidentally call `PumaWorkerKiller.start` as well. ## Enable Worker Killing If you're not running on a containerized platform (like Heroku or Docker) 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`: ```ruby # config/puma.rb before_fork do require 'puma_worker_killer' PumaWorkerKiller.start end ``` 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 and spawn a fresh copy with a much smaller RAM footprint ASAP. ## Troubleshooting When you boot your program locally you should see debug output: ``` [77773] Puma starting in cluster mode... [77773] * Version 3.1.0 (ruby 2.3.1-p112), codename: El Niño Winter Wonderland [77773] * Min threads: 0, max threads: 16 [77773] * Environment: development [77773] * Process workers: 2 [77773] * Phased restart available [77773] * Listening on tcp://0.0.0.0:9292 [77773] Use Ctrl-C to stop [77773] PumaWorkerKiller: Consuming 54.34765625 mb with master and 2 workers. ``` 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: ``` [77773] * Process workers: 2 ``` If you've configured PWK's frequency try reducing it to a very low value ## Configure Before calling `start` you can configure `PumaWorkerKiller`. You can do so using a configure block or calling methods directly: ```ruby PumaWorkerKiller.config do |config| config.ram = 1024 # mb config.frequency = 5 # seconds config.percent_usage = 0.98 config.rolling_restart_frequency = 12 * 3600 # 12 hours in seconds, or 12.hours if using Rails config.reaper_status_logs = true # setting this to false will not log lines like: # PumaWorkerKiller: Consuming 54.34765625 mb with master and 2 workers. config.pre_term = -> (worker) { puts "Worker #{worker.inspect} being killed" } config.rolling_pre_term = -> (worker) { puts "Worker #{worker.inspect} being killed by rolling restart" } end PumaWorkerKiller.start ``` ### pre_term `config.pre_term` will be called just prior to worker termination with the worker that is about to be terminated. This may be useful to use in keeping track of metrics, time of day workers are restarted, etc. By default Puma Worker Killer will emit a log when a worker is being killed ``` PumaWorkerKiller: Out of memory. 5 workers consuming total: 500 mb out of max: 450 mb. Sending TERM to pid 23 consuming 53 mb. ``` or ``` PumaWorkerKiller: Rolling Restart. 5 workers consuming total: 650mb mb. Sending TERM to pid 34. ``` However you may want to collect more data, such as sending an event to an error collection service like rollbar or airbrake. The `pre_term` lambda gets called before any worker is killed by PWK for any reason. ### rolling_pre_term `config.rolling_pre_term` will be called just prior to worker termination by rolling restart when rolling restart is enabled. It is similar to `config.pre_term`. Difference: - `config.pre_term` is triggered only by terminations related with exceeding RAM - `config.rolling_pre_term` is triggered only by terminations caused by enabled rolling restart ### on_calculation `config.on_calculation` will be called every time Puma Worker Killer calculates memory usage (`config.frequency`). This may be useful for monitoring your total puma application memory usage, which can be contrasted with other application monitoring solutions. This callback lambda is given a single value for the amount of memory used. ## Attention If you start puma as a daemon, to add puma worker killer config into puma config file, rather than into initializers: Sample like this: (in `config/puma.rb` file): ```ruby before_fork do PumaWorkerKiller.config do |config| config.ram = 1024 # mb config.frequency = 5 # seconds config.percent_usage = 0.98 config.rolling_restart_frequency = 12 * 3600 # 12 hours in seconds, or 12.hours if using Rails end PumaWorkerKiller.start end ``` 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: ```ruby PumaWorkerKiller.ram = 1024 # mb ``` 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: ```ruby PumaWorkerKiller.percent_usage = 0.98 ``` You may want to tune the worker killer to run more or less often. You can adjust frequency: ```ruby PumaWorkerKiller.frequency = 20 # seconds ``` You may want to periodically restart all of your workers rather than simply killing your largest. To do that set: ```ruby PumaWorkerKiller.rolling_restart_frequency = 12 * 3600 # 12 hours in seconds, or 12.hours if using Rails ``` By default PumaWorkerKiller will perform a rolling restart of all your worker processes every 6 hours. To disable, set to `false`. You may want to hide the following log lines: `PumaWorkerKiller: Consuming 54.34765625 mb with master and 2 workers.`. To do that set: ```ruby PumaWorkerKiller.reaper_status_logs = false ``` Note: It is `true` by default. ## License MIT ## Feedback Open up an issue or ping me on twitter [@schneems](http://twitter.com/schneems). puma_worker_killer-0.3.1/CHANGELOG.md0000644000004100000410000000153213771343061017257 0ustar www-datawww-data## Main ## 0.3.1 - Relax puma dependency (#94) ## 0.3.0 - Test on recent ruby versions #84 - Add option to adjust restart randomizer (#78) ## 0.2.0 - Simplify workers memory calculation in PumaMemory‘s `get_total` method #81 - Add rubocop in gemspec and CI, with offenses corrected and unnecessary cops disabled. - Add `pre_term`-like `rolling_pre_term` config for terminations caused by rolling restart (#86) - Fix compatibility with ruby version 2.3.X (#87) ## 0.1.1 - Allow PWK to be used with Puma 4 (#72) ## 0.1.0 - Emit extra data via `pre_term` callback before puma worker killer terminates a worker #49. ## 0.0.7 - Logging is configurable #41 ## 0.0.6 - Log PID of worker insead of inspecting the worker #33 ## 0.0.5 - Support for Puma 3.x ## 0.0.4 - Add ability to do rolling restart ## 0.0.3 - Fix memory metrics in on linux puma_worker_killer-0.3.1/.rubocop.yml0000644000004100000410000000055213771343061017721 0ustar www-datawww-datainherit_from: .rubocop_todo.yml Naming/AccessorMethodName: Enabled: false Style/Documentation: Enabled: false Style/HashSyntax: Enabled: false Style/ModuleFunction: Enabled: false Style/StringLiterals: EnforcedStyle: single_quotes Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes Gemspec/RequiredRubyVersion: Enabled: false puma_worker_killer-0.3.1/.gitignore0000644000004100000410000000003413771343061017432 0ustar www-datawww-dataGemfile.lock *.gem puma.log puma_worker_killer-0.3.1/.rubocop_todo.yml0000644000004100000410000000146013771343061020745 0ustar www-datawww-data# This configuration was generated by # `rubocop --auto-gen-config` # on 2020-04-03 13:12:53 +0200 using RuboCop version 0.81.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 1 # Configuration parameters: IgnoredMethods. Metrics/AbcSize: Max: 21 # Offense count: 1 # Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: Max: 11 # Offense count: 32 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https Layout/LineLength: Max: 252 puma_worker_killer-0.3.1/Rakefile0000644000004100000410000000037613771343061017120 0ustar www-datawww-data# frozen_string_literal: true require 'bundler/gem_tasks' require 'rake' require 'rake/testtask' task :default => [:test] Rake::TestTask.new(:test) do |t| t.libs << 'lib' t.libs << 'test' t.pattern = 'test/**/*_test.rb' t.verbose = false end puma_worker_killer-0.3.1/lib/0000755000004100000410000000000013771343061016213 5ustar www-datawww-datapuma_worker_killer-0.3.1/lib/puma_worker_killer.rb0000644000004100000410000000332213771343061022435 0ustar www-datawww-data# frozen_string_literal: true require 'get_process_mem' module PumaWorkerKiller extend self attr_accessor :ram, :frequency, :percent_usage, :rolling_restart_frequency, :rolling_restart_splay_seconds, :reaper_status_logs, :pre_term, :rolling_pre_term, :on_calculation self.ram = 512 # mb self.frequency = 10 # seconds self.percent_usage = 0.99 # percent of RAM to use self.rolling_restart_frequency = 6 * 3600 # 6 hours in seconds self.rolling_restart_splay_seconds = 0.0..300.0 # 0 to 5 minutes in seconds self.reaper_status_logs = true self.pre_term = nil self.rolling_pre_term = nil self.on_calculation = nil def config yield self end def reaper(ram = self.ram, percent_usage = self.percent_usage, reaper_status_logs = self.reaper_status_logs, pre_term = self.pre_term, on_calculation = self.on_calculation) Reaper.new(ram * percent_usage, nil, reaper_status_logs, pre_term, on_calculation) end def start(frequency = self.frequency, reaper = self.reaper) AutoReap.new(frequency, reaper).start enable_rolling_restart(rolling_restart_frequency) if rolling_restart_frequency end def enable_rolling_restart(frequency = rolling_restart_frequency, splay_seconds = rolling_restart_splay_seconds) # Randomize so all workers don't restart at the exact same time across multiple machines. frequency += rand(splay_seconds) AutoReap.new(frequency, RollingRestart.new(nil, rolling_pre_term)).start end end require 'puma_worker_killer/puma_memory' require 'puma_worker_killer/reaper' require 'puma_worker_killer/rolling_restart' require 'puma_worker_killer/auto_reap' require 'puma_worker_killer/version' puma_worker_killer-0.3.1/lib/puma_worker_killer/0000755000004100000410000000000013771343061022110 5ustar www-datawww-datapuma_worker_killer-0.3.1/lib/puma_worker_killer/version.rb0000644000004100000410000000011713771343061024121 0ustar www-datawww-data# frozen_string_literal: true module PumaWorkerKiller VERSION = '0.3.1' end puma_worker_killer-0.3.1/lib/puma_worker_killer/auto_reap.rb0000644000004100000410000000057413771343061024422 0ustar www-datawww-data# frozen_string_literal: true module PumaWorkerKiller class AutoReap def initialize(timeout, reaper = Reaper.new) @timeout = timeout # seconds @reaper = reaper @running = false end def start @running = true Thread.new do while @running sleep @timeout @reaper.reap end end end end end puma_worker_killer-0.3.1/lib/puma_worker_killer/reaper.rb0000644000004100000410000000325213771343061023715 0ustar www-datawww-data# frozen_string_literal: true module PumaWorkerKiller class Reaper def initialize(max_ram, master = nil, reaper_status_logs = true, pre_term = nil, on_calculation = nil) @cluster = PumaWorkerKiller::PumaMemory.new(master) @max_ram = max_ram @reaper_status_logs = reaper_status_logs @pre_term = pre_term @on_calculation = on_calculation end # used for tes def get_total_memory @cluster.get_total_memory end def reap return false if @cluster.workers_stopped? total = get_total_memory @on_calculation&.call(total) if total > @max_ram @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." # Fetch the largest_worker so that both `@pre_term` and `term_worker` are called with the same worker # Avoids a race condition where: # Worker A consume 100 mb memory # Worker B consume 99 mb memory # pre_term gets called with Worker A # A new request comes in, Worker B takes it, and consumes 101 mb memory # term_largest_worker (previously here) gets called and terms Worker B (thus not passing the about-to-be-terminated worker to `@pre_term`) largest_worker = @cluster.largest_worker @pre_term&.call(largest_worker) @cluster.term_worker(largest_worker) elsif @reaper_status_logs @cluster.master.log "PumaWorkerKiller: Consuming #{total} mb with master and #{@cluster.workers.count} workers." end end end end puma_worker_killer-0.3.1/lib/puma_worker_killer/rolling_restart.rb0000644000004100000410000000152613771343061025653 0ustar www-datawww-data# frozen_string_literal: true module PumaWorkerKiller class RollingRestart def initialize(master = nil, rolling_pre_term = nil) @cluster = PumaWorkerKiller::PumaMemory.new(master) @rolling_pre_term = rolling_pre_term end # used for tes def get_total_memory @cluster.get_total_memory end def reap(seconds_between_worker_kill = 60) # this will implicitly call set_workers total_memory = get_total_memory return false unless @cluster.running? @cluster.workers.each do |worker, _ram| @cluster.master.log "PumaWorkerKiller: Rolling Restart. #{@cluster.workers.count} workers consuming total: #{total_memory} mb. Sending TERM to pid #{worker.pid}." @rolling_pre_term&.call(worker) worker.term sleep seconds_between_worker_kill end end end end puma_worker_killer-0.3.1/lib/puma_worker_killer/puma_memory.rb0000644000004100000410000000334613771343061024775 0ustar www-datawww-data# frozen_string_literal: true module PumaWorkerKiller class PumaMemory def initialize(master = nil) @master = master || get_master @workers = nil end attr_reader :master def size workers.size end def term_worker(worker) worker.term end def term_largest_worker largest_worker.term end def workers_stopped? !running? end def running? @master && workers.any? end def smallest_worker smallest, = workers.to_a.first smallest end def smallest_worker_memory _, smallest_mem = workers.to_a.first smallest_mem end def largest_worker largest_worker, = workers.to_a.last largest_worker end def largest_worker_memory _, largest_memory_used = workers.to_a.last largest_memory_used end # Will refresh @workers def get_total(workers = set_workers) master_memory = GetProcessMem.new(Process.pid).mb worker_memory = workers.values.inject(:+) || 0 worker_memory + master_memory end alias get_total_memory get_total def workers @workers || set_workers end private def get_master ObjectSpace.each_object(Puma::Cluster).map { |obj| obj }.first if defined?(Puma::Cluster) end # Returns sorted hash, keys are worker objects, values are memory used per worker # sorted by memory ascending (smallest first, largest last) def set_workers workers = {} @master.instance_variable_get('@workers').each do |worker| workers[worker] = GetProcessMem.new(worker.pid).mb end if workers.any? @workers = Hash[workers.sort_by { |_, mem| mem }] else {} end end end end puma_worker_killer-0.3.1/puma_worker_killer.gemspec0000644000004100000410000000230013771343061022702 0ustar www-datawww-data# frozen_string_literal: true lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'puma_worker_killer/version' Gem::Specification.new do |gem| gem.name = 'puma_worker_killer' gem.version = PumaWorkerKiller::VERSION gem.authors = ['Richard Schneeman'] gem.email = ['richard.schneeman+rubygems@gmail.com'] gem.description = ' Kills pumas, the code kind ' gem.summary = ' If you have a memory leak in your web code puma_worker_killer can keep it in check. ' gem.homepage = 'https://github.com/schneems/puma_worker_killer' gem.license = 'MIT' gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ['lib'] gem.add_dependency 'get_process_mem', '~> 0.2' gem.add_dependency 'puma', '>= 2.7' gem.add_development_dependency 'rack', '~> 2.0' gem.add_development_dependency 'rake', '~> 13.0' gem.add_development_dependency 'test-unit', '>= 0' gem.add_development_dependency 'wait_for_it', '~> 0.1' end puma_worker_killer-0.3.1/Gemfile0000644000004100000410000000014513771343061016740 0ustar www-datawww-data# frozen_string_literal: true source 'https://rubygems.org' gemspec gem 'rubocop', require: false puma_worker_killer-0.3.1/.github/0000755000004100000410000000000013771343061017005 5ustar www-datawww-datapuma_worker_killer-0.3.1/.github/workflows/0000755000004100000410000000000013771343061021042 5ustar www-datawww-datapuma_worker_killer-0.3.1/.github/workflows/check_changelog.yml0000644000004100000410000000057113771343061024654 0ustar www-datawww-dataname: Check Changelog on: [pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Check that CHANGELOG is touched run: | cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md