puma 5.3.2 → 6.0.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +284 -11
  3. data/LICENSE +0 -0
  4. data/README.md +61 -16
  5. data/bin/puma-wild +1 -1
  6. data/docs/architecture.md +49 -16
  7. data/docs/compile_options.md +38 -2
  8. data/docs/deployment.md +53 -67
  9. data/docs/fork_worker.md +1 -3
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/jungle/README.md +0 -0
  14. data/docs/jungle/rc.d/README.md +0 -0
  15. data/docs/jungle/rc.d/puma.conf +0 -0
  16. data/docs/kubernetes.md +0 -0
  17. data/docs/nginx.md +0 -0
  18. data/docs/plugins.md +15 -15
  19. data/docs/rails_dev_mode.md +2 -3
  20. data/docs/restart.md +6 -6
  21. data/docs/signals.md +11 -10
  22. data/docs/stats.md +8 -8
  23. data/docs/systemd.md +64 -67
  24. data/docs/testing_benchmarks_local_files.md +150 -0
  25. data/docs/testing_test_rackup_ci_files.md +36 -0
  26. data/ext/puma_http11/PumaHttp11Service.java +0 -0
  27. data/ext/puma_http11/ext_help.h +0 -0
  28. data/ext/puma_http11/extconf.rb +44 -13
  29. data/ext/puma_http11/http11_parser.c +24 -11
  30. data/ext/puma_http11/http11_parser.h +1 -1
  31. data/ext/puma_http11/http11_parser.java.rl +2 -2
  32. data/ext/puma_http11/http11_parser.rl +2 -2
  33. data/ext/puma_http11/http11_parser_common.rl +3 -3
  34. data/ext/puma_http11/mini_ssl.c +122 -23
  35. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
  36. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  37. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +50 -48
  38. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +188 -102
  39. data/ext/puma_http11/puma_http11.c +18 -10
  40. data/lib/puma/app/status.rb +9 -6
  41. data/lib/puma/binder.rb +81 -42
  42. data/lib/puma/cli.rb +23 -19
  43. data/lib/puma/client.rb +124 -30
  44. data/lib/puma/cluster/worker.rb +21 -29
  45. data/lib/puma/cluster/worker_handle.rb +8 -1
  46. data/lib/puma/cluster.rb +57 -48
  47. data/lib/puma/commonlogger.rb +0 -0
  48. data/lib/puma/configuration.rb +74 -55
  49. data/lib/puma/const.rb +21 -24
  50. data/lib/puma/control_cli.rb +22 -19
  51. data/lib/puma/detect.rb +10 -2
  52. data/lib/puma/dsl.rb +196 -57
  53. data/lib/puma/error_logger.rb +17 -9
  54. data/lib/puma/events.rb +6 -126
  55. data/lib/puma/io_buffer.rb +29 -4
  56. data/lib/puma/jruby_restart.rb +2 -1
  57. data/lib/puma/{json.rb → json_serialization.rb} +1 -1
  58. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  59. data/lib/puma/launcher.rb +108 -154
  60. data/lib/puma/log_writer.rb +137 -0
  61. data/lib/puma/minissl/context_builder.rb +29 -16
  62. data/lib/puma/minissl.rb +115 -38
  63. data/lib/puma/null_io.rb +5 -0
  64. data/lib/puma/plugin/tmp_restart.rb +1 -1
  65. data/lib/puma/plugin.rb +2 -2
  66. data/lib/puma/rack/builder.rb +5 -5
  67. data/lib/puma/rack/urlmap.rb +0 -0
  68. data/lib/puma/rack_default.rb +1 -1
  69. data/lib/puma/reactor.rb +3 -3
  70. data/lib/puma/request.rb +293 -153
  71. data/lib/puma/runner.rb +63 -28
  72. data/lib/puma/server.rb +83 -88
  73. data/lib/puma/single.rb +10 -10
  74. data/lib/puma/state_file.rb +39 -7
  75. data/lib/puma/systemd.rb +3 -2
  76. data/lib/puma/thread_pool.rb +22 -17
  77. data/lib/puma/util.rb +20 -15
  78. data/lib/puma.rb +12 -9
  79. data/lib/rack/handler/puma.rb +9 -9
  80. data/tools/Dockerfile +1 -1
  81. data/tools/trickletest.rb +0 -0
  82. metadata +13 -9
  83. data/lib/puma/queue_close.rb +0 -26
data/lib/puma/client.rb CHANGED
@@ -8,7 +8,7 @@ class IO
8
8
  end
9
9
  end
10
10
 
11
- require 'puma/detect'
11
+ require_relative 'detect'
12
12
  require 'tempfile'
13
13
  require 'forwardable'
14
14
 
@@ -23,6 +23,11 @@ module Puma
23
23
 
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
+ class HttpParserError501 < IOError; end
27
+
28
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
29
+
30
+
26
31
  # An instance of this class represents a unique request from a client.
27
32
  # For example, this could be a web request from a browser or from CURL.
28
33
  #
@@ -35,7 +40,21 @@ module Puma
35
40
  # Instances of this class are responsible for knowing if
36
41
  # the header and body are fully buffered via the `try_to_finish` method.
37
42
  # They can be used to "time out" a response via the `timeout_at` reader.
38
- class Client
43
+ #
44
+ class Client # :nodoc:
45
+
46
+ # this tests all values but the last, which must be chunked
47
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
48
+
49
+ # chunked body validation
50
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
51
+ CHUNK_VALID_ENDING = "\r\n".freeze
52
+
53
+ # Content-Length header value validation
54
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
55
+
56
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
57
+
39
58
  # The object used for a request with no body. All requests with
40
59
  # no body share this one object since it has no state.
41
60
  EmptyBody = NullIO.new
@@ -47,15 +66,12 @@ module Puma
47
66
  @io = io
48
67
  @to_io = io.to_io
49
68
  @proto_env = env
50
- if !env
51
- @env = nil
52
- else
53
- @env = env.dup
54
- end
69
+ @env = env ? env.dup : nil
55
70
 
56
71
  @parser = HttpParser.new
57
72
  @parsed_bytes = 0
58
73
  @read_header = true
74
+ @read_proxy = false
59
75
  @ready = false
60
76
 
61
77
  @body = nil
@@ -69,8 +85,10 @@ module Puma
69
85
  @hijacked = false
70
86
 
71
87
  @peerip = nil
88
+ @peer_family = nil
72
89
  @listener = nil
73
90
  @remote_addr_header = nil
91
+ @expect_proxy_proto = false
74
92
 
75
93
  @body_remain = 0
76
94
 
@@ -106,7 +124,7 @@ module Puma
106
124
 
107
125
  # @!attribute [r] in_data_phase
108
126
  def in_data_phase
109
- !@read_header
127
+ !(@read_header || @read_proxy)
110
128
  end
111
129
 
112
130
  def set_timeout(val)
@@ -121,6 +139,7 @@ module Puma
121
139
  def reset(fast_check=true)
122
140
  @parser.reset
123
141
  @read_header = true
142
+ @read_proxy = !!@expect_proxy_proto
124
143
  @env = @proto_env.dup
125
144
  @body = nil
126
145
  @tempfile = nil
@@ -131,6 +150,8 @@ module Puma
131
150
  @in_last_chunk = false
132
151
 
133
152
  if @buffer
153
+ return false unless try_to_parse_proxy_protocol
154
+
134
155
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
135
156
 
136
157
  if @parser.finished?
@@ -143,8 +164,7 @@ module Puma
143
164
  return false
144
165
  else
145
166
  begin
146
- if fast_check &&
147
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
167
+ if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
148
168
  return try_to_finish
149
169
  end
150
170
  rescue IOError
@@ -157,13 +177,37 @@ module Puma
157
177
  def close
158
178
  begin
159
179
  @io.close
160
- rescue IOError
161
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
180
+ rescue IOError, Errno::EBADF
181
+ Puma::Util.purge_interrupt_queue
182
+ end
183
+ end
184
+
185
+ # If necessary, read the PROXY protocol from the buffer. Returns
186
+ # false if more data is needed.
187
+ def try_to_parse_proxy_protocol
188
+ if @read_proxy
189
+ if @expect_proxy_proto == :v1
190
+ if @buffer.include? "\r\n"
191
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
192
+ if md[1]
193
+ @peerip = md[1].split(" ")[0]
194
+ end
195
+ @buffer = md.post_match
196
+ end
197
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
198
+ # request, this is just HTTP from a non-PROXY client; move on
199
+ @read_proxy = false
200
+ return @buffer.size > 0
201
+ else
202
+ return false
203
+ end
204
+ end
162
205
  end
206
+ true
163
207
  end
164
208
 
165
209
  def try_to_finish
166
- return read_body unless @read_header
210
+ return read_body if in_data_phase
167
211
 
168
212
  begin
169
213
  data = @io.read_nonblock(CHUNK_SIZE)
@@ -188,6 +232,8 @@ module Puma
188
232
  @buffer = data
189
233
  end
190
234
 
235
+ return false unless try_to_parse_proxy_protocol
236
+
191
237
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
192
238
 
193
239
  if @parser.finished?
@@ -202,13 +248,13 @@ module Puma
202
248
 
203
249
  def eagerly_finish
204
250
  return true if @ready
205
- return false unless IO.select([@to_io], nil, nil, 0)
251
+ return false unless @to_io.wait_readable(0)
206
252
  try_to_finish
207
253
  end
208
254
 
209
255
  def finish(timeout)
210
256
  return if @ready
211
- IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
257
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
212
258
  end
213
259
 
214
260
  def timeout!
@@ -227,7 +273,7 @@ module Puma
227
273
  return @peerip if @peerip
228
274
 
229
275
  if @remote_addr_header
230
- hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
276
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
231
277
  @peerip = hdr
232
278
  return hdr
233
279
  end
@@ -235,6 +281,16 @@ module Puma
235
281
  @peerip ||= @io.peeraddr.last
236
282
  end
237
283
 
284
+ def peer_family
285
+ return @peer_family if @peer_family
286
+
287
+ @peer_family ||= begin
288
+ @io.local_address.afamily
289
+ rescue
290
+ Socket::AF_INET
291
+ end
292
+ end
293
+
238
294
  # Returns true if the persistent connection can be closed immediately
239
295
  # without waiting for the configured idle/shutdown timeout.
240
296
  # @version 5.0.0
@@ -244,10 +300,21 @@ module Puma
244
300
  @parsed_bytes == 0
245
301
  end
246
302
 
303
+ def expect_proxy_proto=(val)
304
+ if val
305
+ if @read_header
306
+ @read_proxy = true
307
+ end
308
+ else
309
+ @read_proxy = false
310
+ end
311
+ @expect_proxy_proto = val
312
+ end
313
+
247
314
  private
248
315
 
249
316
  def setup_body
250
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
317
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
251
318
 
252
319
  if @env[HTTP_EXPECT] == CONTINUE
253
320
  # TODO allow a hook here to check the headers before
@@ -261,16 +328,27 @@ module Puma
261
328
  body = @parser.body
262
329
 
263
330
  te = @env[TRANSFER_ENCODING2]
264
-
265
331
  if te
266
- if te.include?(",")
267
- te.split(",").each do |part|
268
- if CHUNKED.casecmp(part.strip) == 0
269
- return setup_chunked_body(body)
270
- end
332
+ te_lwr = te.downcase
333
+ if te.include? ','
334
+ te_ary = te_lwr.split ','
335
+ te_count = te_ary.count CHUNKED
336
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
337
+ if te_ary.last == CHUNKED && te_count == 1 && te_valid
338
+ @env.delete TRANSFER_ENCODING2
339
+ return setup_chunked_body body
340
+ elsif te_count >= 1
341
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
342
+ elsif !te_valid
343
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
271
344
  end
272
- elsif CHUNKED.casecmp(te) == 0
273
- return setup_chunked_body(body)
345
+ elsif te_lwr == CHUNKED
346
+ @env.delete TRANSFER_ENCODING2
347
+ return setup_chunked_body body
348
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
349
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
350
+ else
351
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
274
352
  end
275
353
  end
276
354
 
@@ -278,7 +356,12 @@ module Puma
278
356
 
279
357
  cl = @env[CONTENT_LENGTH]
280
358
 
281
- unless cl
359
+ if cl
360
+ # cannot contain characters that are not \d
361
+ if CONTENT_LENGTH_VALUE_INVALID.match? cl
362
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
363
+ end
364
+ else
282
365
  @buffer = body.empty? ? nil : body
283
366
  @body = EmptyBody
284
367
  set_ready
@@ -309,7 +392,7 @@ module Puma
309
392
 
310
393
  @body_remain = remain
311
394
 
312
- return false
395
+ false
313
396
  end
314
397
 
315
398
  def read_body
@@ -437,7 +520,13 @@ module Puma
437
520
  while !io.eof?
438
521
  line = io.gets
439
522
  if line.end_with?("\r\n")
440
- len = line.strip.to_i(16)
523
+ # Puma doesn't process chunk extensions, but should parse if they're
524
+ # present, which is the reason for the semicolon regex
525
+ chunk_hex = line.strip[/\A[^;]+/]
526
+ if CHUNK_SIZE_INVALID.match? chunk_hex
527
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
528
+ end
529
+ len = chunk_hex.to_i(16)
441
530
  if len == 0
442
531
  @in_last_chunk = true
443
532
  @body.rewind
@@ -468,7 +557,12 @@ module Puma
468
557
 
469
558
  case
470
559
  when got == len
471
- write_chunk(part[0..-3]) # to skip the ending \r\n
560
+ # proper chunked segment must end with "\r\n"
561
+ if part.end_with? CHUNK_VALID_ENDING
562
+ write_chunk(part[0..-3]) # to skip the ending \r\n
563
+ else
564
+ raise HttpParserError, "Chunk size mismatch"
565
+ end
472
566
  when got <= len - 2
473
567
  write_chunk(part)
474
568
  @partial_part_left = len - part.size
@@ -492,7 +586,7 @@ module Puma
492
586
 
493
587
  def set_ready
494
588
  if @body_read_start
495
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
589
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
496
590
  end
497
591
  @requests_served += 1
498
592
  @ready = true
@@ -2,27 +2,29 @@
2
2
 
3
3
  module Puma
4
4
  class Cluster < Puma::Runner
5
+ #—————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
5
8
  # This class is instantiated by the `Puma::Cluster` and represents a single
6
9
  # worker process.
7
10
  #
8
11
  # At the core of this class is running an instance of `Puma::Server` which
9
12
  # gets created via the `start_server` method from the `Puma::Runner` class
10
13
  # that this inherits from.
11
- class Worker < Puma::Runner
14
+ class Worker < Puma::Runner # :nodoc:
12
15
  attr_reader :index, :master
13
16
 
14
17
  def initialize(index:, master:, launcher:, pipes:, server: nil)
15
- super launcher, launcher.events
18
+ super(launcher)
16
19
 
17
20
  @index = index
18
21
  @master = master
19
- @launcher = launcher
20
- @options = launcher.options
21
22
  @check_pipe = pipes[:check_pipe]
22
23
  @worker_write = pipes[:worker_write]
23
24
  @fork_pipe = pipes[:fork_pipe]
24
25
  @wakeup = pipes[:wakeup]
25
26
  @server = server
27
+ @hook_data = {}
26
28
  end
27
29
 
28
30
  def run
@@ -33,9 +35,9 @@ module Puma
33
35
  Signal.trap "SIGINT", "IGNORE"
34
36
  Signal.trap "SIGCHLD", "DEFAULT"
35
37
 
36
- Thread.new do
37
- Puma.set_thread_name "worker check pipe"
38
- IO.select [@check_pipe]
38
+ Thread.new do
39
+ Puma.set_thread_name "wrkr check"
40
+ @check_pipe.wait_readable
39
41
  log "! Detected parent died, dying"
40
42
  exit! 1
41
43
  end
@@ -52,13 +54,14 @@ module Puma
52
54
 
53
55
  # Invoke any worker boot hooks so they can get
54
56
  # things in shape before booting the app.
55
- @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
57
+ @config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
56
58
 
57
59
  begin
58
60
  server = @server ||= start_server
59
61
  rescue Exception => e
60
62
  log "! Unable to start worker"
61
- log e.backtrace[0]
63
+ log e
64
+ log e.backtrace.join("\n ")
62
65
  exit 1
63
66
  end
64
67
 
@@ -76,15 +79,14 @@ module Puma
76
79
  end
77
80
 
78
81
  Thread.new do
79
- Puma.set_thread_name "worker fork pipe"
82
+ Puma.set_thread_name "wrkr fork"
80
83
  while (idx = @fork_pipe.gets)
81
84
  idx = idx.to_i
82
85
  if idx == -1 # stop server
83
86
  if restart_server.length > 0
84
87
  restart_server.clear
85
88
  server.begin_restart(true)
86
- @launcher.config.run_hooks :before_refork, nil, @launcher.events
87
- Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
89
+ @config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
88
90
  end
89
91
  elsif idx == 0 # restart server
90
92
  restart_server << true << false
@@ -106,7 +108,7 @@ module Puma
106
108
  begin
107
109
  @worker_write << "b#{Process.pid}:#{index}\n"
108
110
  rescue SystemCallError, IOError
109
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
111
+ Puma::Util.purge_interrupt_queue
110
112
  STDERR.puts "Master seems to have exited, exiting."
111
113
  return
112
114
  end
@@ -114,7 +116,7 @@ module Puma
114
116
  while restart_server.pop
115
117
  server_thread = server.run
116
118
  stat_thread ||= Thread.new(@worker_write) do |io|
117
- Puma.set_thread_name "stat payload"
119
+ Puma.set_thread_name "stat pld"
118
120
  base_payload = "p#{Process.pid}"
119
121
 
120
122
  while true
@@ -127,10 +129,10 @@ module Puma
127
129
  payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
128
130
  io << payload
129
131
  rescue IOError
130
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
132
+ Puma::Util.purge_interrupt_queue
131
133
  break
132
134
  end
133
- sleep Const::WORKER_CHECK_INTERVAL
135
+ sleep @options[:worker_check_interval]
134
136
  end
135
137
  end
136
138
  server_thread.join
@@ -138,7 +140,7 @@ module Puma
138
140
 
139
141
  # Invoke any worker shutdown hooks so they can prevent the worker
140
142
  # exiting until any background operations are completed
141
- @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
143
+ @config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
142
144
  ensure
143
145
  @worker_write << "t#{Process.pid}\n" rescue nil
144
146
  @worker_write.close
@@ -147,7 +149,7 @@ module Puma
147
149
  private
148
150
 
149
151
  def spawn_worker(idx)
150
- @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
152
+ @config.run_hooks(:before_worker_fork, idx, @log_writer, @hook_data)
151
153
 
152
154
  pid = fork do
153
155
  new_worker = Worker.new index: idx,
@@ -165,19 +167,9 @@ module Puma
165
167
  exit! 1
166
168
  end
167
169
 
168
- @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
170
+ @config.run_hooks(:after_worker_fork, idx, @log_writer, @hook_data)
169
171
  pid
170
172
  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
173
  end
182
174
  end
183
175
  end
@@ -2,12 +2,15 @@
2
2
 
3
3
  module Puma
4
4
  class Cluster < Runner
5
+ #—————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
5
8
  # This class represents a worker process from the perspective of the puma
6
9
  # master process. It contains information about the process and its health
7
10
  # and it exposes methods to control the process via IPC. It does not
8
11
  # include the actual logic executed by the worker process itself. For that,
9
12
  # see Puma::Cluster::Worker.
10
- class WorkerHandle
13
+ class WorkerHandle # :nodoc:
11
14
  def initialize(idx, pid, phase, options)
12
15
  @index = idx
13
16
  @pid = pid
@@ -40,6 +43,10 @@ module Puma
40
43
  @stage = :booted
41
44
  end
42
45
 
46
+ def term!
47
+ @term = true
48
+ end
49
+
43
50
  def term?
44
51
  @term
45
52
  end