puma 5.5.0 → 5.6.7

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +140 -3
  3. data/README.md +28 -6
  4. data/docs/architecture.md +49 -16
  5. data/docs/compile_options.md +4 -2
  6. data/docs/deployment.md +53 -52
  7. data/docs/plugins.md +15 -15
  8. data/docs/rails_dev_mode.md +2 -3
  9. data/docs/restart.md +6 -6
  10. data/docs/signals.md +11 -10
  11. data/docs/stats.md +8 -8
  12. data/docs/systemd.md +63 -67
  13. data/ext/puma_http11/extconf.rb +18 -7
  14. data/ext/puma_http11/http11_parser.c +23 -10
  15. data/ext/puma_http11/http11_parser_common.rl +1 -1
  16. data/ext/puma_http11/mini_ssl.c +75 -12
  17. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +49 -47
  18. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +38 -55
  19. data/ext/puma_http11/puma_http11.c +1 -1
  20. data/lib/puma/app/status.rb +3 -0
  21. data/lib/puma/binder.rb +20 -6
  22. data/lib/puma/cli.rb +9 -4
  23. data/lib/puma/client.rb +68 -18
  24. data/lib/puma/cluster/worker.rb +7 -17
  25. data/lib/puma/cluster/worker_handle.rb +4 -0
  26. data/lib/puma/cluster.rb +29 -21
  27. data/lib/puma/configuration.rb +4 -1
  28. data/lib/puma/const.rb +7 -8
  29. data/lib/puma/control_cli.rb +19 -13
  30. data/lib/puma/detect.rb +8 -2
  31. data/lib/puma/dsl.rb +91 -10
  32. data/lib/puma/launcher.rb +13 -1
  33. data/lib/puma/minissl/context_builder.rb +8 -6
  34. data/lib/puma/minissl.rb +28 -7
  35. data/lib/puma/null_io.rb +5 -0
  36. data/lib/puma/plugin.rb +1 -1
  37. data/lib/puma/request.rb +15 -6
  38. data/lib/puma/runner.rb +22 -8
  39. data/lib/puma/server.rb +29 -30
  40. data/lib/puma/state_file.rb +42 -7
  41. data/lib/puma/thread_pool.rb +2 -2
  42. data/lib/puma/util.rb +19 -3
  43. data/lib/puma.rb +5 -3
  44. data/lib/rack/version_restriction.rb +15 -0
  45. data/tools/Dockerfile +1 -1
  46. metadata +4 -3
data/lib/puma/client.rb CHANGED
@@ -23,6 +23,8 @@ module Puma
23
23
 
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
+ class HttpParserError501 < IOError; end
27
+
26
28
  # An instance of this class represents a unique request from a client.
27
29
  # For example, this could be a web request from a browser or from CURL.
28
30
  #
@@ -35,7 +37,22 @@ module Puma
35
37
  # Instances of this class are responsible for knowing if
36
38
  # the header and body are fully buffered via the `try_to_finish` method.
37
39
  # They can be used to "time out" a response via the `timeout_at` reader.
40
+ #
38
41
  class Client
42
+
43
+ # this tests all values but the last, which must be chunked
44
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
45
+
46
+ # chunked body validation
47
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
48
+ CHUNK_VALID_ENDING = Const::LINE_END
49
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
50
+
51
+ # Content-Length header value validation
52
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
53
+
54
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
55
+
39
56
  # The object used for a request with no body. All requests with
40
57
  # no body share this one object since it has no state.
41
58
  EmptyBody = NullIO.new
@@ -161,8 +178,8 @@ module Puma
161
178
  def close
162
179
  begin
163
180
  @io.close
164
- rescue IOError
165
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
181
+ rescue IOError, Errno::EBADF
182
+ Puma::Util.purge_interrupt_queue
166
183
  end
167
184
  end
168
185
 
@@ -302,16 +319,27 @@ module Puma
302
319
  body = @parser.body
303
320
 
304
321
  te = @env[TRANSFER_ENCODING2]
305
-
306
322
  if te
307
- if te.include?(",")
308
- te.split(",").each do |part|
309
- if CHUNKED.casecmp(part.strip) == 0
310
- return setup_chunked_body(body)
311
- end
323
+ te_lwr = te.downcase
324
+ if te.include? ','
325
+ te_ary = te_lwr.split ','
326
+ te_count = te_ary.count CHUNKED
327
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
328
+ if te_ary.last == CHUNKED && te_count == 1 && te_valid
329
+ @env.delete TRANSFER_ENCODING2
330
+ return setup_chunked_body body
331
+ elsif te_count >= 1
332
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
333
+ elsif !te_valid
334
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
312
335
  end
313
- elsif CHUNKED.casecmp(te) == 0
314
- return setup_chunked_body(body)
336
+ elsif te_lwr == CHUNKED
337
+ @env.delete TRANSFER_ENCODING2
338
+ return setup_chunked_body body
339
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
340
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
341
+ else
342
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
315
343
  end
316
344
  end
317
345
 
@@ -319,7 +347,12 @@ module Puma
319
347
 
320
348
  cl = @env[CONTENT_LENGTH]
321
349
 
322
- unless cl
350
+ if cl
351
+ # cannot contain characters that are not \d, or be empty
352
+ if cl =~ CONTENT_LENGTH_VALUE_INVALID || cl.empty?
353
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
354
+ end
355
+ else
323
356
  @buffer = body.empty? ? nil : body
324
357
  @body = EmptyBody
325
358
  set_ready
@@ -477,19 +510,31 @@ module Puma
477
510
 
478
511
  while !io.eof?
479
512
  line = io.gets
480
- if line.end_with?("\r\n")
481
- len = line.strip.to_i(16)
513
+ if line.end_with?(CHUNK_VALID_ENDING)
514
+ # Puma doesn't process chunk extensions, but should parse if they're
515
+ # present, which is the reason for the semicolon regex
516
+ chunk_hex = line.strip[/\A[^;]+/]
517
+ if chunk_hex =~ CHUNK_SIZE_INVALID
518
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
519
+ end
520
+ len = chunk_hex.to_i(16)
482
521
  if len == 0
483
522
  @in_last_chunk = true
484
523
  @body.rewind
485
524
  rest = io.read
486
- last_crlf_size = "\r\n".bytesize
487
- if rest.bytesize < last_crlf_size
525
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
488
526
  @buffer = nil
489
- @partial_part_left = last_crlf_size - rest.bytesize
527
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
490
528
  return false
491
529
  else
492
- @buffer = rest[last_crlf_size..-1]
530
+ # if the next character is a CRLF, set buffer to everything after that CRLF
531
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
532
+ CHUNK_VALID_ENDING_SIZE
533
+ else # we have started a trailer section, which we do not support. skip it!
534
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
535
+ end
536
+
537
+ @buffer = rest[start_of_rest..-1]
493
538
  @buffer = nil if @buffer.empty?
494
539
  set_ready
495
540
  return true
@@ -509,7 +554,12 @@ module Puma
509
554
 
510
555
  case
511
556
  when got == len
512
- write_chunk(part[0..-3]) # to skip the ending \r\n
557
+ # proper chunked segment must end with "\r\n"
558
+ if part.end_with? CHUNK_VALID_ENDING
559
+ write_chunk(part[0..-3]) # to skip the ending \r\n
560
+ else
561
+ raise HttpParserError, "Chunk size mismatch"
562
+ end
513
563
  when got <= len - 2
514
564
  write_chunk(part)
515
565
  @partial_part_left = len - part.size
@@ -33,8 +33,8 @@ module Puma
33
33
  Signal.trap "SIGINT", "IGNORE"
34
34
  Signal.trap "SIGCHLD", "DEFAULT"
35
35
 
36
- Thread.new do
37
- Puma.set_thread_name "worker check pipe"
36
+ Thread.new do
37
+ Puma.set_thread_name "wrkr check"
38
38
  @check_pipe.wait_readable
39
39
  log "! Detected parent died, dying"
40
40
  exit! 1
@@ -76,7 +76,7 @@ module Puma
76
76
  end
77
77
 
78
78
  Thread.new do
79
- Puma.set_thread_name "worker fork pipe"
79
+ Puma.set_thread_name "wrkr fork"
80
80
  while (idx = @fork_pipe.gets)
81
81
  idx = idx.to_i
82
82
  if idx == -1 # stop server
@@ -106,7 +106,7 @@ module Puma
106
106
  begin
107
107
  @worker_write << "b#{Process.pid}:#{index}\n"
108
108
  rescue SystemCallError, IOError
109
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
109
+ Puma::Util.purge_interrupt_queue
110
110
  STDERR.puts "Master seems to have exited, exiting."
111
111
  return
112
112
  end
@@ -114,7 +114,7 @@ module Puma
114
114
  while restart_server.pop
115
115
  server_thread = server.run
116
116
  stat_thread ||= Thread.new(@worker_write) do |io|
117
- Puma.set_thread_name "stat payload"
117
+ Puma.set_thread_name "stat pld"
118
118
  base_payload = "p#{Process.pid}"
119
119
 
120
120
  while true
@@ -127,10 +127,10 @@ module Puma
127
127
  payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
128
128
  io << payload
129
129
  rescue IOError
130
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
130
+ Puma::Util.purge_interrupt_queue
131
131
  break
132
132
  end
133
- sleep Const::WORKER_CHECK_INTERVAL
133
+ sleep @options[:worker_check_interval]
134
134
  end
135
135
  end
136
136
  server_thread.join
@@ -168,16 +168,6 @@ module Puma
168
168
  @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
169
169
  pid
170
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
171
  end
182
172
  end
183
173
  end
@@ -40,6 +40,10 @@ module Puma
40
40
  @stage = :booted
41
41
  end
42
42
 
43
+ def term!
44
+ @term = true
45
+ end
46
+
43
47
  def term?
44
48
  @term
45
49
  end
data/lib/puma/cluster.rb CHANGED
@@ -108,24 +108,42 @@ module Puma
108
108
  def cull_workers
109
109
  diff = @workers.size - @options[:workers]
110
110
  return if diff < 1
111
+ debug "Culling #{diff} workers"
111
112
 
112
- debug "Culling #{diff.inspect} workers"
113
+ workers = workers_to_cull(diff)
114
+ debug "Workers to cull: #{workers.inspect}"
113
115
 
114
- workers_to_cull = @workers[-diff,diff]
115
- debug "Workers to cull: #{workers_to_cull.inspect}"
116
-
117
- workers_to_cull.each do |worker|
116
+ workers.each do |worker|
118
117
  log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
119
118
  worker.term
120
119
  end
121
120
  end
122
121
 
122
+ def workers_to_cull(diff)
123
+ workers = @workers.sort_by(&:started_at)
124
+
125
+ # In fork_worker mode, worker 0 acts as our master process.
126
+ # We should avoid culling it to preserve copy-on-write memory gains.
127
+ workers.reject! { |w| w.index == 0 } if @options[:fork_worker]
128
+
129
+ workers[cull_start_index(diff), diff]
130
+ end
131
+
132
+ def cull_start_index(diff)
133
+ case @options[:worker_culling_strategy]
134
+ when :oldest
135
+ 0
136
+ else # :youngest
137
+ -diff
138
+ end
139
+ end
140
+
123
141
  # @!attribute [r] next_worker_index
124
142
  def next_worker_index
125
- all_positions = 0...@options[:workers]
126
- occupied_positions = @workers.map { |w| w.index }
127
- available_positions = all_positions.to_a - occupied_positions
128
- available_positions.first
143
+ occupied_positions = @workers.map(&:index)
144
+ idx = 0
145
+ idx += 1 until !occupied_positions.include?(idx)
146
+ idx
129
147
  end
130
148
 
131
149
  def all_workers_booted?
@@ -135,7 +153,7 @@ module Puma
135
153
  def check_workers
136
154
  return if @next_check >= Time.now
137
155
 
138
- @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
156
+ @next_check = Time.now + @options[:worker_check_interval]
139
157
 
140
158
  timeout_workers
141
159
  wait_workers
@@ -164,16 +182,6 @@ module Puma
164
182
  ].compact.min
165
183
  end
166
184
 
167
- def wakeup!
168
- return unless @wakeup
169
-
170
- begin
171
- @wakeup.write "!" unless @wakeup.closed?
172
- rescue SystemCallError, IOError
173
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
174
- end
175
- end
176
-
177
185
  def worker(index, master)
178
186
  @workers = []
179
187
 
@@ -450,7 +458,7 @@ module Puma
450
458
  workers_not_booted -= 1
451
459
  when "e"
452
460
  # external term, see worker method, Signal.trap "SIGTERM"
453
- w.instance_variable_set :@term, true
461
+ w.term!
454
462
  when "t"
455
463
  w.term unless w.term?
456
464
  when "p"
@@ -11,6 +11,7 @@ module Puma
11
11
 
12
12
  DefaultTCPHost = "0.0.0.0"
13
13
  DefaultTCPPort = 9292
14
+ DefaultWorkerCheckInterval = 5
14
15
  DefaultWorkerTimeout = 60
15
16
  DefaultWorkerShutdownTimeout = 30
16
17
  end
@@ -195,12 +196,14 @@ module Puma
195
196
  :workers => Integer(ENV['WEB_CONCURRENCY'] || 0),
196
197
  :silence_single_worker_warning => false,
197
198
  :mode => :http,
199
+ :worker_check_interval => DefaultWorkerCheckInterval,
198
200
  :worker_timeout => DefaultWorkerTimeout,
199
201
  :worker_boot_timeout => DefaultWorkerTimeout,
200
202
  :worker_shutdown_timeout => DefaultWorkerShutdownTimeout,
203
+ :worker_culling_strategy => :youngest,
201
204
  :remote_address => :socket,
202
205
  :tag => method(:infer_tag),
203
- :environment => -> { ENV['RACK_ENV'] || ENV['RAILS_ENV'] || "development" },
206
+ :environment => -> { ENV['APP_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development' },
204
207
  :rackup => DefaultRackup,
205
208
  :logger => STDOUT,
206
209
  :persistent_timeout => Const::PERSISTENT_TIMEOUT,
data/lib/puma/const.rb CHANGED
@@ -76,7 +76,7 @@ module Puma
76
76
  508 => 'Loop Detected',
77
77
  510 => 'Not Extended',
78
78
  511 => 'Network Authentication Required'
79
- }
79
+ }.freeze
80
80
 
81
81
  # For some HTTP status codes the client only expects headers.
82
82
  #
@@ -85,7 +85,7 @@ module Puma
85
85
  204 => true,
86
86
  205 => true,
87
87
  304 => true
88
- }
88
+ }.freeze
89
89
 
90
90
  # Frequently used constants when constructing requests or responses. Many times
91
91
  # the constant just refers to a string with the same contents. Using these constants
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "5.5.0".freeze
104
- CODE_NAME = "Zawgyi".freeze
103
+ PUMA_VERSION = VERSION = "5.6.7".freeze
104
+ CODE_NAME = "Birdie's Version".freeze
105
105
 
106
106
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
107
107
 
@@ -145,9 +145,11 @@ module Puma
145
145
  408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze,
146
146
  # Indicate that there was an internal error, obviously.
147
147
  500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze,
148
+ # Incorrect or invalid header value
149
+ 501 => "HTTP/1.1 501 Not Implemented\r\n\r\n".freeze,
148
150
  # A common header for indicating the server is too busy. Not used yet.
149
151
  503 => "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
150
- }
152
+ }.freeze
151
153
 
152
154
  # The basic max request size we'll try to read.
153
155
  CHUNK_SIZE = 16 * 1024
@@ -235,9 +237,6 @@ module Puma
235
237
 
236
238
  EARLY_HINTS = "rack.early_hints".freeze
237
239
 
238
- # Minimum interval to checks worker health
239
- WORKER_CHECK_INTERVAL = 5
240
-
241
240
  # Illegal character in the key or value of response header
242
241
  DQUOTE = "\"".freeze
243
242
  HTTP_HEADER_DELIMITER = Regexp.escape("(),/:;<=>?@[]{}\\").freeze
@@ -17,26 +17,30 @@ module Puma
17
17
  CMD_PATH_SIG_MAP = {
18
18
  'gc' => nil,
19
19
  'gc-stats' => nil,
20
- 'halt' => 'SIGQUIT',
21
- 'phased-restart' => 'SIGUSR1',
22
- 'refork' => 'SIGURG',
20
+ 'halt' => 'SIGQUIT',
21
+ 'info' => 'SIGINFO',
22
+ 'phased-restart' => 'SIGUSR1',
23
+ 'refork' => 'SIGURG',
23
24
  'reload-worker-directory' => nil,
24
- 'restart' => 'SIGUSR2',
25
+ 'reopen-log' => 'SIGHUP',
26
+ 'restart' => 'SIGUSR2',
25
27
  'start' => nil,
26
28
  'stats' => nil,
27
29
  'status' => '',
28
- 'stop' => 'SIGTERM',
29
- 'thread-backtraces' => nil
30
+ 'stop' => 'SIGTERM',
31
+ 'thread-backtraces' => nil,
32
+ 'worker-count-down' => 'SIGTTOU',
33
+ 'worker-count-up' => 'SIGTTIN'
30
34
  }.freeze
31
35
 
32
36
  # @deprecated 6.0.0
33
37
  COMMANDS = CMD_PATH_SIG_MAP.keys.freeze
34
38
 
35
39
  # commands that cannot be used in a request
36
- NO_REQ_COMMANDS = %w{refork}.freeze
40
+ NO_REQ_COMMANDS = %w[info reopen-log worker-count-down worker-count-up].freeze
37
41
 
38
42
  # @version 5.0.0
39
- PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}.freeze
43
+ PRINTABLE_COMMANDS = %w[gc-stats stats thread-backtraces].freeze
40
44
 
41
45
  def initialize(argv, stdout=STDOUT, stderr=STDERR)
42
46
  @state = nil
@@ -47,7 +51,7 @@ module Puma
47
51
  @control_auth_token = nil
48
52
  @config_file = nil
49
53
  @command = nil
50
- @environment = ENV['RACK_ENV'] || ENV['RAILS_ENV']
54
+ @environment = ENV['APP_ENV'] || ENV['RACK_ENV'] || ENV['RAILS_ENV']
51
55
 
52
56
  @argv = argv.dup
53
57
  @stdout = stdout
@@ -185,8 +189,6 @@ module Puma
185
189
 
186
190
  if @command == 'status'
187
191
  message 'Puma is started'
188
- elsif NO_REQ_COMMANDS.include? @command
189
- raise "Invalid request command: #{@command}"
190
192
  else
191
193
  url = "/#{@command}"
192
194
 
@@ -242,7 +244,11 @@ module Puma
242
244
  @stdout.flush unless @stdout.sync
243
245
  return
244
246
  elsif sig.start_with? 'SIG'
245
- Process.kill sig, @pid
247
+ if Signal.list.key? sig.sub(/\ASIG/, '')
248
+ Process.kill sig, @pid
249
+ else
250
+ raise "Signal '#{sig}' not available'"
251
+ end
246
252
  elsif @command == 'status'
247
253
  begin
248
254
  Process.kill 0, @pid
@@ -268,7 +274,7 @@ module Puma
268
274
  return start if @command == 'start'
269
275
  prepare_configuration
270
276
 
271
- if Puma.windows? || @control_url
277
+ if Puma.windows? || @control_url && !NO_REQ_COMMANDS.include?(@command)
272
278
  send_request
273
279
  else
274
280
  send_signal
data/lib/puma/detect.rb CHANGED
@@ -10,8 +10,10 @@ module Puma
10
10
 
11
11
  IS_JRUBY = Object.const_defined? :JRUBY_VERSION
12
12
 
13
- IS_WINDOWS = !!(RUBY_PLATFORM =~ /mswin|ming|cygwin/ ||
14
- IS_JRUBY && RUBY_DESCRIPTION =~ /mswin/)
13
+ IS_OSX = RUBY_PLATFORM.include? 'darwin'
14
+
15
+ IS_WINDOWS = !!(RUBY_PLATFORM =~ /mswin|ming|cygwin/) ||
16
+ IS_JRUBY && RUBY_DESCRIPTION.include?('mswin')
15
17
 
16
18
  # @version 5.2.0
17
19
  IS_MRI = (RUBY_ENGINE == 'ruby' || RUBY_ENGINE.nil?)
@@ -20,6 +22,10 @@ module Puma
20
22
  IS_JRUBY
21
23
  end
22
24
 
25
+ def self.osx?
26
+ IS_OSX
27
+ end
28
+
23
29
  def self.windows?
24
30
  IS_WINDOWS
25
31
  end
data/lib/puma/dsl.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'puma/const'
4
+ require 'puma/util'
4
5
 
5
6
  module Puma
6
7
  # The methods that are available for use inside the configuration file.
@@ -46,7 +47,9 @@ module Puma
46
47
  else ''
47
48
  end
48
49
 
49
- ca_additions = "&ca=#{opts[:ca]}" if ['peer', 'force_peer'].include?(verify)
50
+ ca_additions = "&ca=#{Puma::Util.escape(opts[:ca])}" if ['peer', 'force_peer'].include?(verify)
51
+
52
+ backlog_str = opts[:backlog] ? "&backlog=#{Integer(opts[:backlog])}" : ''
50
53
 
51
54
  if defined?(JRUBY_VERSION)
52
55
  ssl_cipher_list = opts[:ssl_cipher_list] ?
@@ -55,7 +58,7 @@ module Puma
55
58
  keystore_additions = "keystore=#{opts[:keystore]}&keystore-pass=#{opts[:keystore_pass]}"
56
59
 
57
60
  "ssl://#{host}:#{port}?#{keystore_additions}#{ssl_cipher_list}" \
58
- "&verify_mode=#{verify}#{tls_str}#{ca_additions}"
61
+ "&verify_mode=#{verify}#{tls_str}#{ca_additions}#{backlog_str}"
59
62
  else
60
63
  ssl_cipher_filter = opts[:ssl_cipher_filter] ?
61
64
  "&ssl_cipher_filter=#{opts[:ssl_cipher_filter]}" : nil
@@ -63,8 +66,11 @@ module Puma
63
66
  v_flags = (ary = opts[:verification_flags]) ?
64
67
  "&verification_flags=#{Array(ary).join ','}" : nil
65
68
 
66
- "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}" \
67
- "#{ssl_cipher_filter}&verify_mode=#{verify}#{tls_str}#{ca_additions}#{v_flags}"
69
+ cert_flags = (cert = opts[:cert]) ? "cert=#{Puma::Util.escape(opts[:cert])}" : nil
70
+ key_flags = (cert = opts[:key]) ? "&key=#{Puma::Util.escape(opts[:key])}" : nil
71
+
72
+ "ssl://#{host}:#{port}?#{cert_flags}#{key_flags}" \
73
+ "#{ssl_cipher_filter}&verify_mode=#{verify}#{tls_str}#{ca_additions}#{v_flags}#{backlog_str}"
68
74
  end
69
75
  end
70
76
 
@@ -191,7 +197,7 @@ module Puma
191
197
  end
192
198
 
193
199
  # Bind the server to +url+. "tcp://", "unix://" and "ssl://" are the only
194
- # accepted protocols. Multiple urls can be bound to, calling `bind` does
200
+ # accepted protocols. Multiple urls can be bound to, calling +bind+ does
195
201
  # not overwrite previous bindings.
196
202
  #
197
203
  # The default is "tcp://0.0.0.0:9292".
@@ -436,8 +442,15 @@ module Puma
436
442
  @options[:max_threads] = max
437
443
  end
438
444
 
439
- # Instead of `bind 'ssl://127.0.0.1:9292?key=key_path&cert=cert_path'` you
440
- # can also use the this method.
445
+ # Instead of using +bind+ and manually constructing a URI like:
446
+ #
447
+ # bind 'ssl://127.0.0.1:9292?key=key_path&cert=cert_path'
448
+ #
449
+ # you can use the this method.
450
+ #
451
+ # When binding on localhost you don't need to specify +cert+ and +key+,
452
+ # Puma will assume you are using the +localhost+ gem and try to load the
453
+ # appropriate files.
441
454
  #
442
455
  # @example
443
456
  # ssl_bind '127.0.0.1', '9292', {
@@ -447,14 +460,25 @@ module Puma
447
460
  # verify_mode: verify_mode, # default 'none'
448
461
  # verification_flags: flags, # optional, not supported by JRuby
449
462
  # }
450
- # @example For JRuby, two keys are required: keystore & keystore_pass.
463
+ #
464
+ # @example Using self-signed certificate with the +localhost+ gem:
465
+ # ssl_bind '127.0.0.1', '9292'
466
+ #
467
+ # @example Alternatively, you can provide +cert_pem+ and +key_pem+:
468
+ # ssl_bind '127.0.0.1', '9292', {
469
+ # cert_pem: File.read(path_to_cert),
470
+ # key_pem: File.read(path_to_key),
471
+ # }
472
+ #
473
+ # @example For JRuby, two keys are required: +keystore+ & +keystore_pass+
451
474
  # ssl_bind '127.0.0.1', '9292', {
452
475
  # keystore: path_to_keystore,
453
476
  # keystore_pass: password,
454
477
  # ssl_cipher_list: cipher_list, # optional
455
478
  # verify_mode: verify_mode # default 'none'
456
479
  # }
457
- def ssl_bind(host, port, opts)
480
+ def ssl_bind(host, port, opts = {})
481
+ add_pem_values_to_options_store(opts)
458
482
  bind self.class.ssl_bind_str(host, port, opts)
459
483
  end
460
484
 
@@ -727,6 +751,19 @@ module Puma
727
751
  @options[:tag] = string.to_s
728
752
  end
729
753
 
754
+ # Change the default interval for checking workers.
755
+ #
756
+ # The default value is 5 seconds.
757
+ #
758
+ # @note Cluster mode only.
759
+ # @example
760
+ # worker_check_interval 5
761
+ # @see Puma::Cluster#check_workers
762
+ #
763
+ def worker_check_interval(interval)
764
+ @options[:worker_check_interval] = Integer(interval)
765
+ end
766
+
730
767
  # Verifies that all workers have checked in to the master process within
731
768
  # the given timeout. If not the worker process will be restarted. This is
732
769
  # not a request timeout, it is to protect against a hung or dead process.
@@ -741,7 +778,7 @@ module Puma
741
778
  #
742
779
  def worker_timeout(timeout)
743
780
  timeout = Integer(timeout)
744
- min = Const::WORKER_CHECK_INTERVAL
781
+ min = @options.fetch(:worker_check_interval, Puma::ConfigDefault::DefaultWorkerCheckInterval)
745
782
 
746
783
  if timeout <= min
747
784
  raise "The minimum worker_timeout must be greater than the worker reporting interval (#{min})"
@@ -773,6 +810,30 @@ module Puma
773
810
  @options[:worker_shutdown_timeout] = Integer(timeout)
774
811
  end
775
812
 
813
+ # Set the strategy for worker culling.
814
+ #
815
+ # There are two possible values:
816
+ #
817
+ # 1. **:youngest** - the youngest workers (i.e. the workers that were
818
+ # the most recently started) will be culled.
819
+ # 2. **:oldest** - the oldest workers (i.e. the workers that were started
820
+ # the longest time ago) will be culled.
821
+ #
822
+ # @note Cluster mode only.
823
+ # @example
824
+ # worker_culling_strategy :oldest
825
+ # @see Puma::Cluster#cull_workers
826
+ #
827
+ def worker_culling_strategy(strategy)
828
+ stategy = strategy.to_sym
829
+
830
+ if ![:youngest, :oldest].include?(strategy)
831
+ raise "Invalid value for worker_culling_strategy - #{stategy}"
832
+ end
833
+
834
+ @options[:worker_culling_strategy] = strategy
835
+ end
836
+
776
837
  # When set to true (the default), workers accept all requests
777
838
  # and queue them before passing them to the handlers.
778
839
  # When set to false, each worker process accepts exactly as
@@ -927,5 +988,25 @@ module Puma
927
988
  def mutate_stdout_and_stderr_to_sync_on_write(enabled=true)
928
989
  @options[:mutate_stdout_and_stderr_to_sync_on_write] = enabled
929
990
  end
991
+
992
+ private
993
+
994
+ # To avoid adding cert_pem and key_pem as URI params, we store them on the
995
+ # options[:store] from where Puma binder knows how to find and extract them.
996
+ def add_pem_values_to_options_store(opts)
997
+ return if defined?(JRUBY_VERSION)
998
+
999
+ @options[:store] ||= []
1000
+
1001
+ # Store cert_pem and key_pem to options[:store] if present
1002
+ [:cert, :key].each do |v|
1003
+ opt_key = :"#{v}_pem"
1004
+ if opts[opt_key]
1005
+ index = @options[:store].length
1006
+ @options[:store] << opts[opt_key]
1007
+ opts[v] = "store:#{index}"
1008
+ end
1009
+ end
1010
+ end
930
1011
  end
931
1012
  end