puma 3.9.0 → 3.12.0

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 (53) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +98 -0
  3. data/README.md +140 -230
  4. data/docs/architecture.md +36 -0
  5. data/{DEPLOYMENT.md → docs/deployment.md} +0 -0
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +28 -0
  10. data/docs/restart.md +39 -0
  11. data/docs/signals.md +56 -3
  12. data/docs/systemd.md +112 -37
  13. data/ext/puma_http11/http11_parser.c +84 -84
  14. data/ext/puma_http11/http11_parser.rl +9 -9
  15. data/ext/puma_http11/mini_ssl.c +18 -4
  16. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +13 -16
  17. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +6 -0
  18. data/lib/puma.rb +8 -0
  19. data/lib/puma/app/status.rb +8 -0
  20. data/lib/puma/binder.rb +12 -8
  21. data/lib/puma/cli.rb +20 -7
  22. data/lib/puma/client.rb +28 -0
  23. data/lib/puma/cluster.rb +26 -7
  24. data/lib/puma/configuration.rb +19 -14
  25. data/lib/puma/const.rb +7 -2
  26. data/lib/puma/control_cli.rb +5 -5
  27. data/lib/puma/dsl.rb +34 -7
  28. data/lib/puma/jruby_restart.rb +0 -1
  29. data/lib/puma/launcher.rb +36 -19
  30. data/lib/puma/minissl.rb +49 -27
  31. data/lib/puma/plugin/tmp_restart.rb +0 -1
  32. data/lib/puma/reactor.rb +135 -0
  33. data/lib/puma/runner.rb +12 -1
  34. data/lib/puma/server.rb +84 -25
  35. data/lib/puma/single.rb +12 -3
  36. data/lib/puma/thread_pool.rb +47 -8
  37. data/lib/rack/handler/puma.rb +4 -1
  38. data/tools/jungle/README.md +12 -2
  39. data/tools/jungle/init.d/README.md +2 -0
  40. data/tools/jungle/init.d/puma +2 -2
  41. data/tools/jungle/init.d/run-puma +1 -1
  42. data/tools/jungle/rc.d/README.md +74 -0
  43. data/tools/jungle/rc.d/puma +61 -0
  44. data/tools/jungle/rc.d/puma.conf +10 -0
  45. data/tools/trickletest.rb +1 -1
  46. metadata +21 -94
  47. data/.github/issue_template.md +0 -20
  48. data/Gemfile +0 -12
  49. data/Manifest.txt +0 -78
  50. data/Rakefile +0 -158
  51. data/Release.md +0 -9
  52. data/gemfiles/2.1-Gemfile +0 -12
  53. data/puma.gemspec +0 -52
@@ -5,6 +5,17 @@ require 'puma/plugin'
5
5
  require 'time'
6
6
 
7
7
  module Puma
8
+ # This class is instantiated by the `Puma::Launcher` and used
9
+ # to boot and serve a Ruby application when puma "workers" are needed
10
+ # i.e. when using multi-processes. For example `$ puma -w 5`
11
+ #
12
+ # At the core of this class is running an instance of `Puma::Server` which
13
+ # gets created via the `start_server` method from the `Puma::Runner` class
14
+ # that this inherits from.
15
+ #
16
+ # An instance of this class will spawn the number of processes passed in
17
+ # via the `spawn_workers` method call. Each worker will have it's own
18
+ # instance of a `Puma::Server`.
8
19
  class Cluster < Runner
9
20
  WORKER_CHECK_INTERVAL = 5
10
21
 
@@ -24,7 +35,7 @@ module Puma
24
35
  @workers.each { |x| x.term }
25
36
 
26
37
  begin
27
- Process.waitall
38
+ @workers.each { |w| Process.waitpid(w.pid) }
28
39
  rescue Interrupt
29
40
  log "! Cancelled waiting for workers"
30
41
  end
@@ -224,12 +235,13 @@ module Puma
224
235
  begin
225
236
  @wakeup.write "!" unless @wakeup.closed?
226
237
  rescue SystemCallError, IOError
238
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
227
239
  end
228
240
  end
229
241
 
230
242
  def worker(index, master)
231
- title = "puma: cluster worker #{index}: #{master}"
232
- title << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
243
+ title = "puma: cluster worker #{index}: #{master}"
244
+ title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
233
245
  $0 = title
234
246
 
235
247
  Signal.trap "SIGINT", "IGNORE"
@@ -267,6 +279,7 @@ module Puma
267
279
  begin
268
280
  @worker_write << "b#{Process.pid}\n"
269
281
  rescue SystemCallError, IOError
282
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
270
283
  STDERR.puts "Master seems to have exited, exiting."
271
284
  return
272
285
  end
@@ -277,11 +290,14 @@ module Puma
277
290
  while true
278
291
  sleep WORKER_CHECK_INTERVAL
279
292
  begin
280
- b = server.backlog
281
- r = server.running
282
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r} }\n!
293
+ b = server.backlog || 0
294
+ r = server.running || 0
295
+ t = server.pool_capacity || 0
296
+ m = server.max_threads || 0
297
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m} }\n!
283
298
  io << payload
284
299
  rescue IOError
300
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
285
301
  break
286
302
  end
287
303
  end
@@ -337,7 +353,7 @@ module Puma
337
353
  def stats
338
354
  old_worker_count = @workers.count { |w| w.phase != @phase }
339
355
  booted_worker_count = @workers.count { |w| w.booted? }
340
- worker_status = '[' + @workers.map{ |w| %Q!{ "pid": #{w.pid}, "index": #{w.index}, "phase": #{w.phase}, "booted": #{w.booted?}, "last_checkin": "#{w.last_checkin.utc.iso8601}", "last_status": #{w.last_status} }!}.join(",") + ']'
356
+ worker_status = '[' + @workers.map { |w| %Q!{ "pid": #{w.pid}, "index": #{w.index}, "phase": #{w.phase}, "booted": #{w.booted?}, "last_checkin": "#{w.last_checkin.utc.iso8601}", "last_status": #{w.last_status} }!}.join(",") + ']'
341
357
  %Q!{ "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
342
358
  end
343
359
 
@@ -372,7 +388,10 @@ module Puma
372
388
  log "Early termination of worker"
373
389
  exit! 0
374
390
  else
391
+ stop_workers
375
392
  stop
393
+
394
+ raise SignalException, "SIGTERM"
376
395
  end
377
396
  end
378
397
  end
@@ -180,30 +180,31 @@ module Puma
180
180
  :worker_shutdown_timeout => DefaultWorkerShutdownTimeout,
181
181
  :remote_address => :socket,
182
182
  :tag => method(:infer_tag),
183
- :environment => ->{ ENV['RACK_ENV'] || "development" },
183
+ :environment => -> { ENV['RACK_ENV'] || "development" },
184
184
  :rackup => DefaultRackup,
185
185
  :logger => STDOUT,
186
- :persistent_timeout => Const::PERSISTENT_TIMEOUT
186
+ :persistent_timeout => Const::PERSISTENT_TIMEOUT,
187
+ :first_data_timeout => Const::FIRST_DATA_TIMEOUT
187
188
  }
188
189
  end
189
190
 
190
191
  def load
192
+ config_files.each { |config_file| @file_dsl._load_from(config_file) }
193
+
194
+ @options
195
+ end
196
+
197
+ def config_files
191
198
  files = @options.all_of(:config_files)
192
199
 
193
- if files.empty?
194
- imp = %W(config/puma/#{@options[:environment]}.rb config/puma.rb).find { |f|
195
- File.exist?(f)
196
- }
200
+ return [] if files == ['-']
201
+ return files if files.any?
197
202
 
198
- files << imp
199
- elsif files == ["-"]
200
- files = []
203
+ first_default_file = %W(config/puma/#{environment_str}.rb config/puma.rb).find do |f|
204
+ File.exist?(f)
201
205
  end
202
206
 
203
- files.each do |f|
204
- @file_dsl._load_from(f)
205
- end
206
- @options
207
+ [first_default_file]
207
208
  end
208
209
 
209
210
  # Call once all configuration (included from rackup files)
@@ -263,6 +264,10 @@ module Puma
263
264
  @options[:environment]
264
265
  end
265
266
 
267
+ def environment_str
268
+ environment.respond_to?(:call) ? environment.call : environment
269
+ end
270
+
266
271
  def load_plugin(name)
267
272
  @plugins.create name
268
273
  end
@@ -340,7 +345,7 @@ module Puma
340
345
  end
341
346
 
342
347
  if bytes
343
- token = ""
348
+ token = "".dup
344
349
  bytes.each_byte { |b| token << b.to_s(16) }
345
350
  else
346
351
  token = (0..count).to_a.map { rand(255).to_s(16) }.join
@@ -53,6 +53,8 @@ module Puma
53
53
  415 => 'Unsupported Media Type',
54
54
  416 => 'Range Not Satisfiable',
55
55
  417 => 'Expectation Failed',
56
+ 418 => 'I\'m A Teapot',
57
+ 421 => 'Misdirected Request',
56
58
  422 => 'Unprocessable Entity',
57
59
  423 => 'Locked',
58
60
  424 => 'Failed Dependency',
@@ -60,6 +62,7 @@ module Puma
60
62
  428 => 'Precondition Required',
61
63
  429 => 'Too Many Requests',
62
64
  431 => 'Request Header Fields Too Large',
65
+ 451 => 'Unavailable For Legal Reasons',
63
66
  500 => 'Internal Server Error',
64
67
  501 => 'Not Implemented',
65
68
  502 => 'Bad Gateway',
@@ -95,8 +98,8 @@ module Puma
95
98
  # too taxing on performance.
96
99
  module Const
97
100
 
98
- PUMA_VERSION = VERSION = "3.9.0".freeze
99
- CODE_NAME = "Private Caller".freeze
101
+ PUMA_VERSION = VERSION = "3.12.0".freeze
102
+ CODE_NAME = "Llamas in Pajamas".freeze
100
103
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
101
104
 
102
105
  FAST_TRACK_KA_TIMEOUT = 0.2
@@ -220,5 +223,7 @@ module Puma
220
223
  HIJACK_P = "rack.hijack?".freeze
221
224
  HIJACK = "rack.hijack".freeze
222
225
  HIJACK_IO = "rack.hijack_io".freeze
226
+
227
+ EARLY_HINTS = "rack.early_hints".freeze
223
228
  end
224
229
  end
@@ -9,7 +9,7 @@ require 'socket'
9
9
  module Puma
10
10
  class ControlCLI
11
11
 
12
- COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory}
12
+ COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats}
13
13
 
14
14
  def initialize(argv, stdout=STDOUT, stderr=STDERR)
15
15
  @state = nil
@@ -69,6 +69,7 @@ module Puma
69
69
  end
70
70
 
71
71
  opts.order!(argv) { |a| opts.terminate a }
72
+ opts.parse!
72
73
 
73
74
  @command = argv.shift
74
75
 
@@ -169,7 +170,7 @@ module Puma
169
170
  end
170
171
 
171
172
  message "Command #{@command} sent success"
172
- message response.last if @command == "stats"
173
+ message response.last if @command == "stats" || @command == "gc-stats"
173
174
  end
174
175
 
175
176
  @server.close
@@ -204,7 +205,6 @@ module Puma
204
205
  Process.kill "SIGUSR1", @pid
205
206
 
206
207
  else
207
- message "Puma is started"
208
208
  return
209
209
  end
210
210
 
@@ -220,7 +220,7 @@ module Puma
220
220
  end
221
221
 
222
222
  def run
223
- start if @command == "start"
223
+ return start if @command == "start"
224
224
 
225
225
  prepare_configuration
226
226
 
@@ -245,7 +245,7 @@ module Puma
245
245
  run_args += ["-S", @state] if @state
246
246
  run_args += ["-q"] if @quiet
247
247
  run_args += ["--pidfile", @pidfile] if @pidfile
248
- run_args += ["--control", @control_url] if @control_url
248
+ run_args += ["--control-url", @control_url] if @control_url
249
249
  run_args += ["--control-token", @control_auth_token] if @control_auth_token
250
250
  run_args += ["-C", @config_file] if @config_file
251
251
 
@@ -110,14 +110,31 @@ module Puma
110
110
  @options[:config_files] << file
111
111
  end
112
112
 
113
- # Bind the server to +url+. tcp:// and unix:// are the only accepted
114
- # protocols.
113
+ # Adds a binding for the server to +url+. tcp://, unix://, and ssl:// are the only accepted
114
+ # protocols. Use query parameters within the url to specify options.
115
115
  #
116
+ # @note multiple urls can be bound to, calling `bind` does not overwrite previous bindings.
117
+ #
118
+ # @example Explicitly the socket backlog depth (default is 1024)
119
+ # bind('unix:///var/run/puma.sock?backlog=2048')
120
+ #
121
+ # @example Set up ssl cert
122
+ # bind('ssl://127.0.0.1:9292?key=key.key&cert=cert.pem')
123
+ #
124
+ # @example Prefer low-latency over higher throughput (via `Socket::TCP_NODELAY`)
125
+ # bind('tcp://0.0.0.0:9292?low_latency=true')
126
+ #
127
+ # @example Set socket permissions
128
+ # bind('unix:///var/run/puma.sock?umask=0111')
116
129
  def bind(url)
117
130
  @options[:binds] ||= []
118
131
  @options[:binds] << url
119
132
  end
120
133
 
134
+ def clear_binds!
135
+ @options[:binds] = []
136
+ end
137
+
121
138
  # Define the TCP port to bind to. Use +bind+ for more advanced options.
122
139
  #
123
140
  def port(port, host=nil)
@@ -129,7 +146,13 @@ module Puma
129
146
  # them
130
147
  #
131
148
  def persistent_timeout(seconds)
132
- @options[:persistent_timeout] = seconds
149
+ @options[:persistent_timeout] = Integer(seconds)
150
+ end
151
+
152
+ # Define how long the tcp socket stays open, if no data has been received
153
+ #
154
+ def first_data_timeout(seconds)
155
+ @options[:first_data_timeout] = Integer(seconds)
133
156
  end
134
157
 
135
158
  # Work around leaky apps that leave garbage in Thread locals
@@ -146,7 +169,7 @@ module Puma
146
169
  end
147
170
 
148
171
  # When shutting down, drain the accept socket of pending
149
- # connections and proces them. This loops over the accept
172
+ # connections and process them. This loops over the accept
150
173
  # socket until there are no more read events and then stops
151
174
  # looking and waits for the requests to finish.
152
175
  def drain_on_shutdown(which=true)
@@ -231,6 +254,10 @@ module Puma
231
254
  @options[:mode] = :tcp
232
255
  end
233
256
 
257
+ def early_hints(answer=true)
258
+ @options[:early_hints] = answer
259
+ end
260
+
234
261
  # Redirect STDOUT and STDERR to files specified.
235
262
  def stdout_redirect(stdout=nil, stderr=nil, append=false)
236
263
  @options[:redirect_stdout] = stdout
@@ -397,17 +424,17 @@ module Puma
397
424
  # that have not checked in within the given +timeout+.
398
425
  # This mitigates hung processes. Default value is 60 seconds.
399
426
  def worker_timeout(timeout)
400
- @options[:worker_timeout] = timeout
427
+ @options[:worker_timeout] = Integer(timeout)
401
428
  end
402
429
 
403
430
  # *Cluster mode only* Set the timeout for workers to boot
404
431
  def worker_boot_timeout(timeout)
405
- @options[:worker_boot_timeout] = timeout
432
+ @options[:worker_boot_timeout] = Integer(timeout)
406
433
  end
407
434
 
408
435
  # *Cluster mode only* Set the timeout for worker shutdown
409
436
  def worker_shutdown_timeout(timeout)
410
- @options[:worker_shutdown_timeout] = timeout
437
+ @options[:worker_shutdown_timeout] = Integer(timeout)
411
438
  end
412
439
 
413
440
  # When set to true (the default), workers accept all requests
@@ -80,4 +80,3 @@ module Puma
80
80
  end
81
81
  end
82
82
  end
83
-
@@ -40,7 +40,7 @@ module Puma
40
40
  # [200, {}, ["hello world"]]
41
41
  # end
42
42
  # end
43
- # Puma::Launcher.new(conf, argv: Puma::Events.stdio).run
43
+ # Puma::Launcher.new(conf, events: Puma::Events.stdio).run
44
44
  def initialize(conf, launcher_args={})
45
45
  @runner = nil
46
46
  @events = launcher_args[:events] || Events::DEFAULT
@@ -86,6 +86,7 @@ module Puma
86
86
  else
87
87
  @runner = Single.new(self, @events)
88
88
  end
89
+ Puma.stats_object = @runner
89
90
 
90
91
  @status = :run
91
92
  end
@@ -163,7 +164,16 @@ module Puma
163
164
 
164
165
  # Run the server. This blocks until the server is stopped
165
166
  def run
166
- previous_env = (defined?(Bundler) ? Bundler.clean_env : ENV.to_h)
167
+ previous_env =
168
+ if defined?(Bundler)
169
+ env = Bundler::ORIGINAL_ENV.dup
170
+ # add -rbundler/setup so we load from Gemfile when restarting
171
+ bundle = "-rbundler/setup"
172
+ env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
173
+ env
174
+ else
175
+ ENV.to_h
176
+ end
167
177
 
168
178
  @config.clamp
169
179
 
@@ -225,8 +235,8 @@ module Puma
225
235
  else
226
236
  redirects = {:close_others => true}
227
237
  @binder.listeners.each_with_index do |(l, io), i|
228
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
229
- redirects[io.to_i] = io.to_i
238
+ ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
239
+ redirects[io.to_i] = io.to_i
230
240
  end
231
241
 
232
242
  argv = restart_args
@@ -289,8 +299,8 @@ module Puma
289
299
  end
290
300
 
291
301
  def title
292
- buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
293
- buffer << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
302
+ buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
303
+ buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
294
304
  buffer
295
305
  end
296
306
 
@@ -340,8 +350,6 @@ module Puma
340
350
 
341
351
  @restart_dir ||= Dir.pwd
342
352
 
343
- require 'rubygems'
344
-
345
353
  # if $0 is a file in the current directory, then restart
346
354
  # it the same, otherwise add -S on there because it was
347
355
  # picked up in PATH.
@@ -352,9 +360,10 @@ module Puma
352
360
  arg0 = [Gem.ruby, "-S", $0]
353
361
  end
354
362
 
355
- # Detect and reinject -Ilib from the command line
363
+ # Detect and reinject -Ilib from the command line, used for testing without bundler
364
+ # cruby has an expanded path, jruby has just "lib"
356
365
  lib = File.expand_path "lib"
357
- arg0[1,0] = ["-I", lib] if $:[0] == lib
366
+ arg0[1,0] = ["-I", lib] if [lib, "lib"].include?($LOAD_PATH[0])
358
367
 
359
368
  if defined? Puma::WILD_ARGS
360
369
  @restart_argv = arg0 + Puma::WILD_ARGS + @original_argv
@@ -384,12 +393,28 @@ module Puma
384
393
 
385
394
  begin
386
395
  Signal.trap "SIGTERM" do
387
- stop
396
+ graceful_stop
397
+
398
+ raise SignalException, "SIGTERM"
388
399
  end
389
400
  rescue Exception
390
401
  log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
391
402
  end
392
403
 
404
+ begin
405
+ Signal.trap "SIGINT" do
406
+ if Puma.jruby?
407
+ @status = :exit
408
+ graceful_stop
409
+ exit
410
+ end
411
+
412
+ stop
413
+ end
414
+ rescue Exception
415
+ log "*** SIGINT not implemented, signal based gracefully stopping unavailable!"
416
+ end
417
+
393
418
  begin
394
419
  Signal.trap "SIGHUP" do
395
420
  if @runner.redirected_io?
@@ -401,14 +426,6 @@ module Puma
401
426
  rescue Exception
402
427
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
403
428
  end
404
-
405
- if Puma.jruby?
406
- Signal.trap("INT") do
407
- @status = :exit
408
- graceful_stop
409
- exit
410
- end
411
- end
412
429
  end
413
430
  end
414
431
  end