puma 4.3.12 → 6.0.2

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1618 -521
  3. data/LICENSE +23 -20
  4. data/README.md +130 -42
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +63 -26
  7. data/docs/compile_options.md +55 -0
  8. data/docs/deployment.md +60 -69
  9. data/docs/fork_worker.md +31 -0
  10. data/docs/jungle/README.md +9 -0
  11. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  12. data/{tools → docs}/jungle/rc.d/puma +2 -2
  13. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  14. data/docs/kubernetes.md +66 -0
  15. data/docs/nginx.md +2 -2
  16. data/docs/plugins.md +15 -15
  17. data/docs/rails_dev_mode.md +28 -0
  18. data/docs/restart.md +46 -23
  19. data/docs/signals.md +13 -11
  20. data/docs/stats.md +142 -0
  21. data/docs/systemd.md +85 -128
  22. data/docs/testing_benchmarks_local_files.md +150 -0
  23. data/docs/testing_test_rackup_ci_files.md +36 -0
  24. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  25. data/ext/puma_http11/ext_help.h +1 -1
  26. data/ext/puma_http11/extconf.rb +49 -12
  27. data/ext/puma_http11/http11_parser.c +46 -48
  28. data/ext/puma_http11/http11_parser.h +2 -2
  29. data/ext/puma_http11/http11_parser.java.rl +3 -3
  30. data/ext/puma_http11/http11_parser.rl +3 -3
  31. data/ext/puma_http11/http11_parser_common.rl +2 -2
  32. data/ext/puma_http11/mini_ssl.c +250 -93
  33. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  34. data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
  35. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +4 -6
  36. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
  37. data/ext/puma_http11/puma_http11.c +46 -57
  38. data/lib/puma/app/status.rb +52 -38
  39. data/lib/puma/binder.rb +232 -119
  40. data/lib/puma/cli.rb +33 -33
  41. data/lib/puma/client.rb +129 -88
  42. data/lib/puma/cluster/worker.rb +175 -0
  43. data/lib/puma/cluster/worker_handle.rb +97 -0
  44. data/lib/puma/cluster.rb +224 -231
  45. data/lib/puma/commonlogger.rb +2 -2
  46. data/lib/puma/configuration.rb +112 -87
  47. data/lib/puma/const.rb +86 -91
  48. data/lib/puma/control_cli.rb +99 -79
  49. data/lib/puma/detect.rb +31 -2
  50. data/lib/puma/dsl.rb +426 -110
  51. data/lib/puma/error_logger.rb +112 -0
  52. data/lib/puma/events.rb +16 -115
  53. data/lib/puma/io_buffer.rb +44 -2
  54. data/lib/puma/jruby_restart.rb +2 -59
  55. data/lib/puma/json_serialization.rb +96 -0
  56. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  57. data/lib/puma/launcher.rb +170 -148
  58. data/lib/puma/log_writer.rb +137 -0
  59. data/lib/puma/minissl/context_builder.rb +35 -19
  60. data/lib/puma/minissl.rb +213 -55
  61. data/lib/puma/null_io.rb +18 -1
  62. data/lib/puma/plugin/tmp_restart.rb +1 -1
  63. data/lib/puma/plugin.rb +3 -12
  64. data/lib/puma/rack/builder.rb +5 -9
  65. data/lib/puma/rack_default.rb +1 -1
  66. data/lib/puma/reactor.rb +85 -369
  67. data/lib/puma/request.rb +644 -0
  68. data/lib/puma/runner.rb +86 -76
  69. data/lib/puma/server.rb +306 -793
  70. data/lib/puma/single.rb +18 -74
  71. data/lib/puma/state_file.rb +45 -8
  72. data/lib/puma/systemd.rb +47 -0
  73. data/lib/puma/thread_pool.rb +136 -68
  74. data/lib/puma/util.rb +21 -4
  75. data/lib/puma.rb +54 -7
  76. data/lib/rack/handler/puma.rb +11 -12
  77. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  78. metadata +31 -23
  79. data/docs/tcp_mode.md +0 -96
  80. data/ext/puma_http11/io_buffer.c +0 -155
  81. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  82. data/lib/puma/accept_nonblock.rb +0 -29
  83. data/lib/puma/tcp_logger.rb +0 -41
  84. data/tools/jungle/README.md +0 -19
  85. data/tools/jungle/init.d/README.md +0 -61
  86. data/tools/jungle/init.d/puma +0 -421
  87. data/tools/jungle/init.d/run-puma +0 -18
  88. data/tools/jungle/upstart/README.md +0 -61
  89. data/tools/jungle/upstart/puma-manager.conf +0 -31
  90. data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/client.rb CHANGED
@@ -8,7 +8,8 @@ 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
14
  require 'forwardable'
14
15
 
@@ -25,6 +26,9 @@ module Puma
25
26
 
26
27
  class HttpParserError501 < IOError; end
27
28
 
29
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
30
+
31
+
28
32
  # An instance of this class represents a unique request from a client.
29
33
  # For example, this could be a web request from a browser or from CURL.
30
34
  #
@@ -38,7 +42,7 @@ module Puma
38
42
  # the header and body are fully buffered via the `try_to_finish` method.
39
43
  # They can be used to "time out" a response via the `timeout_at` reader.
40
44
  #
41
- class Client
45
+ class Client # :nodoc:
42
46
 
43
47
  # this tests all values but the last, which must be chunked
44
48
  ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
@@ -62,16 +66,14 @@ module Puma
62
66
  def initialize(io, env=nil)
63
67
  @io = io
64
68
  @to_io = io.to_io
69
+ @io_buffer = IOBuffer.new
65
70
  @proto_env = env
66
- if !env
67
- @env = nil
68
- else
69
- @env = env.dup
70
- end
71
+ @env = env ? env.dup : nil
71
72
 
72
73
  @parser = HttpParser.new
73
74
  @parsed_bytes = 0
74
75
  @read_header = true
76
+ @read_proxy = false
75
77
  @ready = false
76
78
 
77
79
  @body = nil
@@ -85,7 +87,10 @@ module Puma
85
87
  @hijacked = false
86
88
 
87
89
  @peerip = nil
90
+ @peer_family = nil
91
+ @listener = nil
88
92
  @remote_addr_header = nil
93
+ @expect_proxy_proto = false
89
94
 
90
95
  @body_remain = 0
91
96
 
@@ -93,14 +98,21 @@ module Puma
93
98
  end
94
99
 
95
100
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
96
- :tempfile
101
+ :tempfile, :io_buffer
97
102
 
98
103
  attr_writer :peerip
99
104
 
100
- attr_accessor :remote_addr_header
105
+ attr_accessor :remote_addr_header, :listener
101
106
 
102
107
  def_delegators :@io, :closed?
103
108
 
109
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
110
+ # used for MiniSSL::Socket
111
+ def io_ok?
112
+ @to_io.is_a?(::BasicSocket) && !closed?
113
+ end
114
+
115
+ # @!attribute [r] inspect
104
116
  def inspect
105
117
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
106
118
  end
@@ -112,27 +124,37 @@ module Puma
112
124
  env[HIJACK_IO] ||= @io
113
125
  end
114
126
 
127
+ # @!attribute [r] in_data_phase
115
128
  def in_data_phase
116
- !@read_header
129
+ !(@read_header || @read_proxy)
117
130
  end
118
131
 
119
132
  def set_timeout(val)
120
- @timeout_at = Time.now + val
133
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
134
+ end
135
+
136
+ # Number of seconds until the timeout elapses.
137
+ def timeout
138
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
121
139
  end
122
140
 
123
141
  def reset(fast_check=true)
124
142
  @parser.reset
143
+ @io_buffer.reset
125
144
  @read_header = true
145
+ @read_proxy = !!@expect_proxy_proto
126
146
  @env = @proto_env.dup
127
147
  @body = nil
128
148
  @tempfile = nil
129
149
  @parsed_bytes = 0
130
150
  @ready = false
131
151
  @body_remain = 0
132
- @peerip = nil
152
+ @peerip = nil if @remote_addr_header
133
153
  @in_last_chunk = false
134
154
 
135
155
  if @buffer
156
+ return false unless try_to_parse_proxy_protocol
157
+
136
158
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
137
159
 
138
160
  if @parser.finished?
@@ -145,8 +167,7 @@ module Puma
145
167
  return false
146
168
  else
147
169
  begin
148
- if fast_check &&
149
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
170
+ if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
150
171
  return try_to_finish
151
172
  end
152
173
  rescue IOError
@@ -159,19 +180,45 @@ module Puma
159
180
  def close
160
181
  begin
161
182
  @io.close
162
- rescue IOError
163
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
183
+ rescue IOError, Errno::EBADF
184
+ Puma::Util.purge_interrupt_queue
164
185
  end
165
186
  end
166
187
 
188
+ # If necessary, read the PROXY protocol from the buffer. Returns
189
+ # false if more data is needed.
190
+ def try_to_parse_proxy_protocol
191
+ if @read_proxy
192
+ if @expect_proxy_proto == :v1
193
+ if @buffer.include? "\r\n"
194
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
195
+ if md[1]
196
+ @peerip = md[1].split(" ")[0]
197
+ end
198
+ @buffer = md.post_match
199
+ end
200
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
201
+ # request, this is just HTTP from a non-PROXY client; move on
202
+ @read_proxy = false
203
+ return @buffer.size > 0
204
+ else
205
+ return false
206
+ end
207
+ end
208
+ end
209
+ true
210
+ end
211
+
167
212
  def try_to_finish
168
- return read_body unless @read_header
213
+ return read_body if in_data_phase
169
214
 
170
215
  begin
171
216
  data = @io.read_nonblock(CHUNK_SIZE)
172
217
  rescue IO::WaitReadable
173
218
  return false
174
- rescue SystemCallError, IOError, EOFError
219
+ rescue EOFError
220
+ # Swallow error, don't log
221
+ rescue SystemCallError, IOError
175
222
  raise ConnectionError, "Connection error detected during read"
176
223
  end
177
224
 
@@ -188,6 +235,8 @@ module Puma
188
235
  @buffer = data
189
236
  end
190
237
 
238
+ return false unless try_to_parse_proxy_protocol
239
+
191
240
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
192
241
 
193
242
  if @parser.finished?
@@ -200,68 +249,20 @@ module Puma
200
249
  false
201
250
  end
202
251
 
203
- if IS_JRUBY
204
- def jruby_start_try_to_finish
205
- return read_body unless @read_header
206
-
207
- begin
208
- data = @io.sysread_nonblock(CHUNK_SIZE)
209
- rescue OpenSSL::SSL::SSLError => e
210
- return false if e.kind_of? IO::WaitReadable
211
- raise e
212
- end
213
-
214
- # No data means a closed socket
215
- unless data
216
- @buffer = nil
217
- set_ready
218
- raise EOFError
219
- end
220
-
221
- if @buffer
222
- @buffer << data
223
- else
224
- @buffer = data
225
- end
226
-
227
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
228
-
229
- if @parser.finished?
230
- return setup_body
231
- elsif @parsed_bytes >= MAX_HEADER
232
- raise HttpParserError,
233
- "HEADER is longer than allowed, aborting client early."
234
- end
235
-
236
- false
237
- end
238
-
239
- def eagerly_finish
240
- return true if @ready
241
-
242
- if @io.kind_of? OpenSSL::SSL::SSLSocket
243
- return true if jruby_start_try_to_finish
244
- end
245
-
246
- return false unless IO.select([@to_io], nil, nil, 0)
247
- try_to_finish
248
- end
249
-
250
- else
252
+ def eagerly_finish
253
+ return true if @ready
254
+ return false unless @to_io.wait_readable(0)
255
+ try_to_finish
256
+ end
251
257
 
252
- def eagerly_finish
253
- return true if @ready
254
- return false unless IO.select([@to_io], nil, nil, 0)
255
- try_to_finish
256
- end
257
- end # IS_JRUBY
258
+ def finish(timeout)
259
+ return if @ready
260
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
261
+ end
258
262
 
259
- def finish
260
- return true if @ready
261
- until try_to_finish
262
- IO.select([@to_io], nil, nil)
263
- end
264
- true
263
+ def timeout!
264
+ write_error(408) if in_data_phase
265
+ raise ConnectionError
265
266
  end
266
267
 
267
268
  def write_error(status_code)
@@ -275,7 +276,7 @@ module Puma
275
276
  return @peerip if @peerip
276
277
 
277
278
  if @remote_addr_header
278
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
279
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
279
280
  @peerip = hdr
280
281
  return hdr
281
282
  end
@@ -283,10 +284,40 @@ module Puma
283
284
  @peerip ||= @io.peeraddr.last
284
285
  end
285
286
 
287
+ def peer_family
288
+ return @peer_family if @peer_family
289
+
290
+ @peer_family ||= begin
291
+ @io.local_address.afamily
292
+ rescue
293
+ Socket::AF_INET
294
+ end
295
+ end
296
+
297
+ # Returns true if the persistent connection can be closed immediately
298
+ # without waiting for the configured idle/shutdown timeout.
299
+ # @version 5.0.0
300
+ #
301
+ def can_close?
302
+ # Allow connection to close if we're not in the middle of parsing a request.
303
+ @parsed_bytes == 0
304
+ end
305
+
306
+ def expect_proxy_proto=(val)
307
+ if val
308
+ if @read_header
309
+ @read_proxy = true
310
+ end
311
+ else
312
+ @read_proxy = false
313
+ end
314
+ @expect_proxy_proto = val
315
+ end
316
+
286
317
  private
287
318
 
288
319
  def setup_body
289
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
320
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
290
321
 
291
322
  if @env[HTTP_EXPECT] == CONTINUE
292
323
  # TODO allow a hook here to check the headers before
@@ -330,7 +361,7 @@ module Puma
330
361
 
331
362
  if cl
332
363
  # cannot contain characters that are not \d
333
- if cl =~ CONTENT_LENGTH_VALUE_INVALID
364
+ if CONTENT_LENGTH_VALUE_INVALID.match? cl
334
365
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
335
366
  end
336
367
  else
@@ -351,6 +382,7 @@ module Puma
351
382
 
352
383
  if remain > MAX_BODY
353
384
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
385
+ @body.unlink
354
386
  @body.binmode
355
387
  @tempfile = @body
356
388
  else
@@ -363,7 +395,7 @@ module Puma
363
395
 
364
396
  @body_remain = remain
365
397
 
366
- return false
398
+ false
367
399
  end
368
400
 
369
401
  def read_body
@@ -430,7 +462,7 @@ module Puma
430
462
  end
431
463
 
432
464
  if decode_chunk(chunk)
433
- @env[CONTENT_LENGTH] = @chunked_content_length
465
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
434
466
  return true
435
467
  end
436
468
  end
@@ -442,17 +474,18 @@ module Puma
442
474
  @prev_chunk = ""
443
475
 
444
476
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
477
+ @body.unlink
445
478
  @body.binmode
446
479
  @tempfile = @body
447
-
448
480
  @chunked_content_length = 0
449
481
 
450
482
  if decode_chunk(body)
451
- @env[CONTENT_LENGTH] = @chunked_content_length
483
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
452
484
  return true
453
485
  end
454
486
  end
455
487
 
488
+ # @version 5.0.0
456
489
  def write_chunk(str)
457
490
  @chunked_content_length += @body.write(str)
458
491
  end
@@ -466,7 +499,15 @@ module Puma
466
499
  chunk = chunk[@partial_part_left..-1]
467
500
  @partial_part_left = 0
468
501
  else
469
- write_chunk(chunk) if @partial_part_left > 2 # don't include the last \r\n
502
+ if @partial_part_left > 2
503
+ if @partial_part_left == chunk.size + 1
504
+ # Don't include the last \r
505
+ write_chunk(chunk[0..(@partial_part_left-3)])
506
+ else
507
+ # don't include the last \r\n
508
+ write_chunk(chunk)
509
+ end
510
+ end
470
511
  @partial_part_left -= chunk.size
471
512
  return false
472
513
  end
@@ -485,7 +526,7 @@ module Puma
485
526
  # Puma doesn't process chunk extensions, but should parse if they're
486
527
  # present, which is the reason for the semicolon regex
487
528
  chunk_hex = line.strip[/\A[^;]+/]
488
- if chunk_hex =~ CHUNK_SIZE_INVALID
529
+ if CHUNK_SIZE_INVALID.match? chunk_hex
489
530
  raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
490
531
  end
491
532
  len = chunk_hex.to_i(16)
@@ -548,7 +589,7 @@ module Puma
548
589
 
549
590
  def set_ready
550
591
  if @body_read_start
551
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
592
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
552
593
  end
553
594
  @requests_served += 1
554
595
  @ready = true
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ class Cluster < Puma::Runner
5
+ #—————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
8
+ # This class is instantiated by the `Puma::Cluster` and represents a single
9
+ # worker process.
10
+ #
11
+ # At the core of this class is running an instance of `Puma::Server` which
12
+ # gets created via the `start_server` method from the `Puma::Runner` class
13
+ # that this inherits from.
14
+ class Worker < Puma::Runner # :nodoc:
15
+ attr_reader :index, :master
16
+
17
+ def initialize(index:, master:, launcher:, pipes:, server: nil)
18
+ super(launcher)
19
+
20
+ @index = index
21
+ @master = master
22
+ @check_pipe = pipes[:check_pipe]
23
+ @worker_write = pipes[:worker_write]
24
+ @fork_pipe = pipes[:fork_pipe]
25
+ @wakeup = pipes[:wakeup]
26
+ @server = server
27
+ @hook_data = {}
28
+ end
29
+
30
+ def run
31
+ title = "puma: cluster worker #{index}: #{master}"
32
+ title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
33
+ $0 = title
34
+
35
+ Signal.trap "SIGINT", "IGNORE"
36
+ Signal.trap "SIGCHLD", "DEFAULT"
37
+
38
+ Thread.new do
39
+ Puma.set_thread_name "wrkr check"
40
+ @check_pipe.wait_readable
41
+ log "! Detected parent died, dying"
42
+ exit! 1
43
+ end
44
+
45
+ # If we're not running under a Bundler context, then
46
+ # report the info about the context we will be using
47
+ if !ENV['BUNDLE_GEMFILE']
48
+ if File.exist?("Gemfile")
49
+ log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
50
+ elsif File.exist?("gems.rb")
51
+ log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
52
+ end
53
+ end
54
+
55
+ # Invoke any worker boot hooks so they can get
56
+ # things in shape before booting the app.
57
+ @config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
58
+
59
+ begin
60
+ server = @server ||= start_server
61
+ rescue Exception => e
62
+ log "! Unable to start worker"
63
+ log e
64
+ log e.backtrace.join("\n ")
65
+ exit 1
66
+ end
67
+
68
+ restart_server = Queue.new << true << false
69
+
70
+ fork_worker = @options[:fork_worker] && index == 0
71
+
72
+ if fork_worker
73
+ restart_server.clear
74
+ worker_pids = []
75
+ Signal.trap "SIGCHLD" do
76
+ wakeup! if worker_pids.reject! do |p|
77
+ Process.wait(p, Process::WNOHANG) rescue true
78
+ end
79
+ end
80
+
81
+ Thread.new do
82
+ Puma.set_thread_name "wrkr fork"
83
+ while (idx = @fork_pipe.gets)
84
+ idx = idx.to_i
85
+ if idx == -1 # stop server
86
+ if restart_server.length > 0
87
+ restart_server.clear
88
+ server.begin_restart(true)
89
+ @config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
90
+ end
91
+ elsif idx == 0 # restart server
92
+ restart_server << true << false
93
+ else # fork worker
94
+ worker_pids << pid = spawn_worker(idx)
95
+ @worker_write << "f#{pid}:#{idx}\n" rescue nil
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ Signal.trap "SIGTERM" do
102
+ @worker_write << "e#{Process.pid}\n" rescue nil
103
+ restart_server.clear
104
+ server.stop
105
+ restart_server << false
106
+ end
107
+
108
+ begin
109
+ @worker_write << "b#{Process.pid}:#{index}\n"
110
+ rescue SystemCallError, IOError
111
+ Puma::Util.purge_interrupt_queue
112
+ STDERR.puts "Master seems to have exited, exiting."
113
+ return
114
+ end
115
+
116
+ while restart_server.pop
117
+ server_thread = server.run
118
+ stat_thread ||= Thread.new(@worker_write) do |io|
119
+ Puma.set_thread_name "stat pld"
120
+ base_payload = "p#{Process.pid}"
121
+
122
+ while true
123
+ begin
124
+ b = server.backlog || 0
125
+ r = server.running || 0
126
+ t = server.pool_capacity || 0
127
+ m = server.max_threads || 0
128
+ rc = server.requests_count || 0
129
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
130
+ io << payload
131
+ rescue IOError
132
+ Puma::Util.purge_interrupt_queue
133
+ break
134
+ end
135
+ sleep @options[:worker_check_interval]
136
+ end
137
+ end
138
+ server_thread.join
139
+ end
140
+
141
+ # Invoke any worker shutdown hooks so they can prevent the worker
142
+ # exiting until any background operations are completed
143
+ @config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
144
+ ensure
145
+ @worker_write << "t#{Process.pid}\n" rescue nil
146
+ @worker_write.close
147
+ end
148
+
149
+ private
150
+
151
+ def spawn_worker(idx)
152
+ @config.run_hooks(:before_worker_fork, idx, @log_writer, @hook_data)
153
+
154
+ pid = fork do
155
+ new_worker = Worker.new index: idx,
156
+ master: master,
157
+ launcher: @launcher,
158
+ pipes: { check_pipe: @check_pipe,
159
+ worker_write: @worker_write },
160
+ server: @server
161
+ new_worker.run
162
+ end
163
+
164
+ if !pid
165
+ log "! Complete inability to spawn new workers detected"
166
+ log "! Seppuku is the only choice."
167
+ exit! 1
168
+ end
169
+
170
+ @config.run_hooks(:after_worker_fork, idx, @log_writer, @hook_data)
171
+ pid
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ class Cluster < Runner
5
+ #—————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
8
+ # This class represents a worker process from the perspective of the puma
9
+ # master process. It contains information about the process and its health
10
+ # and it exposes methods to control the process via IPC. It does not
11
+ # include the actual logic executed by the worker process itself. For that,
12
+ # see Puma::Cluster::Worker.
13
+ class WorkerHandle # :nodoc:
14
+ def initialize(idx, pid, phase, options)
15
+ @index = idx
16
+ @pid = pid
17
+ @phase = phase
18
+ @stage = :started
19
+ @signal = "TERM"
20
+ @options = options
21
+ @first_term_sent = nil
22
+ @started_at = Time.now
23
+ @last_checkin = Time.now
24
+ @last_status = {}
25
+ @term = false
26
+ end
27
+
28
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
29
+
30
+ # @version 5.0.0
31
+ attr_writer :pid, :phase
32
+
33
+ def booted?
34
+ @stage == :booted
35
+ end
36
+
37
+ def uptime
38
+ Time.now - started_at
39
+ end
40
+
41
+ def boot!
42
+ @last_checkin = Time.now
43
+ @stage = :booted
44
+ end
45
+
46
+ def term!
47
+ @term = true
48
+ end
49
+
50
+ def term?
51
+ @term
52
+ end
53
+
54
+ 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
60
+ end
61
+ end
62
+
63
+ # @see Puma::Cluster#check_workers
64
+ # @version 5.0.0
65
+ def ping_timeout
66
+ @last_checkin +
67
+ (booted? ?
68
+ @options[:worker_timeout] :
69
+ @options[:worker_boot_timeout]
70
+ )
71
+ end
72
+
73
+ def term
74
+ begin
75
+ if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
76
+ @signal = "KILL"
77
+ else
78
+ @term ||= true
79
+ @first_term_sent ||= Time.now
80
+ end
81
+ Process.kill @signal, @pid if @pid
82
+ rescue Errno::ESRCH
83
+ end
84
+ end
85
+
86
+ def kill
87
+ @signal = 'KILL'
88
+ term
89
+ end
90
+
91
+ def hup
92
+ Process.kill "HUP", @pid
93
+ rescue Errno::ESRCH
94
+ end
95
+ end
96
+ end
97
+ end