puma 6.3.1 → 6.4.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/puma/cluster.rb CHANGED
@@ -85,9 +85,7 @@ module Puma
85
85
  @workers << WorkerHandle.new(idx, pid, @phase, @options)
86
86
  end
87
87
 
88
- if @options[:fork_worker] &&
89
- @workers.all? {|x| x.phase == @phase}
90
-
88
+ if @options[:fork_worker] && all_workers_in_phase?
91
89
  @fork_writer << "0\n"
92
90
  end
93
91
  end
@@ -148,10 +146,22 @@ module Puma
148
146
  idx
149
147
  end
150
148
 
149
+ def worker_at(idx)
150
+ @workers.find { |w| w.index == idx }
151
+ end
152
+
151
153
  def all_workers_booted?
152
154
  @workers.count { |w| !w.booted? } == 0
153
155
  end
154
156
 
157
+ def all_workers_in_phase?
158
+ @workers.all? { |w| w.phase == @phase }
159
+ end
160
+
161
+ def all_workers_idle_timed_out?
162
+ (@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
163
+ end
164
+
155
165
  def check_workers
156
166
  return if @next_check >= Time.now
157
167
 
@@ -276,7 +286,7 @@ module Puma
276
286
 
277
287
  # @version 5.0.0
278
288
  def fork_worker!
279
- if (worker = @workers.find { |w| w.index == 0 })
289
+ if (worker = worker_at 0)
280
290
  worker.phase += 1
281
291
  end
282
292
  phased_restart(true)
@@ -338,6 +348,8 @@ module Puma
338
348
  def run
339
349
  @status = :run
340
350
 
351
+ @idle_workers = {}
352
+
341
353
  output_header "cluster"
342
354
 
343
355
  # This is aligned with the output from Runner, see Runner#output_header
@@ -411,6 +423,8 @@ module Puma
411
423
 
412
424
  @master_read, @worker_write = read, @wakeup
413
425
 
426
+ @options[:worker_write] = @worker_write
427
+
414
428
  @config.run_hooks(:before_fork, nil, @log_writer)
415
429
 
416
430
  spawn_workers
@@ -426,6 +440,11 @@ module Puma
426
440
 
427
441
  while @status == :run
428
442
  begin
443
+ if all_workers_idle_timed_out?
444
+ log "- All workers reached idle timeout"
445
+ break
446
+ end
447
+
429
448
  if @phased_restart
430
449
  start_phased_restart
431
450
  @phased_restart = false
@@ -446,7 +465,7 @@ module Puma
446
465
 
447
466
  if req == "b" || req == "f"
448
467
  pid, idx = result.split(':').map(&:to_i)
449
- w = @workers.find {|x| x.index == idx}
468
+ w = worker_at idx
450
469
  w.pid = pid if w.pid.nil?
451
470
  end
452
471
 
@@ -463,24 +482,37 @@ module Puma
463
482
  when "t"
464
483
  w.term unless w.term?
465
484
  when "p"
466
- w.ping!(result.sub(/^\d+/,'').chomp)
485
+ status = result.sub(/^\d+/,'').chomp
486
+ w.ping!(status)
467
487
  @events.fire(:ping!, w)
488
+
489
+ if in_phased_restart && workers_not_booted.positive? && w0 = worker_at(0)
490
+ w0.ping!(status)
491
+ @events.fire(:ping!, w0)
492
+ end
493
+
468
494
  if !booted && @workers.none? {|worker| worker.last_status.empty?}
469
495
  @events.fire_on_booted!
470
496
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
471
497
  booted = true
472
498
  end
499
+ when "i"
500
+ if @idle_workers[pid]
501
+ @idle_workers.delete pid
502
+ else
503
+ @idle_workers[pid] = true
504
+ end
473
505
  end
474
506
  else
475
507
  log "! Out-of-sync worker list, no #{pid} worker"
476
508
  end
477
509
  end
510
+
478
511
  if in_phased_restart && workers_not_booted.zero?
479
512
  @events.fire_on_booted!
480
513
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
481
514
  in_phased_restart = false
482
515
  end
483
-
484
516
  rescue Interrupt
485
517
  @status = :stop
486
518
  end
@@ -509,10 +541,28 @@ module Puma
509
541
  # loops thru @workers, removing workers that exited, and calling
510
542
  # `#term` if needed
511
543
  def wait_workers
544
+ # Reap all children, known workers or otherwise.
545
+ # If puma has PID 1, as it's common in containerized environments,
546
+ # then it's responsible for reaping orphaned processes, so we must reap
547
+ # all our dead children, regardless of whether they are workers we spawned
548
+ # or some reattached processes.
549
+ reaped_children = {}
550
+ loop do
551
+ begin
552
+ pid, status = Process.wait2(-1, Process::WNOHANG)
553
+ break unless pid
554
+ reaped_children[pid] = status
555
+ rescue Errno::ECHILD
556
+ break
557
+ end
558
+ end
559
+
512
560
  @workers.reject! do |w|
513
561
  next false if w.pid.nil?
514
562
  begin
515
- if Process.wait(w.pid, Process::WNOHANG)
563
+ # When `fork_worker` is enabled, some worker may not be direct children, but grand children.
564
+ # Because of this they won't be reaped by `Process.wait2(-1)`, so we need to check them individually)
565
+ if reaped_children.delete(w.pid) || (@options[:fork_worker] && Process.wait(w.pid, Process::WNOHANG))
516
566
  true
517
567
  else
518
568
  w.term if w.term?
@@ -529,6 +579,11 @@ module Puma
529
579
  end
530
580
  end
531
581
  end
582
+
583
+ # Log unknown children
584
+ reaped_children.each do |pid, status|
585
+ log "! reaped unknown child process pid=#{pid} status=#{status}"
586
+ end
532
587
  end
533
588
 
534
589
  # @version 5.0.0
@@ -536,14 +591,18 @@ module Puma
536
591
  @workers.each do |w|
537
592
  if !w.term? && w.ping_timeout <= Time.now
538
593
  details = if w.booted?
539
- "(worker failed to check in within #{@options[:worker_timeout]} seconds)"
594
+ "(Worker #{w.index} failed to check in within #{@options[:worker_timeout]} seconds)"
540
595
  else
541
- "(worker failed to boot within #{@options[:worker_boot_timeout]} seconds)"
596
+ "(Worker #{w.index} failed to boot within #{@options[:worker_boot_timeout]} seconds)"
542
597
  end
543
598
  log "! Terminating timed out worker #{details}: #{w.pid}"
544
599
  w.kill
545
600
  end
546
601
  end
547
602
  end
603
+
604
+ def idle_timed_out_worker_pids
605
+ @idle_workers.keys
606
+ end
548
607
  end
549
608
  end
@@ -3,7 +3,7 @@
3
3
  require_relative 'rack/builder'
4
4
  require_relative 'plugin'
5
5
  require_relative 'const'
6
- # note that dsl is loaded at end of file, requires ConfigDefault constants
6
+ require_relative 'dsl'
7
7
 
8
8
  module Puma
9
9
  # A class used for storing "leveled" configuration options.
@@ -133,8 +133,10 @@ module Puma
133
133
  debug: false,
134
134
  early_hints: nil,
135
135
  environment: 'development'.freeze,
136
- # Number of seconds to wait until we get the first data for the request
136
+ # Number of seconds to wait until we get the first data for the request.
137
137
  first_data_timeout: 30,
138
+ # Number of seconds to wait until the next request before shutting down.
139
+ idle_timeout: nil,
138
140
  io_selector_backend: :auto,
139
141
  log_requests: false,
140
142
  logger: STDOUT,
@@ -385,5 +387,3 @@ module Puma
385
387
  end
386
388
  end
387
389
  end
388
-
389
- require_relative 'dsl'
data/lib/puma/const.rb CHANGED
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "6.3.1"
104
- CODE_NAME = "Mugi No Toki Itaru"
103
+ PUMA_VERSION = VERSION = "6.4.3"
104
+ CODE_NAME = "The Eagle of Durango"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
@@ -281,6 +281,14 @@ module Puma
281
281
  # header values can contain HTAB?
282
282
  ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
283
283
 
284
+ # The keys of headers that should not be convert to underscore
285
+ # normalized versions. These headers are ignored at the request reading layer,
286
+ # but if we normalize them after reading, it's just confusing for the application.
287
+ UNMASKABLE_HEADERS = {
288
+ "HTTP_TRANSFER,ENCODING" => true,
289
+ "HTTP_CONTENT,LENGTH" => true,
290
+ }
291
+
284
292
  # Banned keys of response header
285
293
  BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
286
294
 
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'optparse'
4
- require_relative 'state_file'
5
4
  require_relative 'const'
6
5
  require_relative 'detect'
7
- require_relative 'configuration'
8
6
  require 'uri'
9
7
  require 'socket'
10
8
 
@@ -126,6 +124,9 @@ module Puma
126
124
  end
127
125
 
128
126
  if @config_file
127
+ require_relative 'configuration'
128
+ require_relative 'log_writer'
129
+
129
130
  config = Puma::Configuration.new({ config_files: [@config_file] }, {})
130
131
  config.load
131
132
  @state ||= config.options[:state]
@@ -149,6 +150,8 @@ module Puma
149
150
  raise "State file not found: #{@state}"
150
151
  end
151
152
 
153
+ require_relative 'state_file'
154
+
152
155
  sf = Puma::StateFile.new
153
156
  sf.load @state
154
157
 
@@ -164,22 +167,26 @@ module Puma
164
167
  def send_request
165
168
  uri = URI.parse @control_url
166
169
 
170
+ host = uri.host
171
+
167
172
  # create server object by scheme
168
173
  server =
169
174
  case uri.scheme
170
175
  when 'ssl'
171
176
  require 'openssl'
177
+ host = host[1..-2] if host&.start_with? '['
172
178
  OpenSSL::SSL::SSLSocket.new(
173
- TCPSocket.new(uri.host, uri.port),
179
+ TCPSocket.new(host, uri.port),
174
180
  OpenSSL::SSL::SSLContext.new)
175
181
  .tap { |ssl| ssl.sync_close = true } # default is false
176
182
  .tap(&:connect)
177
183
  when 'tcp'
178
- TCPSocket.new uri.host, uri.port
184
+ host = host[1..-2] if host&.start_with? '['
185
+ TCPSocket.new host, uri.port
179
186
  when 'unix'
180
187
  # check for abstract UNIXSocket
181
188
  UNIXSocket.new(@control_url.start_with?('unix://@') ?
182
- "\0#{uri.host}#{uri.path}" : "#{uri.host}#{uri.path}")
189
+ "\0#{host}#{uri.path}" : "#{host}#{uri.path}")
183
190
  else
184
191
  raise "Invalid scheme: #{uri.scheme}"
185
192
  end
data/lib/puma/detect.rb CHANGED
@@ -12,15 +12,14 @@ module Puma
12
12
 
13
13
  IS_JRUBY = Object.const_defined? :JRUBY_VERSION
14
14
 
15
- IS_OSX = RUBY_PLATFORM.include? 'darwin'
15
+ IS_OSX = RUBY_DESCRIPTION.include? 'darwin'
16
16
 
17
- IS_WINDOWS = !!(RUBY_PLATFORM =~ /mswin|ming|cygwin/) ||
18
- IS_JRUBY && RUBY_DESCRIPTION.include?('mswin')
17
+ IS_WINDOWS = RUBY_DESCRIPTION.match?(/mswin|ming|cygwin/)
19
18
 
20
19
  IS_LINUX = !(IS_OSX || IS_WINDOWS)
21
20
 
22
21
  # @version 5.2.0
23
- IS_MRI = (RUBY_ENGINE == 'ruby' || RUBY_ENGINE.nil?)
22
+ IS_MRI = RUBY_ENGINE == 'ruby'
24
23
 
25
24
  def self.jruby?
26
25
  IS_JRUBY
data/lib/puma/dsl.rb CHANGED
@@ -315,16 +315,22 @@ module Puma
315
315
  bind URI::Generic.build(scheme: 'tcp', host: host, port: Integer(port)).to_s
316
316
  end
317
317
 
318
+ # Define how long the tcp socket stays open, if no data has been received.
319
+ # @see Puma::Server.new
320
+ def first_data_timeout(seconds)
321
+ @options[:first_data_timeout] = Integer(seconds)
322
+ end
323
+
318
324
  # Define how long persistent connections can be idle before Puma closes them.
319
325
  # @see Puma::Server.new
320
326
  def persistent_timeout(seconds)
321
327
  @options[:persistent_timeout] = Integer(seconds)
322
328
  end
323
329
 
324
- # Define how long the tcp socket stays open, if no data has been received.
330
+ # If a new request is not received within this number of seconds, begin shutting down.
325
331
  # @see Puma::Server.new
326
- def first_data_timeout(seconds)
327
- @options[:first_data_timeout] = Integer(seconds)
332
+ def idle_timeout(seconds)
333
+ @options[:idle_timeout] = Integer(seconds)
328
334
  end
329
335
 
330
336
  # Work around leaky apps that leave garbage in Thread locals
@@ -510,6 +516,12 @@ module Puma
510
516
  # `true`, which sets reuse 'on' with default values, or a hash, with `:size`
511
517
  # and/or `:timeout` keys, each with integer values.
512
518
  #
519
+ # The `cert:` options hash parameter can be the path to a certificate
520
+ # file including all intermediate certificates in PEM format.
521
+ #
522
+ # The `cert_pem:` options hash parameter can be String containing the
523
+ # cerificate and all intermediate certificates in PEM format.
524
+ #
513
525
  # @example
514
526
  # ssl_bind '127.0.0.1', '9292', {
515
527
  # cert: path_to_cert,
@@ -717,6 +729,51 @@ module Puma
717
729
  process_hook :before_refork, key, block, 'on_refork'
718
730
  end
719
731
 
732
+ # Provide a block to be executed just before a thread is added to the thread
733
+ # pool. Be careful: while the block executes, thread creation is delayed, and
734
+ # probably a request will have to wait too! The new thread will not be added to
735
+ # the threadpool until the provided block returns.
736
+ #
737
+ # Return values are ignored.
738
+ # Raising an exception will log a warning.
739
+ #
740
+ # This hook is useful for doing something when the thread pool grows.
741
+ #
742
+ # This can be called multiple times to add several hooks.
743
+ #
744
+ # @example
745
+ # on_thread_start do
746
+ # puts 'On thread start...'
747
+ # end
748
+ def on_thread_start(&block)
749
+ @options[:before_thread_start] ||= []
750
+ @options[:before_thread_start] << block
751
+ end
752
+
753
+ # Provide a block to be executed after a thread is trimmed from the thread
754
+ # pool. Be careful: while this block executes, Puma's main loop is
755
+ # blocked, so no new requests will be picked up.
756
+ #
757
+ # This hook only runs when a thread in the threadpool is trimmed by Puma.
758
+ # It does not run when a thread dies due to exceptions or any other cause.
759
+ #
760
+ # Return values are ignored.
761
+ # Raising an exception will log a warning.
762
+ #
763
+ # This hook is useful for cleaning up thread local resources when a thread
764
+ # is trimmed.
765
+ #
766
+ # This can be called multiple times to add several hooks.
767
+ #
768
+ # @example
769
+ # on_thread_exit do
770
+ # puts 'On thread exit...'
771
+ # end
772
+ def on_thread_exit(&block)
773
+ @options[:before_thread_exit] ||= []
774
+ @options[:before_thread_exit] << block
775
+ end
776
+
720
777
  # Code to run out-of-band when the worker is idle.
721
778
  # These hooks run immediately after a request has finished
722
779
  # processing and there are no busy threads on the worker.
@@ -848,7 +905,8 @@ module Puma
848
905
  # not a request timeout, it is to protect against a hung or dead process.
849
906
  # Setting this value will not protect against slow requests.
850
907
  #
851
- # The minimum value is 6 seconds, the default value is 60 seconds.
908
+ # This value must be greater than worker_check_interval.
909
+ # The default value is 60 seconds.
852
910
  #
853
911
  # @note Cluster mode only.
854
912
  # @example
@@ -1132,8 +1190,10 @@ module Puma
1132
1190
 
1133
1191
  def warn_if_in_single_mode(hook_name)
1134
1192
  return if @options[:silence_fork_callback_warning]
1135
-
1136
- if (@options[:workers] || 0) == 0
1193
+ # user_options (CLI) have precedence over config file
1194
+ workers_val = @config.options.user_options[:workers] || @options[:workers] ||
1195
+ @config.puma_default_options[:workers] || 0
1196
+ if workers_val == 0
1137
1197
  log_string =
1138
1198
  "Warning: You specified code to run in a `#{hook_name}` block, " \
1139
1199
  "but Puma is not configured to run in cluster mode (worker count > 0 ), " \
@@ -51,6 +51,8 @@ module Puma
51
51
  unless params['ca']
52
52
  log_writer.error "Please specify the SSL ca via 'ca='"
53
53
  end
54
+ # needed for Puma::MiniSSL::Socket#peercert, env['puma.peercert']
55
+ require 'openssl'
54
56
  end
55
57
 
56
58
  ctx.ca = params['ca'] if params['ca']
data/lib/puma/minissl.rb CHANGED
@@ -184,6 +184,11 @@ module Puma
184
184
  @socket.peeraddr
185
185
  end
186
186
 
187
+ # OpenSSL is loaded in `MiniSSL::ContextBuilder` when
188
+ # `MiniSSL::Context#verify_mode` is not `VERIFY_NONE`.
189
+ # When `VERIFY_NONE`, `MiniSSL::Engine#peercert` is nil, regardless of
190
+ # whether the client sends a cert.
191
+ # @return [OpenSSL::X509::Certificate, nil]
187
192
  # @!attribute [r] peercert
188
193
  def peercert
189
194
  return @peercert if @peercert
data/lib/puma/null_io.rb CHANGED
@@ -18,8 +18,22 @@ module Puma
18
18
 
19
19
  # Mimics IO#read with no data.
20
20
  #
21
- def read(count = nil, _buffer = nil)
22
- count && count > 0 ? nil : ""
21
+ def read(length = nil, buffer = nil)
22
+ if length.to_i < 0
23
+ raise ArgumentError, "(negative length #{length} given)"
24
+ end
25
+
26
+ buffer = if buffer.nil?
27
+ "".b
28
+ else
29
+ String.try_convert(buffer) or raise TypeError, "no implicit conversion of #{buffer.class} into String"
30
+ end
31
+ buffer.clear
32
+ if length.to_i > 0
33
+ nil
34
+ else
35
+ buffer
36
+ end
23
37
  end
24
38
 
25
39
  def rewind
@@ -34,7 +34,7 @@ module Puma::Rack
34
34
  end
35
35
 
36
36
  location = location.chomp('/')
37
- match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
37
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
38
38
 
39
39
  [host, location, match, app]
40
40
  }.sort_by do |(host, location, _, _)|
data/lib/puma/request.rb CHANGED
@@ -495,6 +495,11 @@ module Puma
495
495
  # compatibility, we'll convert them back. This code is written to
496
496
  # avoid allocation in the common case (ie there are no headers
497
497
  # with `,` in their names), that's why it has the extra conditionals.
498
+ #
499
+ # @note If a normalized version of a `,` header already exists, we ignore
500
+ # the `,` version. This prevents clobbering headers managed by proxies
501
+ # but not by clients (Like X-Forwarded-For).
502
+ #
498
503
  # @param env [Hash] see Puma::Client#env, from request, modifies in place
499
504
  # @version 5.0.3
500
505
  #
@@ -503,23 +508,31 @@ module Puma
503
508
  to_add = nil
504
509
 
505
510
  env.each do |k,v|
506
- if k.start_with?("HTTP_") && k.include?(",") && k != "HTTP_TRANSFER,ENCODING"
511
+ if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
507
512
  if to_delete
508
513
  to_delete << k
509
514
  else
510
515
  to_delete = [k]
511
516
  end
512
517
 
518
+ new_k = k.tr(",", "_")
519
+ if env.key?(new_k)
520
+ next
521
+ end
522
+
513
523
  unless to_add
514
524
  to_add = {}
515
525
  end
516
526
 
517
- to_add[k.tr(",", "_")] = v
527
+ to_add[new_k] = v
518
528
  end
519
529
  end
520
530
 
521
- if to_delete
531
+ if to_delete # rubocop:disable Style/SafeNavigation
522
532
  to_delete.each { |k| env.delete(k) }
533
+ end
534
+
535
+ if to_add
523
536
  env.merge! to_add
524
537
  end
525
538
  end
data/lib/puma/runner.rb CHANGED
@@ -70,12 +70,16 @@ module Puma
70
70
 
71
71
  app = Puma::App::Status.new @launcher, token
72
72
 
73
- # A Reactor is not created aand nio4r is not loaded when 'queue_requests: false'
73
+ # A Reactor is not created and nio4r is not loaded when 'queue_requests: false'
74
74
  # Use `nil` for events, no hooks in control server
75
75
  control = Puma::Server.new app, nil,
76
76
  { min_threads: 0, max_threads: 1, queue_requests: false, log_writer: @log_writer }
77
77
 
78
- control.binder.parse [str], nil, 'Starting control server'
78
+ begin
79
+ control.binder.parse [str], nil, 'Starting control server'
80
+ rescue Errno::EADDRINUSE, Errno::EACCES => e
81
+ raise e, "Error: Control server address '#{str}' is already in use. Original error: #{e.message}"
82
+ end
79
83
 
80
84
  control.run thread_name: 'ctl'
81
85
  @control = control