puma 6.4.1 → 7.2.1

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +407 -8
  3. data/README.md +109 -49
  4. data/docs/deployment.md +58 -23
  5. data/docs/fork_worker.md +11 -1
  6. data/docs/java_options.md +54 -0
  7. data/docs/jungle/README.md +1 -1
  8. data/docs/kubernetes.md +11 -16
  9. data/docs/plugins.md +6 -2
  10. data/docs/restart.md +2 -2
  11. data/docs/signals.md +21 -21
  12. data/docs/stats.md +11 -5
  13. data/docs/systemd.md +14 -5
  14. data/ext/puma_http11/extconf.rb +20 -32
  15. data/ext/puma_http11/mini_ssl.c +29 -9
  16. data/ext/puma_http11/org/jruby/puma/Http11.java +40 -9
  17. data/ext/puma_http11/puma_http11.c +125 -118
  18. data/lib/puma/app/status.rb +11 -3
  19. data/lib/puma/binder.rb +21 -11
  20. data/lib/puma/cli.rb +10 -8
  21. data/lib/puma/client.rb +183 -83
  22. data/lib/puma/cluster/worker.rb +24 -21
  23. data/lib/puma/cluster/worker_handle.rb +38 -8
  24. data/lib/puma/cluster.rb +73 -47
  25. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  26. data/lib/puma/commonlogger.rb +3 -3
  27. data/lib/puma/configuration.rb +131 -60
  28. data/lib/puma/const.rb +31 -12
  29. data/lib/puma/control_cli.rb +10 -6
  30. data/lib/puma/detect.rb +2 -0
  31. data/lib/puma/dsl.rb +411 -121
  32. data/lib/puma/error_logger.rb +7 -5
  33. data/lib/puma/events.rb +25 -10
  34. data/lib/puma/io_buffer.rb +8 -4
  35. data/lib/puma/jruby_restart.rb +0 -16
  36. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  37. data/lib/puma/launcher.rb +73 -55
  38. data/lib/puma/log_writer.rb +9 -9
  39. data/lib/puma/minissl/context_builder.rb +1 -0
  40. data/lib/puma/minissl.rb +1 -1
  41. data/lib/puma/null_io.rb +26 -0
  42. data/lib/puma/plugin/systemd.rb +3 -3
  43. data/lib/puma/rack/urlmap.rb +1 -1
  44. data/lib/puma/reactor.rb +19 -13
  45. data/lib/puma/request.rb +71 -39
  46. data/lib/puma/runner.rb +15 -17
  47. data/lib/puma/sd_notify.rb +1 -4
  48. data/lib/puma/server.rb +134 -73
  49. data/lib/puma/single.rb +7 -4
  50. data/lib/puma/state_file.rb +3 -2
  51. data/lib/puma/thread_pool.rb +57 -80
  52. data/lib/puma/util.rb +0 -7
  53. data/lib/puma.rb +10 -0
  54. data/lib/rack/handler/puma.rb +10 -7
  55. data/tools/Dockerfile +15 -5
  56. metadata +14 -15
  57. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/server.rb CHANGED
@@ -12,12 +12,13 @@ require_relative 'client'
12
12
  require_relative 'binder'
13
13
  require_relative 'util'
14
14
  require_relative 'request'
15
+ require_relative 'configuration'
16
+ require_relative 'cluster_accept_loop_delay'
15
17
 
16
18
  require 'socket'
17
19
  require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
18
20
 
19
21
  module Puma
20
-
21
22
  # The HTTP Server itself. Serves out a single Rack app.
22
23
  #
23
24
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
@@ -29,9 +30,18 @@ module Puma
29
30
  #
30
31
  # Each `Puma::Server` will have one reactor and one thread pool.
31
32
  class Server
33
+ module FiberPerRequest
34
+ def handle_request(client, requests)
35
+ Fiber.new do
36
+ super
37
+ end.resume
38
+ end
39
+ end
40
+
32
41
  include Puma::Const
33
42
  include Request
34
43
 
44
+ attr_reader :options
35
45
  attr_reader :thread
36
46
  attr_reader :log_writer
37
47
  attr_reader :events
@@ -46,8 +56,6 @@ module Puma
46
56
  attr_accessor :app
47
57
  attr_accessor :binder
48
58
 
49
- THREAD_LOCAL_KEY = :puma_server
50
-
51
59
  # Create a server for the rack app +app+.
52
60
  #
53
61
  # +log_writer+ is a Puma::LogWriter object used to log info and error messages.
@@ -74,6 +82,9 @@ module Puma
74
82
 
75
83
  @thread = nil
76
84
  @thread_pool = nil
85
+ @reactor = nil
86
+
87
+ @env_set_http_version = nil
77
88
 
78
89
  @options = if options.is_a?(UserFileDefaultOptions)
79
90
  options
@@ -91,9 +102,19 @@ module Puma
91
102
  @min_threads = @options[:min_threads]
92
103
  @max_threads = @options[:max_threads]
93
104
  @queue_requests = @options[:queue_requests]
94
- @max_fast_inline = @options[:max_fast_inline]
105
+ @max_keep_alive = @options[:max_keep_alive]
106
+ @enable_keep_alives = @options[:enable_keep_alives]
107
+ @enable_keep_alives &&= @queue_requests
95
108
  @io_selector_backend = @options[:io_selector_backend]
96
109
  @http_content_length_limit = @options[:http_content_length_limit]
110
+ @cluster_accept_loop_delay = ClusterAcceptLoopDelay.new(
111
+ workers: @options[:workers],
112
+ max_delay: @options[:wait_for_less_busy_worker] || 0 # Real default is in Configuration::DEFAULTS, this is for unit testing
113
+ )
114
+
115
+ if @options[:fiber_per_request]
116
+ singleton_class.prepend(FiberPerRequest)
117
+ end
97
118
 
98
119
  # make this a hash, since we prefer `key?` over `include?`
99
120
  @supported_http_methods =
@@ -110,7 +131,7 @@ module Puma
110
131
  temp = !!(@options[:environment] =~ /\A(development|test)\z/)
111
132
  @leak_stack_on_error = @options[:environment] ? temp : true
112
133
 
113
- @binder = Binder.new(log_writer)
134
+ @binder = Binder.new(log_writer, @options)
114
135
 
115
136
  ENV['RACK_ENV'] ||= "development"
116
137
 
@@ -130,7 +151,7 @@ module Puma
130
151
  class << self
131
152
  # @!attribute [r] current
132
153
  def current
133
- Thread.current[THREAD_LOCAL_KEY]
154
+ Thread.current.puma_server
134
155
  end
135
156
 
136
157
  # :nodoc:
@@ -161,7 +182,6 @@ module Puma
161
182
  begin
162
183
  skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 1) if skt.kind_of? TCPSocket
163
184
  rescue IOError, SystemCallError
164
- Puma::Util.purge_interrupt_queue
165
185
  end
166
186
  end
167
187
 
@@ -170,7 +190,6 @@ module Puma
170
190
  begin
171
191
  skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 0) if skt.kind_of? TCPSocket
172
192
  rescue IOError, SystemCallError
173
- Puma::Util.purge_interrupt_queue
174
193
  end
175
194
  end
176
195
  else
@@ -191,7 +210,6 @@ module Puma
191
210
  begin
192
211
  tcp_info = skt.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO)
193
212
  rescue IOError, SystemCallError
194
- Puma::Util.purge_interrupt_queue
195
213
  @precheck_closing = false
196
214
  false
197
215
  else
@@ -216,7 +234,6 @@ module Puma
216
234
  @thread_pool&.spawned
217
235
  end
218
236
 
219
-
220
237
  # This number represents the number of requests that
221
238
  # the server is capable of taking right now.
222
239
  #
@@ -242,16 +259,19 @@ module Puma
242
259
 
243
260
  @status = :run
244
261
 
245
- @thread_pool = ThreadPool.new(thread_name, @options) { |client| process_client client }
262
+ @thread_pool = ThreadPool.new(thread_name, options, server: self) { |client| process_client client }
246
263
 
247
264
  if @queue_requests
248
- @reactor = Reactor.new(@io_selector_backend) { |c| reactor_wakeup c }
265
+ @reactor = Reactor.new(@io_selector_backend) { |c|
266
+ # Inversion of control, the reactor is calling a method on the server when it
267
+ # is done buffering a request or receives a new request from a keepalive connection.
268
+ self.reactor_wakeup(c)
269
+ }
249
270
  @reactor.run
250
271
  end
251
272
 
252
-
253
- @thread_pool.auto_reap! if @options[:reaping_time]
254
- @thread_pool.auto_trim! if @options[:auto_trim_time]
273
+ @thread_pool.auto_reap! if options[:reaping_time]
274
+ @thread_pool.auto_trim! if @min_threads != @max_threads && options[:auto_trim_time]
255
275
 
256
276
  @check, @notify = Puma::Util.pipe unless @notify
257
277
 
@@ -271,12 +291,15 @@ module Puma
271
291
  # This method is called from the Reactor thread when a queued Client receives data,
272
292
  # times out, or when the Reactor is shutting down.
273
293
  #
294
+ # While the code lives in the Server, the logic is executed on the reactor thread, independently
295
+ # from the server.
296
+ #
274
297
  # It is responsible for ensuring that a request has been completely received
275
298
  # before it starts to be processed by the ThreadPool. This may be known as read buffering.
276
299
  # If read buffering is not done, and no other read buffering is performed (such as by an application server
277
300
  # such as nginx) then the application would be subject to a slow client attack.
278
301
  #
279
- # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline).
302
+ # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/main/docs/architecture.md).
280
303
  #
281
304
  # The method checks to see if it has the full header and body with
282
305
  # the `Puma::Client#try_to_finish` method. If the full request has been sent,
@@ -305,25 +328,28 @@ module Puma
305
328
  end
306
329
  rescue StandardError => e
307
330
  client_error(e, client)
308
- client.close
331
+ close_client_safely(client)
309
332
  true
310
333
  end
311
334
 
312
335
  def handle_servers
336
+ @env_set_http_version = Object.const_defined?(:Rack) && ::Rack.respond_to?(:release) &&
337
+ Gem::Version.new(::Rack.release) < Gem::Version.new('3.1.0')
338
+
313
339
  begin
314
340
  check = @check
315
341
  sockets = [check] + @binder.ios
316
342
  pool = @thread_pool
317
343
  queue_requests = @queue_requests
318
- drain = @options[:drain_on_shutdown] ? 0 : nil
344
+ drain = options[:drain_on_shutdown] ? 0 : nil
319
345
 
320
- addr_send_name, addr_value = case @options[:remote_address]
346
+ addr_send_name, addr_value = case options[:remote_address]
321
347
  when :value
322
- [:peerip=, @options[:remote_address_value]]
348
+ [:peerip=, options[:remote_address_value]]
323
349
  when :header
324
- [:remote_addr_header=, @options[:remote_address_header]]
350
+ [:remote_addr_header=, options[:remote_address_header]]
325
351
  when :proxy_protocol
326
- [:expect_proxy_proto=, @options[:remote_address_proxy_protocol]]
352
+ [:expect_proxy_proto=, options[:remote_address_proxy_protocol]]
327
353
  else
328
354
  [nil, nil]
329
355
  end
@@ -336,7 +362,7 @@ module Puma
336
362
  @idle_timeout_reached = true
337
363
 
338
364
  if @clustered
339
- @worker_write << "i#{Process.pid}\n" rescue nil
365
+ @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
340
366
  next
341
367
  else
342
368
  @log_writer.log "- Idle timeout reached"
@@ -349,15 +375,25 @@ module Puma
349
375
 
350
376
  if @idle_timeout_reached && @clustered
351
377
  @idle_timeout_reached = false
352
- @worker_write << "i#{Process.pid}\n" rescue nil
378
+ @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
353
379
  end
354
380
 
355
381
  ios.first.each do |sock|
356
382
  if sock == check
357
383
  break if handle_check
358
384
  else
359
- pool.wait_until_not_full
360
- pool.wait_for_less_busy_worker(@options[:wait_for_less_busy_worker])
385
+ # if ThreadPool out_of_band code is running, we don't want to add
386
+ # clients until the code is finished.
387
+ pool.wait_while_out_of_band_running
388
+
389
+ # A well rested herd (cluster) runs faster
390
+ if @cluster_accept_loop_delay.on? && (busy_threads_plus_todo = pool.busy_threads) > 0
391
+ delay = @cluster_accept_loop_delay.calculate(
392
+ max_threads: @max_threads,
393
+ busy_threads_plus_todo: busy_threads_plus_todo
394
+ )
395
+ sleep(delay)
396
+ end
361
397
 
362
398
  io = begin
363
399
  sock.accept_nonblock
@@ -365,11 +401,9 @@ module Puma
365
401
  next
366
402
  end
367
403
  drain += 1 if shutting_down?
368
- pool << Client.new(io, @binder.env(sock)).tap { |c|
369
- c.listener = sock
370
- c.http_content_length_limit = @http_content_length_limit
371
- c.send(addr_send_name, addr_value) if addr_value
372
- }
404
+ client = new_client(io, sock)
405
+ client.send(addr_send_name, addr_value) if addr_value
406
+ pool << client
373
407
  end
374
408
  end
375
409
  rescue IOError, Errno::EBADF
@@ -406,6 +440,14 @@ module Puma
406
440
  @events.fire :state, :done
407
441
  end
408
442
 
443
+ # :nodoc:
444
+ def new_client(io, sock)
445
+ client = Client.new(io, @binder.env(sock))
446
+ client.listener = sock
447
+ client.http_content_length_limit = @http_content_length_limit
448
+ client
449
+ end
450
+
409
451
  # :nodoc:
410
452
  def handle_check
411
453
  cmd = @check.read(1)
@@ -436,17 +478,12 @@ module Puma
436
478
  #
437
479
  # Return true if one or more requests were processed.
438
480
  def process_client(client)
439
- # Advertise this server into the thread
440
- Thread.current[THREAD_LOCAL_KEY] = self
441
-
442
- clean_thread_locals = @options[:clean_thread_locals]
443
481
  close_socket = true
444
482
 
445
483
  requests = 0
446
484
 
447
485
  begin
448
- if @queue_requests &&
449
- !client.eagerly_finish
486
+ if @queue_requests && !client.eagerly_finish
450
487
 
451
488
  client.set_timeout(@first_data_timeout)
452
489
  if @reactor.add client
@@ -459,38 +496,40 @@ module Puma
459
496
  client.finish(@first_data_timeout)
460
497
  end
461
498
 
462
- while true
499
+ can_loop = true
500
+ while can_loop
501
+ can_loop = false
463
502
  @requests_count += 1
464
503
  case handle_request(client, requests + 1)
465
- when false
466
- break
504
+ when :close
467
505
  when :async
468
506
  close_socket = false
469
- break
470
- when true
471
- ThreadPool.clean_thread_locals if clean_thread_locals
472
-
507
+ when :keep_alive
473
508
  requests += 1
474
509
 
475
- # As an optimization, try to read the next request from the
476
- # socket for a short time before returning to the reactor.
477
- fast_check = @status == :run
510
+ client.reset
478
511
 
479
- # Always pass the client back to the reactor after a reasonable
480
- # number of inline requests if there are other requests pending.
481
- fast_check = false if requests >= @max_fast_inline &&
482
- @thread_pool.backlog > 0
483
-
484
- next_request_ready = with_force_shutdown(client) do
485
- client.reset(fast_check)
512
+ # This indicates data exists in the client read buffer and there may be
513
+ # additional requests on it, so process them
514
+ next_request_ready = if client.has_back_to_back_requests?
515
+ with_force_shutdown(client) { client.process_back_to_back_requests }
516
+ else
517
+ with_force_shutdown(client) { client.eagerly_finish }
486
518
  end
487
519
 
488
- unless next_request_ready
489
- break unless @queue_requests
520
+ if next_request_ready
521
+ # When Puma has spare threads, allow this one to be monopolized
522
+ # Perf optimization for https://github.com/puma/puma/issues/3788
523
+ if @thread_pool.waiting > 0
524
+ can_loop = true
525
+ else
526
+ @thread_pool << client
527
+ close_socket = false
528
+ end
529
+ elsif @queue_requests
490
530
  client.set_timeout @persistent_timeout
491
531
  if @reactor.add client
492
532
  close_socket = false
493
- break
494
533
  end
495
534
  end
496
535
  end
@@ -503,17 +542,21 @@ module Puma
503
542
  ensure
504
543
  client.io_buffer.reset
505
544
 
506
- begin
507
- client.close if close_socket
508
- rescue IOError, SystemCallError
509
- Puma::Util.purge_interrupt_queue
510
- # Already closed
511
- rescue StandardError => e
512
- @log_writer.unknown_error e, nil, "Client"
513
- end
545
+ close_client_safely(client) if close_socket
514
546
  end
515
547
  end
516
548
 
549
+ # :nodoc:
550
+ def close_client_safely(client)
551
+ client.close
552
+ rescue IOError, SystemCallError
553
+ # Already closed
554
+ rescue MiniSSL::SSLError => e
555
+ @log_writer.ssl_error e, client.io
556
+ rescue StandardError => e
557
+ @log_writer.unknown_error e, nil, "Client"
558
+ end
559
+
517
560
  # Triggers a client timeout if the thread-pool shuts down
518
561
  # during execution of the provided block.
519
562
  def with_force_shutdown(client, &block)
@@ -548,7 +591,7 @@ module Puma
548
591
  # A fallback rack response if +@app+ raises as exception.
549
592
  #
550
593
  def lowlevel_error(e, env, status=500)
551
- if handler = @options[:lowlevel_error_handler]
594
+ if handler = options[:lowlevel_error_handler]
552
595
  if handler.arity == 1
553
596
  return handler.call(e)
554
597
  elsif handler.arity == 2
@@ -569,14 +612,13 @@ module Puma
569
612
  def response_to_error(client, requests, err, status_code)
570
613
  status, headers, res_body = lowlevel_error(err, client.env, status_code)
571
614
  prepare_response(status, headers, res_body, requests, client)
572
- client.write_error(status_code)
573
615
  end
574
616
  private :response_to_error
575
617
 
576
618
  # Wait for all outstanding requests to finish.
577
619
  #
578
620
  def graceful_shutdown
579
- if @options[:shutdown_debug]
621
+ if options[:shutdown_debug]
580
622
  threads = Thread.list
581
623
  total = threads.size
582
624
 
@@ -596,7 +638,7 @@ module Puma
596
638
  end
597
639
 
598
640
  if @thread_pool
599
- if timeout = @options[:force_shutdown_after]
641
+ if timeout = options[:force_shutdown_after]
600
642
  @thread_pool.shutdown timeout.to_f
601
643
  else
602
644
  @thread_pool.shutdown
@@ -608,11 +650,10 @@ module Puma
608
650
  @notify << message
609
651
  rescue IOError, NoMethodError, Errno::EPIPE, Errno::EBADF
610
652
  # The server, in another thread, is shutting down
611
- Puma::Util.purge_interrupt_queue
612
653
  rescue RuntimeError => e
613
654
  # Temporary workaround for https://bugs.ruby-lang.org/issues/13239
614
655
  if e.message.include?('IOError')
615
- Puma::Util.purge_interrupt_queue
656
+ # ignore
616
657
  else
617
658
  raise e
618
659
  end
@@ -643,13 +684,33 @@ module Puma
643
684
 
644
685
  # List of methods invoked by #stats.
645
686
  # @version 5.0.0
646
- STAT_METHODS = [:backlog, :running, :pool_capacity, :max_threads, :requests_count].freeze
687
+ STAT_METHODS = [
688
+ :backlog,
689
+ :running,
690
+ :pool_capacity,
691
+ :busy_threads,
692
+ :backlog_max,
693
+ :max_threads,
694
+ :requests_count,
695
+ :reactor_max,
696
+ ].freeze
647
697
 
648
698
  # Returns a hash of stats about the running server for reporting purposes.
649
699
  # @version 5.0.0
650
700
  # @!attribute [r] stats
701
+ # @return [Hash] hash containing stat info from `Server` and `ThreadPool`
651
702
  def stats
652
- STAT_METHODS.map {|name| [name, send(name) || 0]}.to_h
703
+ stats = @thread_pool&.stats || {}
704
+ stats[:max_threads] = @max_threads
705
+ stats[:requests_count] = @requests_count
706
+ stats[:reactor_max] = @reactor.reactor_max if @reactor
707
+ reset_max
708
+ stats
709
+ end
710
+
711
+ def reset_max
712
+ @reactor.reactor_max = 0 if @reactor
713
+ @thread_pool&.reset_max
653
714
  end
654
715
 
655
716
  # below are 'delegations' to binder
data/lib/puma/single.rb CHANGED
@@ -17,7 +17,7 @@ module Puma
17
17
  def stats
18
18
  {
19
19
  started_at: utc_iso8601(@started_at)
20
- }.merge(@server.stats).merge(super)
20
+ }.merge(@server&.stats || {}).merge(super)
21
21
  end
22
22
 
23
23
  def restart
@@ -49,13 +49,16 @@ module Puma
49
49
 
50
50
  start_control
51
51
 
52
- @server = server = start_server
53
- server_thread = server.run
52
+ @server = start_server
53
+ server_thread = @server.run
54
54
 
55
55
  log "Use Ctrl-C to stop"
56
+
57
+ warn_ruby_mn_threads
58
+
56
59
  redirect_io
57
60
 
58
- @events.fire_on_booted!
61
+ @events.fire_after_booted!
59
62
 
60
63
  debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
61
64
 
@@ -32,10 +32,11 @@ module Puma
32
32
  "#{k}: \"#{v}\"\n" : "#{k}: #{v}\n")
33
33
  end
34
34
  end
35
+
35
36
  if permission
36
- File.write path, contents, mode: 'wb:UTF-8'
37
- else
38
37
  File.write path, contents, mode: 'wb:UTF-8', perm: permission
38
+ else
39
+ File.write path, contents, mode: 'wb:UTF-8'
39
40
  end
40
41
  end
41
42