├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── README.md ├── Rakefile ├── bin └── heroku_ulimit_to_ram ├── lib ├── puma_auto_tune.rb └── puma_auto_tune │ ├── defaults │ └── ram │ │ ├── hooks.rb │ │ └── wrappers.rb │ ├── hook.rb │ ├── master.rb │ ├── memory.rb │ ├── version.rb │ └── worker.rb ├── my.log ├── puma_auto_tune.gemspec └── test ├── fixtures ├── app.ru └── config.rb ├── puma_auto_tune_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | *.gem 3 | test/logs/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.1 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 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.2 2 | 3 | - Fix memory metrics in on linux (#6) 4 | - max_workers variable is now max_worker_limit 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Puma Auto Tune 2 | 3 | [![Build Status](https://travis-ci.org/schneems/puma_auto_tune.png?branch=master)](https://travis-ci.org/schneems/puma_auto_tune) 4 | 5 | ## Stability 6 | 7 | Please do not use this library in production until this [schneems/get_process_mem#7](https://github.com/schneems/get_process_mem/issues/7) has been fixed. 8 | 9 | ## What 10 | 11 | Performance without the (T)pain: `puma_auto_tune` will automatically adjust the number of [puma](https://github.com/puma/puma) workers to optimize the performance of your Ruby web application. 12 | 13 | ## How 14 | 15 | Puma is a web server that allows you to adjust the amount of processes and threads it uses to process requests. At a very simple level the more processes and threads you have the more requests you can process concurrently. However this comes at a cost, more processes means more RAM and more threads means more CPU usage. You want to get as close to maxing out your resources without going over. 16 | 17 | The amount of memory and CPU your program consumes is also a factor of your code, as well as the amount of load it is under. Larger applications require more RAM. More requests mean more Ruby objects are created and garbage collected as your application generates web pages. Because of these factors, there is no one size fits all number for workers and threads, that's where Puma Auto Tune comes in. 18 | 19 | Run Puma Auto Tune in production under load, or in staging while simulating load with tools like [siege](http://www.joedog.org/siege-home/), [blitz.io](https://www.blitz.io/), or [flood.io](https://flood.io/) for a long enough time and we will compute and set your application numbers to maximize concurrent requests without going over your system limits. 20 | 21 | Currently Puma Auto Tune will optimize the number of workers (processes) based on RAM. 22 | 23 | ## Install 24 | 25 | In your `Gemfile` add: 26 | 27 | ```ruby 28 | gem 'puma_auto_tune' 29 | ``` 30 | 31 | Then run `$ bundle install`. 32 | 33 | ## Use 34 | 35 | In your application call: 36 | 37 | ```ruby 38 | PumaAutoTune.start 39 | ``` 40 | 41 | In Rails you could place this in an initializer such as `config/initializers/puma_auto_tune.rb`. 42 | 43 | Puma Auto Tune will attempt to find an ideal number of workers for your application. 44 | 45 | 46 | ## Config 47 | 48 | You will need to configure your Puma Auto Tune to be aware of the maximum amount of RAM it can use. 49 | 50 | ```ruby 51 | PumaAutoTune.config do |config| 52 | config.ram = 512 # mb: available on system 53 | end 54 | ``` 55 | 56 | We will attempt to detect your RAM size if you are running on Heroku. If we cannot, the default is `512` mb. There are a few other advanced config options: 57 | 58 | ```ruby 59 | PumaAutoTune.config do |config| 60 | config.ram = 1024 # mb: available on system 61 | config.frequency = 20 # seconds: the duration to check memory usage 62 | config.reap_duration = 30 # seconds: how long `reap_cycle` will be run for 63 | end 64 | ``` 65 | 66 | To see defaults check out [puma_auto_tune.rb](lib/puma_auto_tune.rb) 67 | 68 | 69 | ## Hitting the Sweet Spot 70 | 71 | Puma Auto Tune is designed to tune the number of workers for a given application while it is running. Once you restart the program the tuning must start over. Once the algorithm has found the "sweet spot" you can maximize your application throughput by manually setting the number of `workers` that puma starts with. To help you do this Puma Auto Tune outputs semi-regular logs with formatted values. 72 | 73 | ``` 74 | measure#puma.resource_ram_mb=476.6328125 measure#puma.current_cluster_size=5 75 | ``` 76 | 77 | You can use a service such as [librato](https://metrics.librato.com/) to pull values out of your logs and graph them. When you see over time that your server settles on a given `cluster_size` you should set this as your default `puma -w $PUMA_WORKERS` if you're using the CLI to start your app or if you're using a `config/puma.rb` file: 78 | 79 | ```ruby 80 | workers Integer(ENV['PUMA_WORKERS'] || 3) 81 | ``` 82 | 83 | ## Puma Worker Killer 84 | 85 | Do not use with `puma_worker_killer` gem. Puma Auto Tune takes care of memory leaks in addition to tuning your puma workers. 86 | 87 | 88 | ## How it Works: Tuning Algorithm (RAM) 89 | 90 | Simple by default, custom for true Puma hackers. The best way to think of the tuner is to start with the different states of memory consumption Puma can be under: 91 | 92 | - Unused RAM: we can add a worker 93 | - Memory leak (too much RAM usage): we should restart a worker 94 | - Too much RAM usage: we can remove a worker 95 | - Just right: No need to scale up or down. 96 | 97 | The algorithm will periodically get the total memory used by Puma and take action appropriately. 98 | 99 | #### Memory States: Unused RAM 100 | 101 | The memory of the smallest worker is recorded. If adding another worker does not put the total memory over the threshold then one will be added. 102 | 103 | #### Memory States: Memory Leak (too much RAM usage) 104 | 105 | When the amount of memory is more than that on the system, we assume a memory leak and restart the largest worker. This will trigger a check to determine if the result was due to a memory leak or because we have too many workers. 106 | 107 | #### Memory States: Too much RAM Usage 108 | 109 | After a worker has been restarted we will aggressively check for memory usage for a fixed period of time, default is 90 seconds(`PumaAutoTune.reap_reap_duration`). If memory goes over the limit, it is assumed that the cause is due to excess workers. The number of workers will be decreased by one. Puma Auto Tune will record the number of total workers that were present when we went over and set this as a new maximum worker number. After removing a process, Puma Auto Tune again checks for memory overages for the same duration and continues to decrement the number of workers until the total memory consumed is under the maximum. 110 | 111 | #### Memory States: Just Right 112 | 113 | Periodically the tuner will wake up and take note of memory usage. If it cannot scale up, and doesn't need to scale down it goes back to sleep. 114 | 115 | ## Customizing the Algorithm 116 | 117 | Here's the fun part. You can write your own algorithm using the included hook system. The default algorithm is implemented as a series of [pre-defined hooks](lib/puma_auto_tune/defaults/ram/hooks.rb). 118 | 119 | You can over-write one or more of the hooks to add custom behavior. To define hooks call: 120 | 121 | ```ruby 122 | PumaAutoTune.hooks(:ram) do |auto| 123 | 124 | end 125 | ``` 126 | 127 | Each hook has a name and can be over-written by calling `set` and passing in the symbol of the hook you wish to over-write. These are the default RAM hooks: 128 | 129 | - `:cycle` 130 | - `:reap_cycle` 131 | - `:out_of_memory` 132 | - `:under_memory` 133 | - `:add_worker` 134 | - `:remove_worker` 135 | 136 | 137 | Once you have the hook object you can use the `call` method to jump to other hooks. 138 | 139 | ### Cycle 140 | 141 | This is the main event loop of your program. This code will be called every `PumaAutoTune.frequency` seconds. To over-write you can do this: 142 | 143 | 144 | ```ruby 145 | PumaAutoTune.hooks(:ram) do |auto| 146 | auto.set(:cycle) do |memory, master, workers| 147 | if memory > PumaAutoTune.ram # mb 148 | auto.call(:out_of_memory) 149 | else 150 | auto.call(:under_memory) if memory + workers.last.memory 151 | end 152 | end 153 | end 154 | ``` 155 | 156 | ### Reap Cycle 157 | 158 | When you think you might run out of memory call the `reap_cycle`. The code in this hook will be called in a loop for `PumaAutoTune.reap_duration` seconds. 159 | 160 | ```ruby 161 | PumaAutoTune.hooks do |auto| 162 | auto.set(:reap_cycle) do |memory, master, workers| 163 | if memory > PumaAutoTune.ram 164 | auto.call(:remove_worker) 165 | end 166 | end 167 | end 168 | ``` 169 | 170 | ## Add Worker 171 | 172 | Bumps up the worker size by one. 173 | 174 | ```ruby 175 | PumaAutoTune.hooks do |auto| 176 | auto.set(:add_worker) do |memory, master, workers| 177 | auto.log "Cluster too small. Resizing to add one more worker" 178 | master.add_worker 179 | auto.call(:reap_cycle) 180 | end 181 | end 182 | ``` 183 | 184 | Here we're calling `:reap_cycle` just in case we accidentally went over our memory limit after the increase. 185 | 186 | ## Remove Worker 187 | 188 | Removes a worker. When `remove_worker` is called it will automatically set `PumaAutoTune.max_workers` to be one less than the current number of workers. 189 | 190 | ```ruby 191 | PumaAutoTune.hooks do |hook| 192 | auto.set(:remove_worker) do |memory, master, workers| 193 | auto.log "Cluster too large. Resizing to remove one worker" 194 | master.remove_worker 195 | auto.call(:reap_cycle) 196 | end 197 | end 198 | ``` 199 | 200 | In case removing one worker wasn't enough we call `reap_cycle` again. Once a worker has been flagged with `restart` it will report zero RAM usage even if it has not completely terminated. 201 | 202 | ## License 203 | 204 | MIT 205 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /bin/heroku_ulimit_to_ram: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | limit=$(ulimit -u) 4 | 5 | case $limit in 6 | 256) 7 | ram=512 8 | ;; 9 | 512) 10 | ram=1024 11 | ;; 12 | 32768) 13 | ram=14000 14 | ;; 15 | 16384) 16 | ram=2500 17 | ;; 18 | *) 19 | echo 'nope'; 20 | exit 1; 21 | ;; 22 | esac 23 | 24 | echo $ram 25 | -------------------------------------------------------------------------------- /lib/puma_auto_tune.rb: -------------------------------------------------------------------------------- 1 | require 'get_process_mem' 2 | 3 | module PumaAutoTune; end 4 | 5 | require 'puma_auto_tune/version' 6 | require 'puma_auto_tune/master' 7 | require 'puma_auto_tune/worker' 8 | require 'puma_auto_tune/memory' 9 | 10 | 11 | module PumaAutoTune 12 | INFINITY = 1/0.0 13 | RESOURCES = { ram: PumaAutoTune::Memory.new } 14 | 15 | extend self 16 | 17 | def self.default_ram 18 | result = `bin/heroku_ulimit_to_ram` 19 | default = if $?.success? 20 | Integer(result) 21 | else 22 | 512 23 | end 24 | puts "Default RAM set to #{default}" 25 | default 26 | end 27 | 28 | attr_accessor :ram, :max_worker_limit, :frequency, :reap_duration 29 | self.ram = self.default_ram # mb 30 | self.max_worker_limit = INFINITY 31 | self.frequency = 10 # seconds 32 | self.reap_duration = 90 # seconds 33 | 34 | def self.config 35 | yield self 36 | self 37 | end 38 | 39 | def self.hooks(name = nil, resource = nil, &block) 40 | @hooks ||= {} 41 | return @hooks if name.nil? 42 | resource ||= RESOURCES[name] || raise("no default resource specified for #{name.inspect}") 43 | @hooks[name] ||= Hook.new(resource) 44 | block.call(@hooks[name]) if block 45 | @hooks[name] 46 | end 47 | 48 | def start 49 | hooks.map {|name, hook| hook.auto_cycle } 50 | end 51 | end 52 | 53 | 54 | require 'puma_auto_tune/hook' 55 | -------------------------------------------------------------------------------- /lib/puma_auto_tune/defaults/ram/hooks.rb: -------------------------------------------------------------------------------- 1 | ## This is the default algorithm 2 | PumaAutoTune.hooks(:ram) do |auto| 3 | # Runs in a continual loop controlled by PumaAutoTune.frequency 4 | auto.set(:cycle) do |memory, master, workers| 5 | if memory > PumaAutoTune.ram # mb 6 | auto.call(:out_of_memory) 7 | else 8 | auto.call(:under_memory) 9 | end 10 | end 11 | 12 | # Called repeatedly for `PumaAutoTune.reap_duration`. 13 | # call when you think you may have too many workers 14 | auto.set(:reap_cycle) do |memory, master, workers| 15 | if memory > PumaAutoTune.ram 16 | auto.call(:remove_worker) 17 | end 18 | end 19 | 20 | # Called when puma is using too much memory 21 | auto.set(:out_of_memory) do |memory, master, workers| 22 | if workers.size > 1 23 | largest_worker = workers.last # ascending worker size 24 | auto.log "Potential memory leak. Reaping largest worker", largest_worker_memory_mb: largest_worker.memory 25 | largest_worker.restart 26 | auto.call(:reap_cycle) 27 | else 28 | auto.log "Out of memory but cannot have less than one worker, you need more RAM" 29 | end 30 | end 31 | 32 | # Called when puma is not using all available memory 33 | # PumaAutoTune.max_workers is tracked automatically by `remove_worker` 34 | auto.set(:under_memory) do |memory, master, workers| 35 | theoretical_max_mb = memory + workers.first.memory # ascending worker size 36 | if (theoretical_max_mb < PumaAutoTune.ram) && (workers.size.next < PumaAutoTune.max_worker_limit) 37 | auto.call(:add_worker) 38 | else 39 | auto.log "All is well" 40 | end 41 | end 42 | 43 | # Called to add an extra worker 44 | auto.set(:add_worker) do |memory, master, workers| 45 | auto.log "Cluster too small. Resizing to add one more worker" 46 | master.add_worker 47 | auto.call(:reap_cycle) 48 | end 49 | 50 | # Called to remove 1 worker from pool. Sets maximum size 51 | auto.set(:remove_worker) do |memory, master, workers| 52 | if workers.size > 1 53 | PumaAutoTune.max_worker_limit = workers.size - 1 54 | auto.log "Cluster too large. Resizing to remove one worker" 55 | master.remove_worker 56 | auto.call(:reap_cycle) 57 | else 58 | auto.log "Out of memory but cannot have less than one worker, you need more RAM" 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/puma_auto_tune/defaults/ram/wrappers.rb: -------------------------------------------------------------------------------- 1 | PumaAutoTune.hooks(:ram) do |auto| 2 | auto.wrap(:reap_cycle) do |orig| 3 | Proc.new do |resource, master, workers| 4 | ends_at = Time.now + PumaAutoTune.reap_duration 5 | while Time.now < ends_at 6 | sleep 1 7 | orig.call(*auto.args) 8 | end 9 | end 10 | end 11 | 12 | auto.wrap(:cycle) do |orig| 13 | Proc.new do |resource, master, workers| 14 | loop do 15 | sleep PumaAutoTune.frequency 16 | orig.call(*auto.args) if master.running? 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/puma_auto_tune/hook.rb: -------------------------------------------------------------------------------- 1 | module PumaAutoTune 2 | class Hook 3 | 4 | def initialize(resource) 5 | @resource = resource 6 | @started = Time.now 7 | @hooks = {} 8 | @wraps = {} 9 | end 10 | 11 | def define_hook(name, &block) 12 | if wrap = @wraps[name] 13 | @hooks[name] = wrap.call(block) 14 | else 15 | @hooks[name] = block 16 | end 17 | end 18 | alias :set :define_hook 19 | 20 | def call(name) 21 | hook = @hooks[name] or raise "No such hook #{name.inspect}. Available: #{@hooks.keys.inspect}" 22 | hook.call(*self.args) 23 | end 24 | 25 | # define a hook by passing a block 26 | def wrap_hook(name, &block) 27 | @wraps[name] = block 28 | end 29 | alias :wrap :wrap_hook 30 | 31 | def auto_cycle 32 | Thread.new do 33 | self.call(:cycle) 34 | end 35 | end 36 | 37 | def log(msg, options = {}) 38 | elapsed = (Time.now - @started).ceil 39 | msg = ["PumaAutoTune (#{elapsed}s): #{msg}"] 40 | 41 | options[@resource.name] = @resource.amount 42 | options["current_cluster_size"] = @resource.workers.size 43 | options["max_worker_limit"] = PumaAutoTune.max_worker_limit 44 | options.each { |k, v| msg << "measure#puma.#{k.to_s.downcase}=#{v}" } 45 | puts msg.join(" ") 46 | end 47 | 48 | def args 49 | @resource.reset 50 | [@resource.amount, @resource.master, @resource.workers] 51 | end 52 | end 53 | end 54 | 55 | require 'puma_auto_tune/defaults/ram/wrappers' 56 | require 'puma_auto_tune/defaults/ram/hooks' 57 | -------------------------------------------------------------------------------- /lib/puma_auto_tune/master.rb: -------------------------------------------------------------------------------- 1 | module PumaAutoTune 2 | class Master 3 | def initialize(master = nil) 4 | @master = master || get_master 5 | end 6 | 7 | def running? 8 | @master && workers.any? 9 | end 10 | 11 | # https://github.com/puma/puma/blob/master/docs/signals.md#puma-signals 12 | def remove_worker 13 | previous_worker_count = workers.size 14 | until workers.size < previous_worker_count 15 | send_signal("TTOU") 16 | sleep 1 17 | end 18 | end 19 | 20 | # https://github.com/puma/puma/blob/master/docs/signals.md#puma-signals 21 | def add_worker 22 | send_signal("TTIN") 23 | end 24 | 25 | # less cryptic interface 26 | def send_signal(signal, pid = Process.pid) 27 | Process.kill(signal, pid) 28 | end 29 | 30 | def memory 31 | @memory 32 | end 33 | alias :mb :memory 34 | 35 | def get_memory 36 | @memory = ::GetProcessMem.new(Process.pid).mb 37 | end 38 | 39 | def workers 40 | @master.instance_variable_get("@workers"). 41 | reject { |w| w.instance_variable_get("@first_term_sent") }. 42 | map { |w| PumaAutoTune::Worker.new(w) } 43 | end 44 | 45 | private 46 | 47 | def get_master 48 | ObjectSpace.each_object(Puma::Cluster).map { |obj| obj }.first if defined?(Puma::Cluster) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/puma_auto_tune/memory.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | 3 | module PumaAutoTune 4 | 5 | class Memory 6 | attr_accessor :master, :workers 7 | 8 | def initialize(master = PumaAutoTune::Master.new) 9 | @master = master 10 | end 11 | 12 | def name 13 | "resource_ram_mb" 14 | end 15 | 16 | def amount 17 | @mb ||= begin 18 | worker_memory = workers.map {|w| w.memory }.inject(&:+) || 0 19 | worker_memory + @master.get_memory 20 | end 21 | end 22 | 23 | def largest_worker 24 | workers.last 25 | end 26 | 27 | def smallest_worker 28 | workers.first 29 | end 30 | 31 | def workers 32 | workers ||= @master.workers.sort_by! {|w| w.get_memory } 33 | end 34 | 35 | def reset 36 | raise "must set master" unless @master 37 | @workers = nil 38 | @mb = nil 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /lib/puma_auto_tune/version.rb: -------------------------------------------------------------------------------- 1 | module PumaAutoTune 2 | VERSION = '0.0.2' 3 | end 4 | -------------------------------------------------------------------------------- /lib/puma_auto_tune/worker.rb: -------------------------------------------------------------------------------- 1 | module PumaAutoTune 2 | class Worker 3 | 4 | def initialize(worker) 5 | @worker = worker 6 | @restarting = false 7 | end 8 | 9 | def memory 10 | @memory || get_memory 11 | end 12 | alias :mb :memory 13 | 14 | def get_memory 15 | @memory = if restarting? 16 | 0 17 | else 18 | ::GetProcessMem.new(self.pid).mb 19 | end 20 | end 21 | 22 | def restarting? 23 | @restarting 24 | end 25 | 26 | 27 | def restart 28 | @restarting = true 29 | @worker.term 30 | end 31 | 32 | def pid 33 | @worker.pid 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /my.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schneems/puma_auto_tune/0942cee51df1fae7b4326b4f2deaedf663c6628f/my.log -------------------------------------------------------------------------------- /puma_auto_tune.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'puma_auto_tune/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "puma_auto_tune" 8 | gem.version = PumaAutoTune::VERSION 9 | gem.authors = ["Richard Schneeman"] 10 | gem.email = ["richard.schneeman+rubygems@gmail.com"] 11 | gem.description = %q{ Puma performance without all the (T)pain } 12 | gem.summary = %q{ } 13 | gem.homepage = "https://github.com/schneems/puma_auto_tune" 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 | 22 | gem.add_dependency "puma", ">= 2.7" 23 | gem.add_dependency "get_process_mem", "~> 0.1" 24 | gem.add_development_dependency "rake", "~> 10.1" 25 | gem.add_development_dependency "test-unit", "~> 3.1" 26 | gem.add_development_dependency "rack", ">= 1" 27 | end 28 | -------------------------------------------------------------------------------- /test/fixtures/app.ru: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'rack/server' 3 | 4 | run Proc.new {|env| [200, {}, ['Hello World']] } 5 | 6 | 7 | 8 | require 'puma_auto_tune' 9 | 10 | PumaAutoTune.config do |config| 11 | config.ram = Integer(ENV['PUMA_RAM']) if ENV['PUMA_RAM'] 12 | config.frequency = Integer(ENV['PUMA_FREQUENCY']) if ENV['PUMA_FREQUENCY'] 13 | config.reap_duration = Integer(ENV['PUMA_REAP_DURATION']) if ENV['PUMA_REAP_DURATION'] 14 | end 15 | PumaAutoTune.start 16 | -------------------------------------------------------------------------------- /test/fixtures/config.rb: -------------------------------------------------------------------------------- 1 | threads Integer(ENV['MIN_THREADS'] || 1), Integer(ENV['MAX_THREADS'] || 16) 2 | workers Integer(ENV['PUMA_WORKERS'] || 3) 3 | 4 | port ENV['PORT'] || 0 # using 0 tells the OS to grab first open port 5 | environment ENV['RACK_ENV'] || 'development' 6 | preload_app! 7 | 8 | Thread.abort_on_exception = true 9 | 10 | on_worker_boot do 11 | end 12 | -------------------------------------------------------------------------------- /test/puma_auto_tune_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PumaAutoTuneTest < Test::Unit::TestCase 4 | 5 | def teardown 6 | @puma.cleanup if @puma 7 | end 8 | 9 | def test_starts 10 | @puma = PumaRemote.new.spawn 11 | @puma.wait 12 | 13 | assert @puma.wait %r{PumaAutoTune} 14 | end 15 | 16 | def test_cannot_drop_below_one 17 | @puma = PumaRemote.new(ram: 1, puma_workers: 1).spawn 18 | @puma.wait 19 | 20 | assert @puma.wait %r{cannot have less than one worker} 21 | end 22 | 23 | def test_reap_workers 24 | @puma = PumaRemote.new(ram: 1, puma_workers: 5).spawn 25 | @puma.wait 26 | 27 | assert @puma.wait %r{current_cluster_size=1} 28 | assert_match "max_worker_limit=3", @puma.log.read 29 | assert_match "max_worker_limit=2", @puma.log.read 30 | assert_match "max_worker_limit=1", @puma.log.read 31 | # refute 32 | refute_match "max_worker_limit=0", @puma.log.read 33 | end 34 | 35 | def test_increment_workers 36 | @puma = PumaRemote.new(puma_workers: 1, frequency: 1, reap_duration: 1).spawn 37 | assert @puma.wait %r{current_cluster_size=3} 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | Bundler.require 2 | 3 | require 'test/unit' 4 | 5 | class PumaRemote 6 | 7 | attr_accessor :path, :frequency, :reap_duration, :config, :log, :ram, :pid, :puma_workers 8 | 9 | def initialize(options = {}) 10 | @path = options[:path] || fixture_path("app.ru") 11 | @frequency = options[:frequency] || 1 12 | @reap_duration = options[:reap_duration] 13 | @config = options[:config] || fixture_path("config.rb") 14 | @log = options[:log] || new_log_file 15 | @ram = options[:ram] || 512 16 | @puma_workers = options[:puma_workers] || 3 17 | end 18 | 19 | def wait(regex = %r{booted}, timeout = 30) 20 | Timeout::timeout(timeout) do 21 | until log.read.match regex 22 | sleep 1 23 | end 24 | end 25 | sleep 1 26 | self 27 | rescue Timeout::Error 28 | puts "Timeout waiting for #{regex.inspect} in \n#{log.read}" 29 | false 30 | end 31 | 32 | def cleanup 33 | shutdown 34 | FileUtils.remove_entry_secure log 35 | end 36 | 37 | def shutdown 38 | if pid 39 | Process.kill('TERM', pid) 40 | Process.wait(pid) 41 | end 42 | rescue Errno::ESRCH 43 | end 44 | 45 | def spawn 46 | FileUtils.mkdir_p(log.dirname) 47 | FileUtils.touch(log) 48 | env = {} 49 | env["PUMA_WORKERS"] = puma_workers 50 | env["PUMA_FREQUENCY"] = frequency 51 | env["PUMA_RAM"] = ram 52 | env["PUMA_REAP_DURATION"] = reap_duration 53 | env_string = env.map {|key, value| "#{key}=#{value}" if value }.join(" ") 54 | 55 | @pid = Process.spawn("exec env #{env_string} bundle exec puma #{path} -C #{config} > #{log}") 56 | self 57 | end 58 | 59 | def new_log_file 60 | Pathname.new("test/logs/puma_#{rand(1...2000)}_#{Time.now.to_f}.log") 61 | end 62 | 63 | def fixture_path(name = nil) 64 | path = Pathname.new(File.expand_path("../fixtures", __FILE__)) 65 | return path.join(name) if name 66 | path 67 | end 68 | end 69 | --------------------------------------------------------------------------------