puma 5.6.9-java → 6.6.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +465 -18
  3. data/README.md +152 -42
  4. data/bin/puma-wild +1 -1
  5. data/docs/compile_options.md +34 -0
  6. data/docs/fork_worker.md +12 -4
  7. data/docs/java_options.md +54 -0
  8. data/docs/kubernetes.md +12 -0
  9. data/docs/nginx.md +1 -1
  10. data/docs/plugins.md +4 -0
  11. data/docs/restart.md +1 -0
  12. data/docs/signals.md +2 -2
  13. data/docs/stats.md +8 -3
  14. data/docs/systemd.md +13 -7
  15. data/docs/testing_benchmarks_local_files.md +150 -0
  16. data/docs/testing_test_rackup_ci_files.md +36 -0
  17. data/ext/puma_http11/extconf.rb +27 -17
  18. data/ext/puma_http11/http11_parser.c +1 -1
  19. data/ext/puma_http11/http11_parser.h +1 -1
  20. data/ext/puma_http11/http11_parser.java.rl +2 -2
  21. data/ext/puma_http11/http11_parser.rl +2 -2
  22. data/ext/puma_http11/http11_parser_common.rl +2 -2
  23. data/ext/puma_http11/mini_ssl.c +137 -19
  24. data/ext/puma_http11/org/jruby/puma/Http11.java +31 -10
  25. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  26. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +157 -53
  27. data/ext/puma_http11/puma_http11.c +21 -10
  28. data/lib/puma/app/status.rb +4 -4
  29. data/lib/puma/binder.rb +60 -55
  30. data/lib/puma/cli.rb +22 -20
  31. data/lib/puma/client.rb +93 -30
  32. data/lib/puma/cluster/worker.rb +27 -17
  33. data/lib/puma/cluster/worker_handle.rb +8 -6
  34. data/lib/puma/cluster.rb +121 -47
  35. data/lib/puma/commonlogger.rb +21 -14
  36. data/lib/puma/configuration.rb +101 -65
  37. data/lib/puma/const.rb +141 -93
  38. data/lib/puma/control_cli.rb +19 -15
  39. data/lib/puma/detect.rb +7 -4
  40. data/lib/puma/dsl.rb +521 -88
  41. data/lib/puma/error_logger.rb +22 -13
  42. data/lib/puma/events.rb +6 -126
  43. data/lib/puma/io_buffer.rb +39 -4
  44. data/lib/puma/jruby_restart.rb +0 -15
  45. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  46. data/lib/puma/launcher.rb +121 -181
  47. data/lib/puma/log_writer.rb +147 -0
  48. data/lib/puma/minissl/context_builder.rb +27 -12
  49. data/lib/puma/minissl.rb +105 -11
  50. data/lib/puma/null_io.rb +42 -2
  51. data/lib/puma/plugin/systemd.rb +90 -0
  52. data/lib/puma/plugin/tmp_restart.rb +1 -1
  53. data/lib/puma/puma_http11.jar +0 -0
  54. data/lib/puma/rack/builder.rb +6 -6
  55. data/lib/puma/rack/urlmap.rb +1 -1
  56. data/lib/puma/rack_default.rb +19 -4
  57. data/lib/puma/reactor.rb +19 -10
  58. data/lib/puma/request.rb +368 -169
  59. data/lib/puma/runner.rb +65 -22
  60. data/lib/puma/sd_notify.rb +146 -0
  61. data/lib/puma/server.rb +161 -102
  62. data/lib/puma/single.rb +13 -11
  63. data/lib/puma/state_file.rb +3 -6
  64. data/lib/puma/thread_pool.rb +71 -21
  65. data/lib/puma/util.rb +1 -12
  66. data/lib/puma.rb +9 -10
  67. data/lib/rack/handler/puma.rb +116 -86
  68. data/tools/Dockerfile +2 -2
  69. metadata +17 -12
  70. data/lib/puma/queue_close.rb +0 -26
  71. data/lib/puma/systemd.rb +0 -46
  72. data/lib/rack/version_restriction.rb +0 -15
data/lib/puma/client.rb CHANGED
@@ -8,9 +8,9 @@ class IO
8
8
  end
9
9
  end
10
10
 
11
- require 'puma/detect'
11
+ require_relative 'detect'
12
+ require_relative 'io_buffer'
12
13
  require 'tempfile'
13
- require 'forwardable'
14
14
 
15
15
  if Puma::IS_JRUBY
16
16
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -25,6 +25,9 @@ module Puma
25
25
 
26
26
  class HttpParserError501 < IOError; end
27
27
 
28
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
29
+
30
+
28
31
  # An instance of this class represents a unique request from a client.
29
32
  # For example, this could be a web request from a browser or from CURL.
30
33
  #
@@ -38,7 +41,7 @@ module Puma
38
41
  # the header and body are fully buffered via the `try_to_finish` method.
39
42
  # They can be used to "time out" a response via the `timeout_at` reader.
40
43
  #
41
- class Client
44
+ class Client # :nodoc:
42
45
 
43
46
  # this tests all values but the last, which must be chunked
44
47
  ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
@@ -66,17 +69,13 @@ module Puma
66
69
  EmptyBody = NullIO.new
67
70
 
68
71
  include Puma::Const
69
- extend Forwardable
70
72
 
71
73
  def initialize(io, env=nil)
72
74
  @io = io
73
75
  @to_io = io.to_io
76
+ @io_buffer = IOBuffer.new
74
77
  @proto_env = env
75
- if !env
76
- @env = nil
77
- else
78
- @env = env.dup
79
- end
78
+ @env = env&.dup
80
79
 
81
80
  @parser = HttpParser.new
82
81
  @parsed_bytes = 0
@@ -94,7 +93,11 @@ module Puma
94
93
  @requests_served = 0
95
94
  @hijacked = false
96
95
 
96
+ @http_content_length_limit = nil
97
+ @http_content_length_limit_exceeded = false
98
+
97
99
  @peerip = nil
100
+ @peer_family = nil
98
101
  @listener = nil
99
102
  @remote_addr_header = nil
100
103
  @expect_proxy_proto = false
@@ -102,16 +105,22 @@ module Puma
102
105
  @body_remain = 0
103
106
 
104
107
  @in_last_chunk = false
108
+
109
+ # need unfrozen ASCII-8BIT, +'' is UTF-8
110
+ @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
105
111
  end
106
112
 
107
113
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
108
- :tempfile
114
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded
109
115
 
110
- attr_writer :peerip
116
+ attr_writer :peerip, :http_content_length_limit
111
117
 
112
118
  attr_accessor :remote_addr_header, :listener
113
119
 
114
- def_delegators :@io, :closed?
120
+ # Remove in Puma 7?
121
+ def closed?
122
+ @to_io.closed?
123
+ end
115
124
 
116
125
  # Test to see if io meets a bare minimum of functioning, @to_io needs to be
117
126
  # used for MiniSSL::Socket
@@ -147,16 +156,16 @@ module Puma
147
156
 
148
157
  def reset(fast_check=true)
149
158
  @parser.reset
159
+ @io_buffer.reset
150
160
  @read_header = true
151
161
  @read_proxy = !!@expect_proxy_proto
152
162
  @env = @proto_env.dup
153
- @body = nil
154
- @tempfile = nil
155
163
  @parsed_bytes = 0
156
164
  @ready = false
157
165
  @body_remain = 0
158
166
  @peerip = nil if @remote_addr_header
159
167
  @in_last_chunk = false
168
+ @http_content_length_limit_exceeded = false
160
169
 
161
170
  if @buffer
162
171
  return false unless try_to_parse_proxy_protocol
@@ -179,11 +188,11 @@ module Puma
179
188
  rescue IOError
180
189
  # swallow it
181
190
  end
182
-
183
191
  end
184
192
  end
185
193
 
186
194
  def close
195
+ tempfile_close
187
196
  begin
188
197
  @io.close
189
198
  rescue IOError, Errno::EBADF
@@ -191,6 +200,15 @@ module Puma
191
200
  end
192
201
  end
193
202
 
203
+ def tempfile_close
204
+ tf_path = @tempfile&.path
205
+ @tempfile&.close
206
+ File.unlink(tf_path) if tf_path
207
+ @tempfile = nil
208
+ @body = nil
209
+ rescue Errno::ENOENT, IOError
210
+ end
211
+
194
212
  # If necessary, read the PROXY protocol from the buffer. Returns
195
213
  # false if more data is needed.
196
214
  def try_to_parse_proxy_protocol
@@ -216,8 +234,20 @@ module Puma
216
234
  end
217
235
 
218
236
  def try_to_finish
237
+ if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
238
+ @http_content_length_limit_exceeded = true
239
+ end
240
+
241
+ if @http_content_length_limit_exceeded
242
+ @buffer = nil
243
+ @body = EmptyBody
244
+ set_ready
245
+ return true
246
+ end
247
+
219
248
  return read_body if in_data_phase
220
249
 
250
+ data = nil
221
251
  begin
222
252
  data = @io.read_nonblock(CHUNK_SIZE)
223
253
  rescue IO::WaitReadable
@@ -245,6 +275,10 @@ module Puma
245
275
 
246
276
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
247
277
 
278
+ if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
279
+ @http_content_length_limit_exceeded = true
280
+ end
281
+
248
282
  if @parser.finished?
249
283
  return setup_body
250
284
  elsif @parsed_bytes >= MAX_HEADER
@@ -282,7 +316,7 @@ module Puma
282
316
  return @peerip if @peerip
283
317
 
284
318
  if @remote_addr_header
285
- hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
319
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
286
320
  @peerip = hdr
287
321
  return hdr
288
322
  end
@@ -290,6 +324,16 @@ module Puma
290
324
  @peerip ||= @io.peeraddr.last
291
325
  end
292
326
 
327
+ def peer_family
328
+ return @peer_family if @peer_family
329
+
330
+ @peer_family ||= begin
331
+ @io.local_address.afamily
332
+ rescue
333
+ Socket::AF_INET
334
+ end
335
+ end
336
+
293
337
  # Returns true if the persistent connection can be closed immediately
294
338
  # without waiting for the configured idle/shutdown timeout.
295
339
  # @version 5.0.0
@@ -313,7 +357,7 @@ module Puma
313
357
  private
314
358
 
315
359
  def setup_body
316
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
360
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
317
361
 
318
362
  if @env[HTTP_EXPECT] == CONTINUE
319
363
  # TODO allow a hook here to check the headers before
@@ -357,7 +401,7 @@ module Puma
357
401
 
358
402
  if cl
359
403
  # cannot contain characters that are not \d, or be empty
360
- if cl =~ CONTENT_LENGTH_VALUE_INVALID || cl.empty?
404
+ if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
361
405
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
362
406
  end
363
407
  else
@@ -367,18 +411,33 @@ module Puma
367
411
  return true
368
412
  end
369
413
 
370
- remain = cl.to_i - body.bytesize
414
+ content_length = cl.to_i
415
+
416
+ remain = content_length - body.bytesize
371
417
 
372
418
  if remain <= 0
373
- @body = StringIO.new(body)
374
- @buffer = nil
419
+ # Part of the body is a pipelined request OR garbage. We'll deal with that later.
420
+ if content_length == 0
421
+ @body = EmptyBody
422
+ if body.empty?
423
+ @buffer = nil
424
+ else
425
+ @buffer = body
426
+ end
427
+ elsif remain == 0
428
+ @body = StringIO.new body
429
+ @buffer = nil
430
+ else
431
+ @body = StringIO.new(body[0,content_length])
432
+ @buffer = body[content_length..-1]
433
+ end
375
434
  set_ready
376
435
  return true
377
436
  end
378
437
 
379
438
  if remain > MAX_BODY
380
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
381
- @body.unlink
439
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
440
+ File.unlink @body.path unless IS_WINDOWS
382
441
  @body.binmode
383
442
  @tempfile = @body
384
443
  else
@@ -410,7 +469,7 @@ module Puma
410
469
  end
411
470
 
412
471
  begin
413
- chunk = @io.read_nonblock(want)
472
+ chunk = @io.read_nonblock(want, @read_buffer)
414
473
  rescue IO::WaitReadable
415
474
  return false
416
475
  rescue SystemCallError, IOError
@@ -442,7 +501,7 @@ module Puma
442
501
  def read_chunked_body
443
502
  while true
444
503
  begin
445
- chunk = @io.read_nonblock(4096)
504
+ chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
446
505
  rescue IO::WaitReadable
447
506
  return false
448
507
  rescue SystemCallError, IOError
@@ -470,8 +529,8 @@ module Puma
470
529
  @prev_chunk = ""
471
530
  @excess_cr = 0
472
531
 
473
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
474
- @body.unlink
532
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
533
+ File.unlink @body.path unless IS_WINDOWS
475
534
  @body.binmode
476
535
  @tempfile = @body
477
536
  @chunked_content_length = 0
@@ -523,7 +582,7 @@ module Puma
523
582
  # Puma doesn't process chunk extensions, but should parse if they're
524
583
  # present, which is the reason for the semicolon regex
525
584
  chunk_hex = line.strip[/\A[^;]+/]
526
- if chunk_hex =~ CHUNK_SIZE_INVALID
585
+ if CHUNK_SIZE_INVALID.match? chunk_hex
527
586
  raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
528
587
  end
529
588
  len = chunk_hex.to_i(16)
@@ -591,7 +650,7 @@ module Puma
591
650
  @partial_part_left = len - part.size
592
651
  end
593
652
  else
594
- if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
653
+ if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
595
654
  raise HttpParserError, "maximum size of chunk header exceeded"
596
655
  end
597
656
 
@@ -610,10 +669,14 @@ module Puma
610
669
 
611
670
  def set_ready
612
671
  if @body_read_start
613
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
672
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
614
673
  end
615
674
  @requests_served += 1
616
675
  @ready = true
617
676
  end
677
+
678
+ def above_http_content_limit(value)
679
+ @http_content_length_limit&.< value
680
+ end
618
681
  end
619
682
  end
@@ -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
@@ -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
 
@@ -83,28 +86,29 @@ module Puma
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
91
+ elsif idx == -2 # refork cycle is done
92
+ @config.run_hooks(:after_refork, nil, @log_writer, @hook_data)
89
93
  elsif idx == 0 # restart server
90
94
  restart_server << true << false
91
95
  else # fork worker
92
96
  worker_pids << pid = spawn_worker(idx)
93
- @worker_write << "f#{pid}:#{idx}\n" rescue nil
97
+ @worker_write << "#{PIPE_FORK}#{pid}:#{idx}\n" rescue nil
94
98
  end
95
99
  end
96
100
  end
97
101
  end
98
102
 
99
103
  Signal.trap "SIGTERM" do
100
- @worker_write << "e#{Process.pid}\n" rescue nil
104
+ @worker_write << "#{PIPE_EXTERNAL_TERM}#{Process.pid}\n" rescue nil
101
105
  restart_server.clear
102
106
  server.stop
103
107
  restart_server << false
104
108
  end
105
109
 
106
110
  begin
107
- @worker_write << "b#{Process.pid}:#{index}\n"
111
+ @worker_write << "#{PIPE_BOOT}#{Process.pid}:#{index}\n"
108
112
  rescue SystemCallError, IOError
109
113
  Puma::Util.purge_interrupt_queue
110
114
  STDERR.puts "Master seems to have exited, exiting."
@@ -113,9 +117,14 @@ module Puma
113
117
 
114
118
  while restart_server.pop
115
119
  server_thread = server.run
120
+
121
+ if @log_writer.debug? && index == 0
122
+ debug_loaded_extensions "Loaded Extensions - worker 0:"
123
+ end
124
+
116
125
  stat_thread ||= Thread.new(@worker_write) do |io|
117
126
  Puma.set_thread_name "stat pld"
118
- base_payload = "p#{Process.pid}"
127
+ base_payload = "#{PIPE_PING}#{Process.pid}"
119
128
 
120
129
  while true
121
130
  begin
@@ -124,7 +133,8 @@ module Puma
124
133
  t = server.pool_capacity || 0
125
134
  m = server.max_threads || 0
126
135
  rc = server.requests_count || 0
127
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
136
+ bt = server.busy_threads || 0
137
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads":#{m}, "requests_count":#{rc}, "busy_threads":#{bt} }\n!
128
138
  io << payload
129
139
  rescue IOError
130
140
  Puma::Util.purge_interrupt_queue
@@ -138,16 +148,16 @@ module Puma
138
148
 
139
149
  # Invoke any worker shutdown hooks so they can prevent the worker
140
150
  # exiting until any background operations are completed
141
- @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
151
+ @config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
142
152
  ensure
143
- @worker_write << "t#{Process.pid}\n" rescue nil
153
+ @worker_write << "#{PIPE_TERM}#{Process.pid}\n" rescue nil
144
154
  @worker_write.close
145
155
  end
146
156
 
147
157
  private
148
158
 
149
159
  def spawn_worker(idx)
150
- @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
160
+ @config.run_hooks(:before_worker_fork, idx, @log_writer, @hook_data)
151
161
 
152
162
  pid = fork do
153
163
  new_worker = Worker.new index: idx,
@@ -165,7 +175,7 @@ module Puma
165
175
  exit! 1
166
176
  end
167
177
 
168
- @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
178
+ @config.run_hooks(:after_worker_fork, idx, @log_writer, @hook_data)
169
179
  pid
170
180
  end
171
181
  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
@@ -48,13 +51,12 @@ module Puma
48
51
  @term
49
52
  end
50
53
 
54
+ STATUS_PATTERN = /{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads":(?<max_threads>\d*), "requests_count":(?<requests_count>\d*), "busy_threads":(?<busy_threads>\d*) }/
55
+ private_constant :STATUS_PATTERN
56
+
51
57
  def ping!(status)
52
58
  @last_checkin = Time.now
53
- captures = status.match(/{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads": (?<max_threads>\d*), "requests_count": (?<requests_count>\d*) }/)
54
- @last_status = captures.names.inject({}) do |hash, key|
55
- hash[key.to_sym] = captures[key].to_i
56
- hash
57
- end
59
+ @last_status = status.match(STATUS_PATTERN).named_captures.map { |c_name, c| [c_name.to_sym, c.to_i] }.to_h
58
60
  end
59
61
 
60
62
  # @see Puma::Cluster#check_workers