puma 6.4.3 → 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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +448 -8
  3. data/README.md +110 -51
  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 +11 -1
  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/java_options.md +54 -0
  15. data/docs/jungle/README.md +1 -1
  16. data/docs/kubernetes.md +11 -16
  17. data/docs/plugins.md +6 -2
  18. data/docs/restart.md +2 -2
  19. data/docs/signals.md +21 -21
  20. data/docs/stats.md +11 -5
  21. data/docs/systemd.md +14 -5
  22. data/ext/puma_http11/extconf.rb +20 -32
  23. data/ext/puma_http11/http11_parser.java.rl +51 -65
  24. data/ext/puma_http11/mini_ssl.c +29 -9
  25. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  26. data/ext/puma_http11/org/jruby/puma/Http11.java +194 -101
  27. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  28. data/ext/puma_http11/puma_http11.c +125 -118
  29. data/lib/puma/app/status.rb +11 -3
  30. data/lib/puma/binder.rb +22 -12
  31. data/lib/puma/cli.rb +11 -9
  32. data/lib/puma/client.rb +233 -136
  33. data/lib/puma/client_env.rb +171 -0
  34. data/lib/puma/cluster/worker.rb +24 -21
  35. data/lib/puma/cluster/worker_handle.rb +38 -8
  36. data/lib/puma/cluster.rb +74 -48
  37. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  38. data/lib/puma/commonlogger.rb +3 -3
  39. data/lib/puma/configuration.rb +197 -64
  40. data/lib/puma/const.rb +23 -12
  41. data/lib/puma/control_cli.rb +11 -7
  42. data/lib/puma/detect.rb +13 -0
  43. data/lib/puma/dsl.rb +483 -127
  44. data/lib/puma/error_logger.rb +7 -5
  45. data/lib/puma/events.rb +25 -10
  46. data/lib/puma/io_buffer.rb +8 -4
  47. data/lib/puma/jruby_restart.rb +0 -16
  48. data/lib/puma/launcher/bundle_pruner.rb +3 -5
  49. data/lib/puma/launcher.rb +76 -59
  50. data/lib/puma/log_writer.rb +17 -11
  51. data/lib/puma/minissl/context_builder.rb +1 -0
  52. data/lib/puma/minissl.rb +1 -1
  53. data/lib/puma/null_io.rb +26 -0
  54. data/lib/puma/plugin/systemd.rb +3 -3
  55. data/lib/puma/rack/urlmap.rb +1 -1
  56. data/lib/puma/reactor.rb +19 -13
  57. data/lib/puma/{request.rb → response.rb} +57 -209
  58. data/lib/puma/runner.rb +15 -17
  59. data/lib/puma/sd_notify.rb +1 -4
  60. data/lib/puma/server.rb +200 -104
  61. data/lib/puma/server_plugin_control.rb +32 -0
  62. data/lib/puma/single.rb +7 -4
  63. data/lib/puma/state_file.rb +3 -2
  64. data/lib/puma/thread_pool.rb +179 -96
  65. data/lib/puma/util.rb +0 -7
  66. data/lib/puma.rb +10 -0
  67. data/lib/rack/handler/puma.rb +11 -8
  68. data/tools/Dockerfile +15 -5
  69. metadata +26 -16
  70. data/ext/puma_http11/ext_help.h +0 -15
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+
5
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
8
+ # This module is included in `Client`. It contains code to process the `env`
9
+ # before it is passed to the app.
10
+ #
11
+ module ClientEnv # :nodoc:
12
+
13
+ include Puma::Const
14
+
15
+ # Given a Hash +env+ for the request read from +client+, add
16
+ # and fixup keys to comply with Rack's env guidelines.
17
+ # @param env [Hash] see Puma::Client#env, from request
18
+ # @param client [Puma::Client] only needed for Client#peerip
19
+ #
20
+ def normalize_env
21
+ if host = @env[HTTP_HOST]
22
+ # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
23
+ if colon = host.rindex("]:") # IPV6 with port
24
+ @env[SERVER_NAME] = host[0, colon+1]
25
+ @env[SERVER_PORT] = host[colon+2, host.bytesize]
26
+ elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
27
+ @env[SERVER_NAME] = host[0, colon]
28
+ @env[SERVER_PORT] = host[colon+1, host.bytesize]
29
+ else
30
+ @env[SERVER_NAME] = host
31
+ @env[SERVER_PORT] = default_server_port
32
+ end
33
+ else
34
+ @env[SERVER_NAME] = LOCALHOST
35
+ @env[SERVER_PORT] = default_server_port
36
+ end
37
+
38
+ unless @env[REQUEST_PATH]
39
+ # it might be a dumbass full host request header
40
+ uri = begin
41
+ URI.parse(@env[REQUEST_URI])
42
+ rescue URI::InvalidURIError
43
+ raise Puma::HttpParserError
44
+ end
45
+ @env[REQUEST_PATH] = uri.path
46
+
47
+ # A nil env value will cause a LintError (and fatal errors elsewhere),
48
+ # so only set the env value if there actually is a value.
49
+ @env[QUERY_STRING] = uri.query if uri.query
50
+ end
51
+
52
+ @env[PATH_INFO] = @env[REQUEST_PATH].to_s # #to_s in case it's nil
53
+
54
+ # From https://www.ietf.org/rfc/rfc3875 :
55
+ # "Script authors should be aware that the REMOTE_ADDR and
56
+ # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
57
+ # may not identify the ultimate source of the request.
58
+ # They identify the client for the immediate request to the
59
+ # server; that client may be a proxy, gateway, or other
60
+ # intermediary acting on behalf of the actual source client."
61
+ #
62
+
63
+ unless @env.key?(REMOTE_ADDR)
64
+ begin
65
+ addr = peerip
66
+ rescue Errno::ENOTCONN
67
+ # Client disconnects can result in an inability to get the
68
+ # peeraddr from the socket; default to unspec.
69
+ if peer_family == Socket::AF_INET6
70
+ addr = UNSPECIFIED_IPV6
71
+ else
72
+ addr = UNSPECIFIED_IPV4
73
+ end
74
+ end
75
+
76
+ # Set unix socket addrs to localhost
77
+ if addr.empty?
78
+ addr = peer_family == Socket::AF_INET6 ? LOCALHOST_IPV6 : LOCALHOST_IPV4
79
+ end
80
+
81
+ @env[REMOTE_ADDR] = addr
82
+ end
83
+
84
+ # The legacy HTTP_VERSION header can be sent as a client header.
85
+ # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
86
+ @env[HTTP_VERSION] = @env[SERVER_PROTOCOL] if @env_set_http_version
87
+
88
+ @env[PUMA_SOCKET] = @io
89
+
90
+ if @env[HTTPS_KEY] && @io.peercert
91
+ @env[PUMA_PEERCERT] = @io.peercert
92
+ end
93
+
94
+ @env[HIJACK_P] = true
95
+ @env[HIJACK] = method(:full_hijack).to_proc
96
+
97
+ @env[RACK_INPUT] = @body || EmptyBody
98
+ @env[RACK_URL_SCHEME] ||= default_server_port == PORT_443 ? HTTPS : HTTP
99
+ end
100
+
101
+ # Fixup any headers with `,` in the name to have `_` now. We emit
102
+ # headers with `,` in them during the parse phase to avoid ambiguity
103
+ # with the `-` to `_` conversion for critical headers. But here for
104
+ # compatibility, we'll convert them back. This code is written to
105
+ # avoid allocation in the common case (ie there are no headers
106
+ # with `,` in their names), that's why it has the extra conditionals.
107
+ #
108
+ # @note If a normalized version of a `,` header already exists, we ignore
109
+ # the `,` version. This prevents clobbering headers managed by proxies
110
+ # but not by clients (Like X-Forwarded-For).
111
+ #
112
+ # @param env [Hash] see Puma::Client#env, from request, modifies in place
113
+ # @version 5.0.3
114
+ #
115
+ def req_env_post_parse
116
+ to_delete = nil
117
+ to_add = nil
118
+
119
+ @env.each do |k,v|
120
+ if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
121
+ if to_delete
122
+ to_delete << k
123
+ else
124
+ to_delete = [k]
125
+ end
126
+
127
+ new_k = k.tr(",", "_")
128
+ if @env.key?(new_k)
129
+ next
130
+ end
131
+
132
+ unless to_add
133
+ to_add = {}
134
+ end
135
+
136
+ to_add[new_k] = v
137
+ end
138
+ end
139
+
140
+ if to_delete # rubocop:disable Style/SafeNavigation
141
+ to_delete.each { |k| env.delete(k) }
142
+ end
143
+
144
+ if to_add
145
+ @env.merge! to_add
146
+ end
147
+
148
+ # A rack extension. If the app writes #call'ables to this
149
+ # array, we will invoke them when the request is done.
150
+ #
151
+ env[RACK_AFTER_REPLY] ||= []
152
+ env[RACK_RESPONSE_FINISHED] ||= []
153
+ end
154
+
155
+ HTTP_ON_VALUES = { "on" => true, HTTPS => true }
156
+ private_constant :HTTP_ON_VALUES
157
+
158
+ # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
159
+ #
160
+ def default_server_port
161
+ if HTTP_ON_VALUES[@env[HTTPS_KEY]] ||
162
+ @env[HTTP_X_FORWARDED_PROTO]&.start_with?(HTTPS) ||
163
+ @env[HTTP_X_FORWARDED_SCHEME] == HTTPS ||
164
+ @env[HTTP_X_FORWARDED_SSL] == "on"
165
+ PORT_443
166
+ else
167
+ PORT_80
168
+ end
169
+ end
170
+ end
171
+ end
@@ -14,7 +14,7 @@ module Puma
14
14
  class Worker < Puma::Runner # :nodoc:
15
15
  attr_reader :index, :master
16
16
 
17
- def initialize(index:, master:, launcher:, pipes:, server: nil)
17
+ def initialize(index:, master:, launcher:, pipes:, app: nil)
18
18
  super(launcher)
19
19
 
20
20
  @index = index
@@ -23,7 +23,8 @@ module Puma
23
23
  @worker_write = pipes[:worker_write]
24
24
  @fork_pipe = pipes[:fork_pipe]
25
25
  @wakeup = pipes[:wakeup]
26
- @server = server
26
+ @app = app
27
+ @server = nil
27
28
  @hook_data = {}
28
29
  end
29
30
 
@@ -57,7 +58,7 @@ module Puma
57
58
  @config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
58
59
 
59
60
  begin
60
- server = @server ||= start_server
61
+ @server = start_server
61
62
  rescue Exception => e
62
63
  log "! Unable to start worker"
63
64
  log e
@@ -85,36 +86,37 @@ module Puma
85
86
  if idx == -1 # stop server
86
87
  if restart_server.length > 0
87
88
  restart_server.clear
88
- server.begin_restart(true)
89
+ @server.begin_restart(true)
89
90
  @config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
90
91
  end
92
+ elsif idx == -2 # refork cycle is done
93
+ @config.run_hooks(:after_refork, nil, @log_writer, @hook_data)
91
94
  elsif idx == 0 # restart server
92
95
  restart_server << true << false
93
96
  else # fork worker
94
97
  worker_pids << pid = spawn_worker(idx)
95
- @worker_write << "f#{pid}:#{idx}\n" rescue nil
98
+ @worker_write << "#{PIPE_FORK}#{pid}:#{idx}\n" rescue nil
96
99
  end
97
100
  end
98
101
  end
99
102
  end
100
103
 
101
104
  Signal.trap "SIGTERM" do
102
- @worker_write << "e#{Process.pid}\n" rescue nil
105
+ @worker_write << "#{PIPE_EXTERNAL_TERM}#{Process.pid}\n" rescue nil
103
106
  restart_server.clear
104
- server.stop
107
+ @server.stop
105
108
  restart_server << false
106
109
  end
107
110
 
108
111
  begin
109
- @worker_write << "b#{Process.pid}:#{index}\n"
112
+ @worker_write << "#{PIPE_BOOT}#{Process.pid}:#{index}\n"
110
113
  rescue SystemCallError, IOError
111
- Puma::Util.purge_interrupt_queue
112
114
  STDERR.puts "Master seems to have exited, exiting."
113
115
  return
114
116
  end
115
117
 
116
118
  while restart_server.pop
117
- server_thread = server.run
119
+ server_thread = @server.run
118
120
 
119
121
  if @log_writer.debug? && index == 0
120
122
  debug_loaded_extensions "Loaded Extensions - worker 0:"
@@ -122,19 +124,20 @@ module Puma
122
124
 
123
125
  stat_thread ||= Thread.new(@worker_write) do |io|
124
126
  Puma.set_thread_name "stat pld"
125
- base_payload = "p#{Process.pid}"
127
+ base_payload = "#{PIPE_PING}#{Process.pid}"
126
128
 
127
129
  while true
128
130
  begin
129
- b = server.backlog || 0
130
- r = server.running || 0
131
- t = server.pool_capacity || 0
132
- m = server.max_threads || 0
133
- rc = server.requests_count || 0
134
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
135
- io << payload
131
+ payload = base_payload.dup
132
+
133
+ hsh = @server.stats
134
+ hsh.each do |k, v|
135
+ payload << %Q! "#{k}":#{v || 0},!
136
+ end
137
+ # sub call properly adds 'closing' string
138
+ io << payload.sub(/,\z/, " }\n")
139
+ @server.reset_max
136
140
  rescue IOError
137
- Puma::Util.purge_interrupt_queue
138
141
  break
139
142
  end
140
143
  sleep @options[:worker_check_interval]
@@ -147,7 +150,7 @@ module Puma
147
150
  # exiting until any background operations are completed
148
151
  @config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
149
152
  ensure
150
- @worker_write << "t#{Process.pid}\n" rescue nil
153
+ @worker_write << "#{PIPE_TERM}#{Process.pid}\n" rescue nil
151
154
  @worker_write.close
152
155
  end
153
156
 
@@ -162,7 +165,7 @@ module Puma
162
165
  launcher: @launcher,
163
166
  pipes: { check_pipe: @check_pipe,
164
167
  worker_write: @worker_write },
165
- server: @server
168
+ app: @app
166
169
  new_worker.run
167
170
  end
168
171
 
@@ -4,13 +4,15 @@ module Puma
4
4
  class Cluster < Runner
5
5
  #—————————————————————— DO NOT USE — this class is for internal use only ———
6
6
 
7
-
8
7
  # This class represents a worker process from the perspective of the puma
9
8
  # master process. It contains information about the process and its health
10
9
  # and it exposes methods to control the process via IPC. It does not
11
10
  # include the actual logic executed by the worker process itself. For that,
12
11
  # see Puma::Cluster::Worker.
13
12
  class WorkerHandle # :nodoc:
13
+ # array of stat 'max' keys
14
+ WORKER_MAX_KEYS = [:backlog_max, :reactor_max]
15
+
14
16
  def initialize(idx, pid, phase, options)
15
17
  @index = idx
16
18
  @pid = pid
@@ -23,12 +25,13 @@ module Puma
23
25
  @last_checkin = Time.now
24
26
  @last_status = {}
25
27
  @term = false
28
+ @worker_max = Array.new WORKER_MAX_KEYS.length, 0
26
29
  end
27
30
 
28
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
31
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at, :process_status
29
32
 
30
33
  # @version 5.0.0
31
- attr_writer :pid, :phase
34
+ attr_writer :pid, :phase, :process_status
32
35
 
33
36
  def booted?
34
37
  @stage == :booted
@@ -52,12 +55,39 @@ module Puma
52
55
  end
53
56
 
54
57
  def ping!(status)
55
- @last_checkin = Time.now
56
- captures = status.match(/{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads": (?<max_threads>\d*), "requests_count": (?<requests_count>\d*) }/)
57
- @last_status = captures.names.inject({}) do |hash, key|
58
- hash[key.to_sym] = captures[key].to_i
59
- hash
58
+ hsh = {}
59
+ k, v = nil, nil
60
+ status.tr('}{"', '').strip.split(", ") do |kv|
61
+ cntr = 0
62
+ kv.split(':') do |t|
63
+ if cntr == 0
64
+ k = t
65
+ cntr = 1
66
+ else
67
+ v = t
68
+ end
69
+ end
70
+ hsh[k.to_sym] = v.to_i
71
+ end
72
+
73
+ # check stat max values, we can't signal workers to reset the max values,
74
+ # so we do so here
75
+ WORKER_MAX_KEYS.each_with_index do |key, idx|
76
+ next unless hsh[key]
77
+
78
+ if hsh[key] < @worker_max[idx]
79
+ hsh[key] = @worker_max[idx]
80
+ else
81
+ @worker_max[idx] = hsh[key]
82
+ end
60
83
  end
84
+ @last_checkin = Time.now
85
+ @last_status = hsh
86
+ end
87
+
88
+ # Resets max values to zero. Called whenever `Cluster#stats` is called
89
+ def reset_max
90
+ WORKER_MAX_KEYS.length.times { |idx| @worker_max[idx] = 0 }
61
91
  end
62
92
 
63
93
  # @see Puma::Cluster#check_workers
data/lib/puma/cluster.rb CHANGED
@@ -22,7 +22,8 @@ module Puma
22
22
  @workers = []
23
23
  @next_check = Time.now
24
24
 
25
- @phased_restart = false
25
+ @worker_max = [] # keeps track of 'max' stat values
26
+ @pending_phased_restart = false
26
27
  end
27
28
 
28
29
  # Returns the list of cluster worker handles.
@@ -44,10 +45,14 @@ module Puma
44
45
  end
45
46
  end
46
47
 
47
- def start_phased_restart
48
- @events.fire_on_restart!
48
+ def start_phased_restart(refork = false)
49
+ @events.fire_before_restart!
49
50
  @phase += 1
50
- log "- Starting phased worker restart, phase: #{@phase}"
51
+ if refork
52
+ log "- Starting worker refork, phase: #{@phase}"
53
+ else
54
+ log "- Starting phased worker restart, phase: #{@phase}"
55
+ end
51
56
 
52
57
  # Be sure to change the directory again before loading
53
58
  # the app. This way we can pick up new code.
@@ -82,11 +87,15 @@ module Puma
82
87
  end
83
88
 
84
89
  debug "Spawned worker: #{pid}"
85
- @workers << WorkerHandle.new(idx, pid, @phase, @options)
90
+ @workers.insert(idx, WorkerHandle.new(idx, pid, @phase, @options))
86
91
  end
87
92
 
88
93
  if @options[:fork_worker] && all_workers_in_phase?
89
94
  @fork_writer << "0\n"
95
+
96
+ if worker_at(0).phase > 0
97
+ @fork_writer << "-2\n"
98
+ end
90
99
  end
91
100
  end
92
101
 
@@ -162,7 +171,7 @@ module Puma
162
171
  (@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
163
172
  end
164
173
 
165
- def check_workers
174
+ def check_workers(refork = false)
166
175
  return if @next_check >= Time.now
167
176
 
168
177
  @next_check = Time.now + @options[:worker_check_interval]
@@ -177,10 +186,15 @@ module Puma
177
186
  # we need to phase any workers out (which will restart
178
187
  # in the right phase).
179
188
  #
180
- w = @workers.find { |x| x.phase != @phase }
189
+ w = @workers.find { |x| x.phase < @phase }
181
190
 
182
191
  if w
183
- log "- Stopping #{w.pid} for phased upgrade..."
192
+ if refork
193
+ log "- Stopping #{w.pid} for refork..."
194
+ else
195
+ log "- Stopping #{w.pid} for phased upgrade..."
196
+ end
197
+
184
198
  unless w.term?
185
199
  w.term
186
200
  log "- #{w.signal} sent to #{w.pid}..."
@@ -207,12 +221,11 @@ module Puma
207
221
  pipes[:wakeup] = @wakeup
208
222
  end
209
223
 
210
- server = start_server if preload?
211
224
  new_worker = Worker.new index: index,
212
225
  master: master,
213
226
  launcher: @launcher,
214
227
  pipes: pipes,
215
- server: server
228
+ app: (app if preload?)
216
229
  new_worker.run
217
230
  end
218
231
 
@@ -224,7 +237,7 @@ module Puma
224
237
  def phased_restart(refork = false)
225
238
  return false if @options[:preload_app] && !refork
226
239
 
227
- @phased_restart = true
240
+ @pending_phased_restart = refork ? :refork : true
228
241
  wakeup!
229
242
 
230
243
  true
@@ -254,11 +267,14 @@ module Puma
254
267
  end
255
268
 
256
269
  # Inside of a child process, this will return all zeroes, as @workers is only populated in
257
- # the master process.
270
+ # the master process. Calling this also resets stat 'max' values to zero.
258
271
  # @!attribute [r] stats
272
+ # @return [Hash]
273
+
259
274
  def stats
260
275
  old_worker_count = @workers.count { |w| w.phase != @phase }
261
276
  worker_status = @workers.map do |w|
277
+ w.reset_max
262
278
  {
263
279
  started_at: utc_iso8601(w.started_at),
264
280
  pid: w.pid,
@@ -269,7 +285,6 @@ module Puma
269
285
  last_status: w.last_status,
270
286
  }
271
287
  end
272
-
273
288
  {
274
289
  started_at: utc_iso8601(@started_at),
275
290
  workers: @workers.size,
@@ -338,7 +353,7 @@ module Puma
338
353
 
339
354
  stop_workers
340
355
  stop
341
- @events.fire_on_stopped!
356
+ @events.fire_after_stopped!
342
357
  raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
343
358
  exit 0 # Clean exit, workers were stopped
344
359
  end
@@ -348,8 +363,6 @@ module Puma
348
363
  def run
349
364
  @status = :run
350
365
 
351
- @idle_workers = {}
352
-
353
366
  output_header "cluster"
354
367
 
355
368
  # This is aligned with the output from Runner, see Runner#output_header
@@ -357,16 +370,12 @@ module Puma
357
370
 
358
371
  if preload?
359
372
  # Threads explicitly marked as fork safe will be ignored. Used in Rails,
360
- # but may be used by anyone. Note that we need to explicit
361
- # Process::Waiter check here because there's a bug in Ruby 2.6 and below
362
- # where calling thread_variable_get on a Process::Waiter will segfault.
363
- # We can drop that clause once those versions of Ruby are no longer
364
- # supported.
365
- fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
373
+ # but may be used by anyone.
374
+ fork_safe = ->(t) { t.thread_variable_get(:fork_safe) }
366
375
 
367
376
  before = Thread.list.reject(&fork_safe)
368
377
 
369
- log "* Restarts: (\u2714) hot (\u2716) phased"
378
+ log "* Restarts: (\u2714) hot (\u2716) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
370
379
  log "* Preloading application"
371
380
  load_and_bind
372
381
 
@@ -384,7 +393,7 @@ module Puma
384
393
  end
385
394
  end
386
395
  else
387
- log "* Restarts: (\u2714) hot (\u2714) phased"
396
+ log "* Restarts: (\u2714) hot (\u2714) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
388
397
 
389
398
  unless @config.app_configured?
390
399
  error "No application configured, nothing to run"
@@ -411,6 +420,7 @@ module Puma
411
420
 
412
421
  log "Use Ctrl-C to stop"
413
422
 
423
+ warn_ruby_mn_threads
414
424
  single_worker_warning
415
425
 
416
426
  redirect_io
@@ -440,30 +450,37 @@ module Puma
440
450
 
441
451
  while @status == :run
442
452
  begin
443
- if all_workers_idle_timed_out?
453
+ if @options[:idle_timeout] && all_workers_idle_timed_out?
444
454
  log "- All workers reached idle timeout"
445
455
  break
446
456
  end
447
457
 
448
- if @phased_restart
449
- start_phased_restart
450
- @phased_restart = false
451
- in_phased_restart = true
458
+ if @pending_phased_restart
459
+ start_phased_restart(@pending_phased_restart == :refork)
460
+
461
+ in_phased_restart = @pending_phased_restart
462
+ @pending_phased_restart = false
463
+
452
464
  workers_not_booted = @options[:workers]
465
+ # worker 0 is not restarted on refork
466
+ workers_not_booted -= 1 if in_phased_restart == :refork
453
467
  end
454
468
 
455
- check_workers
469
+ check_workers(in_phased_restart == :refork)
456
470
 
457
471
  if read.wait_readable([0, @next_check - Time.now].max)
458
472
  req = read.read_nonblock(1)
473
+ next unless req
459
474
 
460
- @next_check = Time.now if req == "!"
461
- next if !req || req == "!"
475
+ if req == PIPE_WAKEUP
476
+ @next_check = Time.now
477
+ next
478
+ end
462
479
 
463
480
  result = read.gets
464
481
  pid = result.to_i
465
482
 
466
- if req == "b" || req == "f"
483
+ if req == PIPE_BOOT || req == PIPE_FORK
467
484
  pid, idx = result.split(':').map(&:to_i)
468
485
  w = worker_at idx
469
486
  w.pid = pid if w.pid.nil?
@@ -471,36 +488,36 @@ module Puma
471
488
 
472
489
  if w = @workers.find { |x| x.pid == pid }
473
490
  case req
474
- when "b"
491
+ when PIPE_BOOT
475
492
  w.boot!
476
493
  log "- Worker #{w.index} (PID: #{pid}) booted in #{w.uptime.round(2)}s, phase: #{w.phase}"
477
494
  @next_check = Time.now
478
495
  workers_not_booted -= 1
479
- when "e"
496
+ when PIPE_EXTERNAL_TERM
480
497
  # external term, see worker method, Signal.trap "SIGTERM"
481
498
  w.term!
482
- when "t"
499
+ when PIPE_TERM
483
500
  w.term unless w.term?
484
- when "p"
501
+ when PIPE_PING
485
502
  status = result.sub(/^\d+/,'').chomp
486
503
  w.ping!(status)
487
504
  @events.fire(:ping!, w)
488
505
 
489
- if in_phased_restart && workers_not_booted.positive? && w0 = worker_at(0)
506
+ if in_phased_restart && @options[:fork_worker] && workers_not_booted.positive? && w0 = worker_at(0)
490
507
  w0.ping!(status)
491
508
  @events.fire(:ping!, w0)
492
509
  end
493
510
 
494
511
  if !booted && @workers.none? {|worker| worker.last_status.empty?}
495
- @events.fire_on_booted!
512
+ @events.fire_after_booted!
496
513
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
497
514
  booted = true
498
515
  end
499
- when "i"
500
- if @idle_workers[pid]
501
- @idle_workers.delete pid
516
+ when PIPE_IDLE
517
+ if idle_workers[pid]
518
+ idle_workers.delete pid
502
519
  else
503
- @idle_workers[pid] = true
520
+ idle_workers[pid] = true
504
521
  end
505
522
  end
506
523
  else
@@ -509,7 +526,7 @@ module Puma
509
526
  end
510
527
 
511
528
  if in_phased_restart && workers_not_booted.zero?
512
- @events.fire_on_booted!
529
+ @events.fire_after_booted!
513
530
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
514
531
  in_phased_restart = false
515
532
  end
@@ -560,9 +577,14 @@ module Puma
560
577
  @workers.reject! do |w|
561
578
  next false if w.pid.nil?
562
579
  begin
563
- # When `fork_worker` is enabled, some worker may not be direct children, but grand children.
564
- # Because of this they won't be reaped by `Process.wait2(-1)`, so we need to check them individually)
565
- if reaped_children.delete(w.pid) || (@options[:fork_worker] && Process.wait(w.pid, Process::WNOHANG))
580
+ # We may need to check the PID individually because:
581
+ # 1. From Ruby versions 2.6 to 3.2, `Process.detach` can prevent or delay
582
+ # `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
583
+ # 2. When `fork_worker` is enabled, some worker may not be direct children,
584
+ # but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
585
+ if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
586
+ w.process_status = status
587
+ @config.run_hooks(:after_worker_shutdown, w, @log_writer)
566
588
  true
567
589
  else
568
590
  w.term if w.term?
@@ -602,7 +624,11 @@ module Puma
602
624
  end
603
625
 
604
626
  def idle_timed_out_worker_pids
605
- @idle_workers.keys
627
+ idle_workers.keys
628
+ end
629
+
630
+ def idle_workers
631
+ @idle_workers ||= {}
606
632
  end
607
633
  end
608
634
  end