puma 4.3.6 → 5.3.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1346 -518
  3. data/LICENSE +23 -20
  4. data/README.md +74 -31
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +24 -20
  7. data/docs/compile_options.md +19 -0
  8. data/docs/deployment.md +15 -10
  9. data/docs/fork_worker.md +33 -0
  10. data/docs/jungle/README.md +9 -0
  11. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  12. data/{tools → docs}/jungle/rc.d/puma +2 -2
  13. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  14. data/docs/kubernetes.md +66 -0
  15. data/docs/nginx.md +1 -1
  16. data/docs/plugins.md +2 -2
  17. data/docs/rails_dev_mode.md +29 -0
  18. data/docs/restart.md +46 -23
  19. data/docs/signals.md +7 -6
  20. data/docs/stats.md +142 -0
  21. data/docs/systemd.md +27 -67
  22. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  23. data/ext/puma_http11/ext_help.h +1 -1
  24. data/ext/puma_http11/extconf.rb +22 -8
  25. data/ext/puma_http11/http11_parser.c +45 -47
  26. data/ext/puma_http11/http11_parser.h +1 -1
  27. data/ext/puma_http11/http11_parser.java.rl +1 -1
  28. data/ext/puma_http11/http11_parser.rl +1 -1
  29. data/ext/puma_http11/mini_ssl.c +211 -118
  30. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  31. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  32. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +5 -7
  33. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  34. data/ext/puma_http11/puma_http11.c +31 -50
  35. data/lib/puma.rb +46 -0
  36. data/lib/puma/app/status.rb +47 -36
  37. data/lib/puma/binder.rb +177 -103
  38. data/lib/puma/cli.rb +11 -15
  39. data/lib/puma/client.rb +73 -74
  40. data/lib/puma/cluster.rb +184 -198
  41. data/lib/puma/cluster/worker.rb +183 -0
  42. data/lib/puma/cluster/worker_handle.rb +90 -0
  43. data/lib/puma/commonlogger.rb +2 -2
  44. data/lib/puma/configuration.rb +55 -49
  45. data/lib/puma/const.rb +13 -5
  46. data/lib/puma/control_cli.rb +93 -76
  47. data/lib/puma/detect.rb +24 -3
  48. data/lib/puma/dsl.rb +266 -92
  49. data/lib/puma/error_logger.rb +104 -0
  50. data/lib/puma/events.rb +55 -34
  51. data/lib/puma/io_buffer.rb +9 -2
  52. data/lib/puma/jruby_restart.rb +0 -58
  53. data/lib/puma/json.rb +96 -0
  54. data/lib/puma/launcher.rb +113 -45
  55. data/lib/puma/minissl.rb +114 -33
  56. data/lib/puma/minissl/context_builder.rb +6 -3
  57. data/lib/puma/null_io.rb +13 -1
  58. data/lib/puma/plugin.rb +1 -10
  59. data/lib/puma/queue_close.rb +26 -0
  60. data/lib/puma/rack/builder.rb +0 -4
  61. data/lib/puma/reactor.rb +85 -369
  62. data/lib/puma/request.rb +467 -0
  63. data/lib/puma/runner.rb +29 -58
  64. data/lib/puma/server.rb +267 -729
  65. data/lib/puma/single.rb +9 -65
  66. data/lib/puma/state_file.rb +8 -3
  67. data/lib/puma/systemd.rb +46 -0
  68. data/lib/puma/thread_pool.rb +119 -53
  69. data/lib/puma/util.rb +12 -0
  70. data/lib/rack/handler/puma.rb +2 -3
  71. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  72. metadata +25 -21
  73. data/docs/tcp_mode.md +0 -96
  74. data/ext/puma_http11/io_buffer.c +0 -155
  75. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  76. data/lib/puma/accept_nonblock.rb +0 -29
  77. data/lib/puma/tcp_logger.rb +0 -41
  78. data/tools/jungle/README.md +0 -19
  79. data/tools/jungle/init.d/README.md +0 -61
  80. data/tools/jungle/init.d/puma +0 -421
  81. data/tools/jungle/init.d/run-puma +0 -18
  82. data/tools/jungle/upstart/README.md +0 -61
  83. data/tools/jungle/upstart/puma-manager.conf +0 -31
  84. data/tools/jungle/upstart/puma.conf +0 -69
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ class Cluster < Puma::Runner
5
+ # This class is instantiated by the `Puma::Cluster` and represents a single
6
+ # worker process.
7
+ #
8
+ # At the core of this class is running an instance of `Puma::Server` which
9
+ # gets created via the `start_server` method from the `Puma::Runner` class
10
+ # that this inherits from.
11
+ class Worker < Puma::Runner
12
+ attr_reader :index, :master
13
+
14
+ def initialize(index:, master:, launcher:, pipes:, server: nil)
15
+ super launcher, launcher.events
16
+
17
+ @index = index
18
+ @master = master
19
+ @launcher = launcher
20
+ @options = launcher.options
21
+ @check_pipe = pipes[:check_pipe]
22
+ @worker_write = pipes[:worker_write]
23
+ @fork_pipe = pipes[:fork_pipe]
24
+ @wakeup = pipes[:wakeup]
25
+ @server = server
26
+ end
27
+
28
+ def run
29
+ title = "puma: cluster worker #{index}: #{master}"
30
+ title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
31
+ $0 = title
32
+
33
+ Signal.trap "SIGINT", "IGNORE"
34
+ Signal.trap "SIGCHLD", "DEFAULT"
35
+
36
+ Thread.new do
37
+ Puma.set_thread_name "worker check pipe"
38
+ IO.select [@check_pipe]
39
+ log "! Detected parent died, dying"
40
+ exit! 1
41
+ end
42
+
43
+ # If we're not running under a Bundler context, then
44
+ # report the info about the context we will be using
45
+ if !ENV['BUNDLE_GEMFILE']
46
+ if File.exist?("Gemfile")
47
+ log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
48
+ elsif File.exist?("gems.rb")
49
+ log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
50
+ end
51
+ end
52
+
53
+ # Invoke any worker boot hooks so they can get
54
+ # things in shape before booting the app.
55
+ @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
56
+
57
+ begin
58
+ server = @server ||= start_server
59
+ rescue Exception => e
60
+ log "! Unable to start worker"
61
+ log e.backtrace[0]
62
+ exit 1
63
+ end
64
+
65
+ restart_server = Queue.new << true << false
66
+
67
+ fork_worker = @options[:fork_worker] && index == 0
68
+
69
+ if fork_worker
70
+ restart_server.clear
71
+ worker_pids = []
72
+ Signal.trap "SIGCHLD" do
73
+ wakeup! if worker_pids.reject! do |p|
74
+ Process.wait(p, Process::WNOHANG) rescue true
75
+ end
76
+ end
77
+
78
+ Thread.new do
79
+ Puma.set_thread_name "worker fork pipe"
80
+ while (idx = @fork_pipe.gets)
81
+ idx = idx.to_i
82
+ if idx == -1 # stop server
83
+ if restart_server.length > 0
84
+ restart_server.clear
85
+ server.begin_restart(true)
86
+ @launcher.config.run_hooks :before_refork, nil, @launcher.events
87
+ Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
88
+ end
89
+ elsif idx == 0 # restart server
90
+ restart_server << true << false
91
+ else # fork worker
92
+ worker_pids << pid = spawn_worker(idx)
93
+ @worker_write << "f#{pid}:#{idx}\n" rescue nil
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ Signal.trap "SIGTERM" do
100
+ @worker_write << "e#{Process.pid}\n" rescue nil
101
+ restart_server.clear
102
+ server.stop
103
+ restart_server << false
104
+ end
105
+
106
+ begin
107
+ @worker_write << "b#{Process.pid}:#{index}\n"
108
+ rescue SystemCallError, IOError
109
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
110
+ STDERR.puts "Master seems to have exited, exiting."
111
+ return
112
+ end
113
+
114
+ while restart_server.pop
115
+ server_thread = server.run
116
+ stat_thread ||= Thread.new(@worker_write) do |io|
117
+ Puma.set_thread_name "stat payload"
118
+ base_payload = "p#{Process.pid}"
119
+
120
+ while true
121
+ begin
122
+ b = server.backlog || 0
123
+ r = server.running || 0
124
+ t = server.pool_capacity || 0
125
+ m = server.max_threads || 0
126
+ rc = server.requests_count || 0
127
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
128
+ io << payload
129
+ rescue IOError
130
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
131
+ break
132
+ end
133
+ sleep Const::WORKER_CHECK_INTERVAL
134
+ end
135
+ end
136
+ server_thread.join
137
+ end
138
+
139
+ # Invoke any worker shutdown hooks so they can prevent the worker
140
+ # exiting until any background operations are completed
141
+ @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
142
+ ensure
143
+ @worker_write << "t#{Process.pid}\n" rescue nil
144
+ @worker_write.close
145
+ end
146
+
147
+ private
148
+
149
+ def spawn_worker(idx)
150
+ @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
151
+
152
+ pid = fork do
153
+ new_worker = Worker.new index: idx,
154
+ master: master,
155
+ launcher: @launcher,
156
+ pipes: { check_pipe: @check_pipe,
157
+ worker_write: @worker_write },
158
+ server: @server
159
+ new_worker.run
160
+ end
161
+
162
+ if !pid
163
+ log "! Complete inability to spawn new workers detected"
164
+ log "! Seppuku is the only choice."
165
+ exit! 1
166
+ end
167
+
168
+ @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
169
+ pid
170
+ end
171
+
172
+ def wakeup!
173
+ return unless @wakeup
174
+
175
+ begin
176
+ @wakeup.write "!" unless @wakeup.closed?
177
+ rescue SystemCallError, IOError
178
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ class Cluster < Runner
5
+ # This class represents a worker process from the perspective of the puma
6
+ # master process. It contains information about the process and its health
7
+ # and it exposes methods to control the process via IPC. It does not
8
+ # include the actual logic executed by the worker process itself. For that,
9
+ # see Puma::Cluster::Worker.
10
+ class WorkerHandle
11
+ def initialize(idx, pid, phase, options)
12
+ @index = idx
13
+ @pid = pid
14
+ @phase = phase
15
+ @stage = :started
16
+ @signal = "TERM"
17
+ @options = options
18
+ @first_term_sent = nil
19
+ @started_at = Time.now
20
+ @last_checkin = Time.now
21
+ @last_status = {}
22
+ @term = false
23
+ end
24
+
25
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
26
+
27
+ # @version 5.0.0
28
+ attr_writer :pid, :phase
29
+
30
+ def booted?
31
+ @stage == :booted
32
+ end
33
+
34
+ def uptime
35
+ Time.now - started_at
36
+ end
37
+
38
+ def boot!
39
+ @last_checkin = Time.now
40
+ @stage = :booted
41
+ end
42
+
43
+ def term?
44
+ @term
45
+ end
46
+
47
+ def ping!(status)
48
+ @last_checkin = Time.now
49
+ captures = status.match(/{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads": (?<max_threads>\d*), "requests_count": (?<requests_count>\d*) }/)
50
+ @last_status = captures.names.inject({}) do |hash, key|
51
+ hash[key.to_sym] = captures[key].to_i
52
+ hash
53
+ end
54
+ end
55
+
56
+ # @see Puma::Cluster#check_workers
57
+ # @version 5.0.0
58
+ def ping_timeout
59
+ @last_checkin +
60
+ (booted? ?
61
+ @options[:worker_timeout] :
62
+ @options[:worker_boot_timeout]
63
+ )
64
+ end
65
+
66
+ def term
67
+ begin
68
+ if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
69
+ @signal = "KILL"
70
+ else
71
+ @term ||= true
72
+ @first_term_sent ||= Time.now
73
+ end
74
+ Process.kill @signal, @pid if @pid
75
+ rescue Errno::ESRCH
76
+ end
77
+ end
78
+
79
+ def kill
80
+ @signal = 'KILL'
81
+ term
82
+ end
83
+
84
+ def hup
85
+ Process.kill "HUP", @pid
86
+ rescue Errno::ESRCH
87
+ end
88
+ end
89
+ end
90
+ end
@@ -3,7 +3,7 @@
3
3
  module Puma
4
4
  # Rack::CommonLogger forwards every request to the given +app+, and
5
5
  # logs a line in the
6
- # {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
6
+ # {Apache common log format}[https://httpd.apache.org/docs/1.3/logs.html#common]
7
7
  # to the +logger+.
8
8
  #
9
9
  # If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is
@@ -16,7 +16,7 @@ module Puma
16
16
  # (which is called without arguments in order to make the error appear for
17
17
  # sure)
18
18
  class CommonLogger
19
- # Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
19
+ # Common Log Format: https://httpd.apache.org/docs/1.3/logs.html#common
20
20
  #
21
21
  # lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
22
22
  #
@@ -54,9 +54,7 @@ module Puma
54
54
  attr_reader :user_options, :file_options, :default_options
55
55
 
56
56
  def [](key)
57
- return user_options[key] if user_options.key?(key)
58
- return file_options[key] if file_options.key?(key)
59
- return default_options[key] if default_options.key?(key)
57
+ fetch(key)
60
58
  end
61
59
 
62
60
  def []=(key, value)
@@ -64,7 +62,11 @@ module Puma
64
62
  end
65
63
 
66
64
  def fetch(key, default_value = nil)
67
- self[key] || default_value
65
+ return user_options[key] if user_options.key?(key)
66
+ return file_options[key] if file_options.key?(key)
67
+ return default_options[key] if default_options.key?(key)
68
+
69
+ default_value
68
70
  end
69
71
 
70
72
  def all_of(key)
@@ -90,6 +92,12 @@ module Puma
90
92
  end
91
93
  end
92
94
  end
95
+
96
+ def final_options
97
+ default_options
98
+ .merge(file_options)
99
+ .merge(user_options)
100
+ end
93
101
  end
94
102
 
95
103
  # The main configuration class of Puma.
@@ -106,16 +114,17 @@ module Puma
106
114
  #
107
115
  # It also handles loading plugins.
108
116
  #
109
- # > Note: `:port` and `:host` are not valid keys. By they time they make it to the
117
+ # [Note:]
118
+ # `:port` and `:host` are not valid keys. By the time they make it to the
110
119
  # configuration options they are expected to be incorporated into a `:binds` key.
111
120
  # Under the hood the DSL maps `port` and `host` calls to `:binds`
112
121
  #
113
- # config = Configuration.new({}) do |user_config, file_config, default_config|
114
- # user_config.port 3003
115
- # end
116
- # config.load
117
- # puts config.options[:port]
118
- # # => 3003
122
+ # config = Configuration.new({}) do |user_config, file_config, default_config|
123
+ # user_config.port 3003
124
+ # end
125
+ # config.load
126
+ # puts config.options[:port]
127
+ # # => 3003
119
128
  #
120
129
  # It is expected that `load` is called on the configuration instance after setting
121
130
  # config. This method expands any values in `config_file` and puts them into the
@@ -137,6 +146,10 @@ module Puma
137
146
  @file_dsl = DSL.new(@options.file_options, self)
138
147
  @default_dsl = DSL.new(@options.default_options, self)
139
148
 
149
+ if !@options[:prune_bundler]
150
+ default_options[:preload_app] = (@options[:workers] > 1) && Puma.forkable?
151
+ end
152
+
140
153
  if block
141
154
  configure(&block)
142
155
  end
@@ -167,27 +180,35 @@ module Puma
167
180
  self
168
181
  end
169
182
 
183
+ # @version 5.0.0
184
+ def default_max_threads
185
+ Puma.mri? ? 5 : 16
186
+ end
187
+
170
188
  def puma_default_options
171
189
  {
172
- :min_threads => 0,
173
- :max_threads => 16,
190
+ :min_threads => Integer(ENV['PUMA_MIN_THREADS'] || ENV['MIN_THREADS'] || 0),
191
+ :max_threads => Integer(ENV['PUMA_MAX_THREADS'] || ENV['MAX_THREADS'] || default_max_threads),
174
192
  :log_requests => false,
175
193
  :debug => false,
176
194
  :binds => ["tcp://#{DefaultTCPHost}:#{DefaultTCPPort}"],
177
- :workers => 0,
178
- :daemon => false,
195
+ :workers => Integer(ENV['WEB_CONCURRENCY'] || 0),
196
+ :silence_single_worker_warning => false,
179
197
  :mode => :http,
180
198
  :worker_timeout => DefaultWorkerTimeout,
181
199
  :worker_boot_timeout => DefaultWorkerTimeout,
182
200
  :worker_shutdown_timeout => DefaultWorkerShutdownTimeout,
183
201
  :remote_address => :socket,
184
202
  :tag => method(:infer_tag),
185
- :environment => -> { ENV['RACK_ENV'] || "development" },
203
+ :environment => -> { ENV['RACK_ENV'] || ENV['RAILS_ENV'] || "development" },
186
204
  :rackup => DefaultRackup,
187
205
  :logger => STDOUT,
188
206
  :persistent_timeout => Const::PERSISTENT_TIMEOUT,
189
207
  :first_data_timeout => Const::FIRST_DATA_TIMEOUT,
190
- :raise_exception_on_sigterm => true
208
+ :raise_exception_on_sigterm => true,
209
+ :max_fast_inline => Const::MAX_FAST_INLINE,
210
+ :io_selector_backend => :auto,
211
+ :mutate_stdout_and_stderr_to_sync_on_write => true,
191
212
  }
192
213
  end
193
214
 
@@ -245,14 +266,6 @@ module Puma
245
266
  def app
246
267
  found = options[:app] || load_rackup
247
268
 
248
- if @options[:mode] == :tcp
249
- require 'puma/tcp_logger'
250
-
251
- logger = @options[:logger]
252
- quiet = !@options[:log_requests]
253
- return TCPLogger.new(logger, found, quiet)
254
- end
255
-
256
269
  if @options[:log_requests]
257
270
  require 'puma/commonlogger'
258
271
  logger = @options[:logger]
@@ -275,8 +288,19 @@ module Puma
275
288
  @plugins.create name
276
289
  end
277
290
 
278
- def run_hooks(key, arg)
279
- @options.all_of(key).each { |b| b.call arg }
291
+ def run_hooks(key, arg, events)
292
+ @options.all_of(key).each do |b|
293
+ begin
294
+ b.call arg
295
+ rescue => e
296
+ events.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
297
+ events.debug e.backtrace.join("\n")
298
+ end
299
+ end
300
+ end
301
+
302
+ def final_options
303
+ @options.final_options
280
304
  end
281
305
 
282
306
  def self.temp_path
@@ -319,6 +343,8 @@ module Puma
319
343
  raise "Missing rackup file '#{rackup}'" unless File.exist?(rackup)
320
344
 
321
345
  rack_app, rack_options = rack_builder.parse_file(rackup)
346
+ rack_options = rack_options || {}
347
+
322
348
  @options.file_options.merge!(rack_options)
323
349
 
324
350
  config_ru_binds = []
@@ -332,29 +358,9 @@ module Puma
332
358
  end
333
359
 
334
360
  def self.random_token
335
- begin
336
- require 'openssl'
337
- rescue LoadError
338
- end
339
-
340
- count = 16
341
-
342
- bytes = nil
343
-
344
- if defined? OpenSSL::Random
345
- bytes = OpenSSL::Random.random_bytes(count)
346
- elsif File.exist?("/dev/urandom")
347
- File.open('/dev/urandom') { |f| bytes = f.read(count) }
348
- end
349
-
350
- if bytes
351
- token = "".dup
352
- bytes.each_byte { |b| token << b.to_s(16) }
353
- else
354
- token = (0..count).to_a.map { rand(255).to_s(16) }.join
355
- end
361
+ require 'securerandom' unless defined?(SecureRandom)
356
362
 
357
- return token
363
+ SecureRandom.hex(16)
358
364
  end
359
365
  end
360
366
  end