puma_worker_killer-1.0.0/0000755000004100000410000000000014664106604015444 5ustar www-datawww-datapuma_worker_killer-1.0.0/puma_worker_killer.gemspec0000644000004100000410000000220114664106604022701 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($/) gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.require_paths = ["lib"] gem.add_dependency "puma", ">= 2.7" gem.add_dependency "bigdecimal", ">= 2.0" gem.add_dependency "get_process_mem", ">= 0.2" gem.add_development_dependency "rack", ">= 3.0" gem.add_development_dependency "rake", ">= 13.0" gem.add_development_dependency "rackup", ">= 2.1" gem.add_development_dependency "test-unit", ">= 0" gem.add_development_dependency "wait_for_it", ">= 0.1" end puma_worker_killer-1.0.0/.gitignore0000644000004100000410000000003414664106604017431 0ustar www-datawww-dataGemfile.lock *.gem puma.log puma_worker_killer-1.0.0/.standard.yml0000644000004100000410000000002214664106604020037 0ustar www-datawww-dataruby_version: 3.1 puma_worker_killer-1.0.0/.github/0000755000004100000410000000000014664106604017004 5ustar www-datawww-datapuma_worker_killer-1.0.0/.github/workflows/0000755000004100000410000000000014664106604021041 5ustar www-datawww-datapuma_worker_killer-1.0.0/.github/workflows/check_changelog.yml0000644000004100000410000000063414664106604024653 0ustar www-datawww-dataname: Check Changelog on: [pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - 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 puma_worker_killer-1.0.0/.github/workflows/ci.yml0000644000004100000410000000113414664106604022156 0ustar www-datawww-dataname: CI on: - push - pull_request jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby: - 3.1 - 3.2 - 3.3 - head steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Ruby linting run: bundle exec standardrb - name: test run: bundle exec rake test continue-on-error: ${{ matrix.ruby == 'head' }} puma_worker_killer-1.0.0/lib/0000755000004100000410000000000014664106604016212 5ustar www-datawww-datapuma_worker_killer-1.0.0/lib/puma_worker_killer/0000755000004100000410000000000014664106604022107 5ustar www-datawww-datapuma_worker_killer-1.0.0/lib/puma_worker_killer/rolling_restart.rb0000644000004100000410000000152614664106604025652 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-1.0.0/lib/puma_worker_killer/version.rb0000644000004100000410000000011714664106604024120 0ustar www-datawww-data# frozen_string_literal: true module PumaWorkerKiller VERSION = "1.0.0" end puma_worker_killer-1.0.0/lib/puma_worker_killer/auto_reap.rb0000644000004100000410000000057314664106604024420 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-1.0.0/lib/puma_worker_killer/puma_memory.rb0000644000004100000410000000335514664106604024774 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_method :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 = workers.sort_by { |_, mem| mem }.to_h else {} end end end end puma_worker_killer-1.0.0/lib/puma_worker_killer/reaper.rb0000644000004100000410000000325214664106604023714 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-1.0.0/lib/puma_worker_killer.rb0000644000004100000410000000322314664106604022434 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-1.0.0/test/0000755000004100000410000000000014664106604016423 5ustar www-datawww-datapuma_worker_killer-1.0.0/test/test_helper.rb0000644000004100000410000000030314664106604021262 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-1.0.0/test/puma_worker_killer_test.rb0000644000004100000410000000737514664106604023720 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-1.0.0/test/fixtures/0000755000004100000410000000000014664106604020274 5ustar www-datawww-datapuma_worker_killer-1.0.0/test/fixtures/rolling_restart.ru0000644000004100000410000000027014664106604024055 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-1.0.0/test/fixtures/rolling_pre_term.ru0000644000004100000410000000050614664106604024210 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-1.0.0/test/fixtures/default.ru0000644000004100000410000000017614664106604022274 0ustar www-datawww-data# frozen_string_literal: true load File.expand_path("fixture_helper.rb", __dir__) PumaWorkerKiller.start run HelloWorldApp puma_worker_killer-1.0.0/test/fixtures/big.ru0000644000004100000410000000030314664106604021401 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-1.0.0/test/fixtures/on_calculation.ru0000644000004100000410000000037414664106604023642 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-1.0.0/test/fixtures/pre_term.ru0000644000004100000410000000037614664106604022467 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-1.0.0/test/fixtures/fixture_helper.rb0000644000004100000410000000103414664106604023644 0ustar www-datawww-data# frozen_string_literal: true require "securerandom" require "rack" require "rackup/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-1.0.0/test/fixtures/config/0000755000004100000410000000000014664106604021541 5ustar www-datawww-datapuma_worker_killer-1.0.0/test/fixtures/config/puma_worker_killer_start.rb0000644000004100000410000000024214664106604027176 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-1.0.0/Rakefile0000644000004100000410000000037314664106604017114 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-1.0.0/.rubocop_todo.yml0000644000004100000410000000146014664106604020744 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-1.0.0/Gemfile0000644000004100000410000000021114664106604016731 0ustar www-datawww-data# frozen_string_literal: true source "https://rubygems.org" gemspec # This is the last version which supports Ruby 2.3 gem "standard" puma_worker_killer-1.0.0/README.md0000644000004100000410000002235014664106604016725 0ustar www-datawww-data# Puma Worker Killer [![CI](https://github.com/schneems/puma_worker_killer/actions/workflows/ci.yml/badge.svg)](https://github.com/schneems/puma_worker_killer/actions/workflows/ci.yml) [![Help Contribute to Open Source](https://www.codetriage.com/zombocom/puma_worker_killer/badges/users.svg)](https://www.codetriage.com/zombocom/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 behavior 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 rolling 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-1.0.0/.rubocop.yml0000644000004100000410000000055214664106604017720 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-1.0.0/CHANGELOG.md0000644000004100000410000000205514664106604017257 0ustar www-datawww-data## Main ## 1.0.0 - Add `bigdecimal` as a dependency (https://github.com/zombocom/puma_worker_killer/pull/109) - Ruby versions prior to 3.1 may no longer work (https://github.com/zombocom/puma_worker_killer/pull/109) ## 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 instead 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