puma 6.6.0 → 8.0.2

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +309 -5
  3. data/README.md +41 -42
  4. data/docs/5.0-Upgrade.md +98 -0
  5. data/docs/6.0-Upgrade.md +56 -0
  6. data/docs/7.0-Upgrade.md +52 -0
  7. data/docs/8.0-Upgrade.md +100 -0
  8. data/docs/deployment.md +58 -23
  9. data/docs/fork_worker.md +5 -5
  10. data/docs/grpc.md +62 -0
  11. data/docs/images/favicon.svg +1 -0
  12. data/docs/images/running-puma.svg +1 -0
  13. data/docs/images/standard-logo.svg +1 -0
  14. data/docs/jungle/README.md +1 -1
  15. data/docs/kubernetes.md +11 -16
  16. data/docs/plugins.md +2 -2
  17. data/docs/restart.md +2 -2
  18. data/docs/signals.md +21 -21
  19. data/docs/stats.md +4 -3
  20. data/docs/systemd.md +4 -4
  21. data/ext/puma_http11/extconf.rb +2 -17
  22. data/ext/puma_http11/http11_parser.java.rl +51 -65
  23. data/ext/puma_http11/mini_ssl.c +18 -8
  24. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  25. data/ext/puma_http11/org/jruby/puma/Http11.java +174 -102
  26. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  27. data/ext/puma_http11/puma_http11.c +122 -118
  28. data/lib/puma/app/status.rb +10 -2
  29. data/lib/puma/binder.rb +12 -10
  30. data/lib/puma/cli.rb +4 -6
  31. data/lib/puma/client.rb +205 -131
  32. data/lib/puma/client_env.rb +171 -0
  33. data/lib/puma/cluster/worker.rb +17 -17
  34. data/lib/puma/cluster/worker_handle.rb +38 -7
  35. data/lib/puma/cluster.rb +44 -30
  36. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  37. data/lib/puma/commonlogger.rb +3 -3
  38. data/lib/puma/configuration.rb +173 -58
  39. data/lib/puma/const.rb +11 -11
  40. data/lib/puma/control_cli.rb +7 -3
  41. data/lib/puma/detect.rb +13 -0
  42. data/lib/puma/dsl.rb +225 -108
  43. data/lib/puma/error_logger.rb +3 -1
  44. data/lib/puma/events.rb +25 -10
  45. data/lib/puma/io_buffer.rb +8 -4
  46. data/lib/puma/launcher/bundle_pruner.rb +3 -5
  47. data/lib/puma/launcher.rb +57 -53
  48. data/lib/puma/log_writer.rb +8 -2
  49. data/lib/puma/minissl.rb +0 -1
  50. data/lib/puma/plugin/systemd.rb +3 -3
  51. data/lib/puma/rack/urlmap.rb +1 -1
  52. data/lib/puma/reactor.rb +19 -13
  53. data/lib/puma/{request.rb → response.rb} +56 -212
  54. data/lib/puma/runner.rb +9 -18
  55. data/lib/puma/server.rb +182 -97
  56. data/lib/puma/server_plugin_control.rb +32 -0
  57. data/lib/puma/single.rb +7 -4
  58. data/lib/puma/state_file.rb +3 -2
  59. data/lib/puma/thread_pool.rb +170 -99
  60. data/lib/puma/util.rb +0 -7
  61. data/lib/puma.rb +10 -0
  62. data/lib/rack/handler/puma.rb +3 -3
  63. data/tools/Dockerfile +15 -5
  64. metadata +19 -7
  65. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/events.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Puma
4
4
 
5
5
  # This is an event sink used by `Puma::Server` to handle
6
- # lifecycle events such as :on_booted, :on_restart, and :on_stopped.
6
+ # lifecycle events such as :after_booted, :before_restart, and :after_stopped.
7
7
  # Using `Puma::DSL` it is possible to register callback hooks
8
8
  # for each event type.
9
9
  class Events
@@ -30,28 +30,43 @@ module Puma
30
30
  h
31
31
  end
32
32
 
33
+ def after_booted(&block)
34
+ register(:after_booted, &block)
35
+ end
36
+
37
+ def before_restart(&block)
38
+ register(:before_restart, &block)
39
+ end
40
+
41
+ def after_stopped(&block)
42
+ register(:after_stopped, &block)
43
+ end
44
+
33
45
  def on_booted(&block)
34
- register(:on_booted, &block)
46
+ Puma.deprecate_method_change :on_booted, __callee__, :after_booted
47
+ after_booted(&block)
35
48
  end
36
49
 
37
50
  def on_restart(&block)
38
- register(:on_restart, &block)
51
+ Puma.deprecate_method_change :on_restart, __callee__, :before_restart
52
+ before_restart(&block)
39
53
  end
40
54
 
41
55
  def on_stopped(&block)
42
- register(:on_stopped, &block)
56
+ Puma.deprecate_method_change :on_stopped, __callee__, :after_stopped
57
+ after_stopped(&block)
43
58
  end
44
59
 
45
- def fire_on_booted!
46
- fire(:on_booted)
60
+ def fire_after_booted!
61
+ fire(:after_booted)
47
62
  end
48
63
 
49
- def fire_on_restart!
50
- fire(:on_restart)
64
+ def fire_before_restart!
65
+ fire(:before_restart)
51
66
  end
52
67
 
53
- def fire_on_stopped!
54
- fire(:on_stopped)
68
+ def fire_after_stopped!
69
+ fire(:after_stopped)
55
70
  end
56
71
  end
57
72
  end
@@ -34,13 +34,17 @@ module Puma
34
34
 
35
35
  alias_method :clear, :reset
36
36
 
37
- # before Ruby 2.5, `write` would only take one argument
38
- if RUBY_VERSION >= '2.5' && RUBY_ENGINE != 'truffleruby'
39
- alias_method :append, :write
40
- else
37
+ # Create an `IoBuffer#append` method that accepts multiple strings and writes them
38
+ if RUBY_ENGINE == 'truffleruby'
39
+ # truffleruby (24.2.1, like ruby 3.3.7)
40
+ # StringIO.new.write("a", "b") # => `write': wrong number of arguments (given 2, expected 1) (ArgumentError)
41
41
  def append(*strs)
42
42
  strs.each { |str| write str }
43
43
  end
44
+ else
45
+ # Ruby 3+
46
+ # StringIO.new.write("a", "b") # => 2
47
+ alias_method :append, :write
44
48
  end
45
49
  end
46
50
  end
@@ -28,16 +28,14 @@ module Puma
28
28
 
29
29
  log '* Pruning Bundler environment'
30
30
  home = ENV['GEM_HOME']
31
- bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
32
- bundle_app_config = Bundler.original_env['BUNDLE_APP_CONFIG']
31
+ original_bundle_env = Bundler.original_env.select { |k, v| k.start_with?('BUNDLE_') && v }
33
32
 
34
33
  with_unbundled_env do
35
34
  ENV['GEM_HOME'] = home
36
- ENV['BUNDLE_GEMFILE'] = bundle_gemfile
37
35
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
38
- ENV["BUNDLE_APP_CONFIG"] = bundle_app_config
36
+ original_bundle_env.each { |k, v| ENV[k] = v }
39
37
  args = [Gem.ruby, puma_wild_path, '-I', dirs.join(':')] + @original_argv
40
- # Ruby 2.0+ defaults to true which breaks socket activation
38
+ # Defaults to true which breaks socket activation
41
39
  args += [{:close_others => false}]
42
40
  Kernel.exec(*args)
43
41
  end
data/lib/puma/launcher.rb CHANGED
@@ -22,12 +22,15 @@ module Puma
22
22
  #
23
23
  # +conf+ A Puma::Configuration object indicating how to run the server.
24
24
  #
25
- # +launcher_args+ A Hash that currently has one required key `:events`,
26
- # this is expected to hold an object similar to an `Puma::LogWriter.stdio`,
27
- # this object will be responsible for broadcasting Puma's internal state
28
- # to a logging destination. An optional key `:argv` can be supplied,
29
- # this should be an array of strings, these arguments are re-used when
30
- # restarting the puma server.
25
+ # +launcher_args+ A Hash that has a few optional keys.
26
+ # - +:log_writer+:: Expected to hold an object similar to `Puma::LogWriter.stdio`.
27
+ # This object will be responsible for broadcasting Puma's internal state
28
+ # to a logging destination.
29
+ # - +:events+:: Expected to hold an object similar to `Puma::Events`.
30
+ # - +:argv+:: Expected to be an array of strings.
31
+ # - +:env+:: Expected to hold a hash of environment variables.
32
+ #
33
+ # These arguments are re-used when restarting the puma server.
31
34
  #
32
35
  # Examples:
33
36
  #
@@ -39,27 +42,44 @@ module Puma
39
42
  # end
40
43
  # Puma::Launcher.new(conf, log_writer: Puma::LogWriter.stdio).run
41
44
  def initialize(conf, launcher_args={})
42
- @runner = nil
43
- @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
44
- @events = launcher_args[:events] || Events.new
45
- @argv = launcher_args[:argv] || []
45
+ ## Minimal initialization before potential early restart (e.g. from bundle pruning)
46
+
47
+ @config = conf
48
+ # Advertise the CLI Configuration before config files are loaded
49
+ Puma.cli_config = @config if defined?(Puma.cli_config)
50
+ @config.clamp
51
+
52
+ @options = @config.options
53
+
54
+ @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
55
+ @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
56
+ @log_writer.formatter = @options[:log_formatter] if @options[:log_formatter]
57
+ @log_writer.custom_logger = @options[:custom_logger] if @options[:custom_logger]
58
+ @options[:log_writer] = @log_writer
59
+ @options[:logger] = @log_writer if clustered?
60
+
61
+ @events = launcher_args[:events] || Events.new
62
+
63
+ @argv = launcher_args[:argv] || []
46
64
  @original_argv = @argv.dup
47
- @config = conf
48
65
 
49
- env = launcher_args.delete(:env) || ENV
66
+ ## End minimal initialization
50
67
 
51
- @config.options[:log_writer] = @log_writer
68
+ generate_restart_data
69
+ Dir.chdir(@restart_dir)
52
70
 
53
- # Advertise the Configuration
54
- Puma.cli_config = @config if defined?(Puma.cli_config)
71
+ prune_bundler!
72
+
73
+ env = launcher_args.delete(:env) || ENV
55
74
 
56
- @config.load
75
+ # Log after prune_bundler! to avoid duplicate logging if a restart occurs
76
+ log_config if env['PUMA_LOG_CONFIG']
57
77
 
58
- @binder = Binder.new(@log_writer, conf)
59
- @binder.create_inherited_fds(ENV).each { |k| ENV.delete k }
60
- @binder.create_activated_fds(ENV).each { |k| ENV.delete k }
78
+ @binder = Binder.new(@log_writer, @options)
79
+ @binder.create_inherited_fds(env).each { |k| env.delete k }
80
+ @binder.create_activated_fds(env).each { |k| env.delete k }
61
81
 
62
- @environment = conf.environment
82
+ @environment = @config.environment
63
83
 
64
84
  # Load the systemd integration if we detect systemd's NOTIFY_SOCKET.
65
85
  # Skip this on JRuby though, because it is incompatible with the systemd
@@ -68,37 +88,21 @@ module Puma
68
88
  @config.plugins.create('systemd')
69
89
  end
70
90
 
71
- if @config.options[:bind_to_activated_sockets]
72
- @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
73
- @config.options[:binds],
74
- @config.options[:bind_to_activated_sockets] == 'only'
91
+ if @options[:bind_to_activated_sockets]
92
+ @options[:binds] = @binder.synthesize_binds_from_activated_fs(
93
+ @options[:binds],
94
+ @options[:bind_to_activated_sockets] == 'only'
75
95
  )
76
96
  end
77
97
 
78
- @options = @config.options
79
- @config.clamp
80
-
81
- @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
82
- @log_writer.formatter = options[:log_formatter] if @options[:log_formatter]
83
-
84
- @log_writer.custom_logger = options[:custom_logger] if @options[:custom_logger]
85
-
86
- generate_restart_data
87
-
88
98
  if clustered? && !Puma.forkable?
89
99
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
90
100
  end
91
101
 
92
- Dir.chdir(@restart_dir)
93
-
94
- prune_bundler!
95
-
96
102
  @environment = @options[:environment] if @options[:environment]
97
103
  set_rack_environment
98
104
 
99
105
  if clustered?
100
- @options[:logger] = @log_writer
101
-
102
106
  @runner = Cluster.new(self)
103
107
  else
104
108
  @runner = Single.new(self)
@@ -106,8 +110,6 @@ module Puma
106
110
  Puma.stats_object = @runner
107
111
 
108
112
  @status = :run
109
-
110
- log_config if env['PUMA_LOG_CONFIG']
111
113
  end
112
114
 
113
115
  attr_reader :binder, :log_writer, :events, :config, :options, :restart_dir
@@ -140,7 +142,10 @@ module Puma
140
142
  # Delete the configured pidfile
141
143
  def delete_pidfile
142
144
  path = @options[:pidfile]
143
- File.unlink(path) if path && File.exist?(path)
145
+ begin
146
+ File.unlink(path) if path
147
+ rescue Errno::ENOENT
148
+ end
144
149
  end
145
150
 
146
151
  # Begin async shutdown of the server
@@ -277,7 +282,7 @@ module Puma
277
282
  end
278
283
 
279
284
  def do_graceful_stop
280
- @events.fire_on_stopped!
285
+ @events.fire_after_stopped!
281
286
  @runner.stop_blocked
282
287
  end
283
288
 
@@ -289,8 +294,8 @@ module Puma
289
294
  end
290
295
 
291
296
  def restart!
292
- @events.fire_on_restart!
293
- @config.run_hooks :on_restart, self, @log_writer
297
+ @events.fire_before_restart!
298
+ @config.run_hooks :before_restart, self, @log_writer
294
299
 
295
300
  if Puma.jruby?
296
301
  close_binder_listeners
@@ -382,9 +387,9 @@ module Puma
382
387
  # using it.
383
388
  @restart_dir = Dir.pwd
384
389
 
385
- # Use the same trick as unicorn, namely favor PWD because
386
- # it will contain an unresolved symlink, useful for when
387
- # the pwd is /data/releases/current.
390
+ # Use the same trick as unicorn, namely favor PWD because
391
+ # it will contain an unresolved symlink, useful for when
392
+ # the pwd is /data/releases/current.
388
393
  elsif dir = ENV['PWD']
389
394
  s_env = File.stat(dir)
390
395
  s_pwd = File.stat(Dir.pwd)
@@ -471,8 +476,8 @@ module Puma
471
476
  end
472
477
 
473
478
  begin
474
- unless Puma.jruby? # INFO in use by JVM already
475
- Signal.trap "SIGINFO" do
479
+ if Puma.backtrace_signal
480
+ Signal.trap Puma.backtrace_signal do
476
481
  thread_status do |name, backtrace|
477
482
  @log_writer.log(name)
478
483
  @log_writer.log(backtrace.map { |bt| " #{bt}" })
@@ -480,8 +485,7 @@ module Puma
480
485
  end
481
486
  end
482
487
  rescue Exception
483
- # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
484
- # to see this constantly on Linux.
488
+ log "*** SIGINFO/SIGPWR not implemented, signal based backtrace unavailable!"
485
489
  end
486
490
  end
487
491
 
@@ -90,8 +90,14 @@ module Puma
90
90
  @debug
91
91
  end
92
92
 
93
- def debug(str)
94
- log("% #{str}") if @debug
93
+ def debug(str=nil, &block)
94
+ if @debug
95
+ if block_given?
96
+ log("% #{yield}")
97
+ else
98
+ log("% #{str}")
99
+ end
100
+ end
95
101
  end
96
102
 
97
103
  # Write +str+ to +@stderr+
data/lib/puma/minissl.rb CHANGED
@@ -172,7 +172,6 @@ module Puma
172
172
  end
173
173
  end
174
174
  rescue IOError, SystemCallError
175
- Puma::Util.purge_interrupt_queue
176
175
  # nothing
177
176
  ensure
178
177
  @socket.close
@@ -14,9 +14,9 @@ Puma::Plugin.create do
14
14
  launcher.log_writer.log "* Enabling systemd notification integration"
15
15
 
16
16
  # hook_events
17
- launcher.events.on_booted { Puma::SdNotify.ready }
18
- launcher.events.on_stopped { Puma::SdNotify.stopping }
19
- launcher.events.on_restart { Puma::SdNotify.reloading }
17
+ launcher.events.after_booted { Puma::SdNotify.ready }
18
+ launcher.events.after_stopped { Puma::SdNotify.stopping }
19
+ launcher.events.before_restart { Puma::SdNotify.reloading }
20
20
 
21
21
  # start watchdog
22
22
  if Puma::SdNotify.watchdog?
@@ -70,7 +70,7 @@ module Puma::Rack
70
70
  return app.call(env)
71
71
  end
72
72
 
73
- [404, {'Content-Type' => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
73
+ [404, {'content-type' => "text/plain", "x-cascade" => "pass"}, ["Not Found: #{path}"]]
74
74
 
75
75
  ensure
76
76
  env['PATH_INFO'] = path
data/lib/puma/reactor.rb CHANGED
@@ -15,6 +15,12 @@ module Puma
15
15
  #
16
16
  # The implementation uses a Queue to synchronize adding new objects from the internal select loop.
17
17
  class Reactor
18
+
19
+ # @!attribute [rw] reactor_max
20
+ # Maximum number of clients in the selector. Reset with calls to `Server.stats`.
21
+ attr_accessor :reactor_max
22
+ attr_reader :reactor_size
23
+
18
24
  # Create a new Reactor to monitor IO objects added by #add.
19
25
  # The provided block will be invoked when an IO has data available to read,
20
26
  # its timeout elapses, or when the Reactor shuts down.
@@ -29,6 +35,8 @@ module Puma
29
35
  @input = Queue.new
30
36
  @timeouts = []
31
37
  @block = block
38
+ @reactor_size = 0
39
+ @reactor_max = 0
32
40
  end
33
41
 
34
42
  # Run the internal select loop, using a background thread by default.
@@ -67,17 +75,18 @@ module Puma
67
75
  private
68
76
 
69
77
  def select_loop
70
- close_selector = true
71
78
  begin
72
79
  until @input.closed? && @input.empty?
73
80
  # Wakeup any registered object that receives incoming data.
74
81
  # Block until the earliest timeout or Selector#wakeup is called.
75
82
  timeout = (earliest = @timeouts.first) && earliest.timeout
76
- @selector.select(timeout) {|mon| wakeup!(mon.value)}
83
+ @selector.select(timeout) do |monitor|
84
+ wakeup!(monitor.value)
85
+ end
77
86
 
78
87
  # Wakeup all objects that timed out.
79
- timed_out = @timeouts.take_while {|t| t.timeout == 0}
80
- timed_out.each { |c| wakeup! c }
88
+ timed_out = @timeouts.take_while { |client| client.timeout == 0 }
89
+ timed_out.each { |client| wakeup!(client) }
81
90
 
82
91
  unless @input.empty?
83
92
  until @input.empty?
@@ -91,23 +100,19 @@ module Puma
91
100
  STDERR.puts "Error in reactor loop escaped: #{e.message} (#{e.class})"
92
101
  STDERR.puts e.backtrace
93
102
 
94
- # NoMethodError may be rarely raised when calling @selector.select, which
95
- # is odd. Regardless, it may continue for thousands of calls if retried.
96
- # Also, when it raises, @selector.close also raises an error.
97
- if NoMethodError === e
98
- close_selector = false
99
- else
100
- retry
101
- end
103
+ retry
102
104
  end
105
+
103
106
  # Wakeup all remaining objects on shutdown.
104
107
  @timeouts.each(&@block)
105
- @selector.close if close_selector
108
+ @selector.close
106
109
  end
107
110
 
108
111
  # Start monitoring the object.
109
112
  def register(client)
110
113
  @selector.register(client.to_io, :r).value = client
114
+ @reactor_size += 1
115
+ @reactor_max = @reactor_size if @reactor_max < @reactor_size
111
116
  @timeouts << client
112
117
  rescue ArgumentError
113
118
  # unreadable clients raise error when processed by NIO
@@ -118,6 +123,7 @@ module Puma
118
123
  def wakeup!(client)
119
124
  if @block.call client
120
125
  @selector.deregister client.to_io
126
+ @reactor_size -= 1
121
127
  @timeouts.delete client
122
128
  end
123
129
  end