puma 4.3.12 → 6.0.0

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 +1591 -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 +1 -1
  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 +125 -87
  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 -229
  45. data/lib/puma/commonlogger.rb +2 -2
  46. data/lib/puma/configuration.rb +112 -87
  47. data/lib/puma/const.rb +25 -22
  48. data/lib/puma/control_cli.rb +99 -79
  49. data/lib/puma/detect.rb +31 -2
  50. data/lib/puma/dsl.rb +423 -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 +34 -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 +607 -0
  68. data/lib/puma/runner.rb +83 -77
  69. data/lib/puma/server.rb +305 -789
  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 +137 -66
  74. data/lib/puma/util.rb +21 -4
  75. data/lib/puma.rb +54 -5
  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,7 @@ class IO
8
8
  end
9
9
  end
10
10
 
11
- require 'puma/detect'
11
+ require_relative 'detect'
12
12
  require 'tempfile'
13
13
  require 'forwardable'
14
14
 
@@ -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
@@ -63,15 +66,12 @@ module Puma
63
66
  @io = io
64
67
  @to_io = io.to_io
65
68
  @proto_env = env
66
- if !env
67
- @env = nil
68
- else
69
- @env = env.dup
70
- end
69
+ @env = env ? env.dup : nil
71
70
 
72
71
  @parser = HttpParser.new
73
72
  @parsed_bytes = 0
74
73
  @read_header = true
74
+ @read_proxy = false
75
75
  @ready = false
76
76
 
77
77
  @body = nil
@@ -85,7 +85,10 @@ module Puma
85
85
  @hijacked = false
86
86
 
87
87
  @peerip = nil
88
+ @peer_family = nil
89
+ @listener = nil
88
90
  @remote_addr_header = nil
91
+ @expect_proxy_proto = false
89
92
 
90
93
  @body_remain = 0
91
94
 
@@ -97,10 +100,17 @@ module Puma
97
100
 
98
101
  attr_writer :peerip
99
102
 
100
- attr_accessor :remote_addr_header
103
+ attr_accessor :remote_addr_header, :listener
101
104
 
102
105
  def_delegators :@io, :closed?
103
106
 
107
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
108
+ # used for MiniSSL::Socket
109
+ def io_ok?
110
+ @to_io.is_a?(::BasicSocket) && !closed?
111
+ end
112
+
113
+ # @!attribute [r] inspect
104
114
  def inspect
105
115
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
106
116
  end
@@ -112,27 +122,36 @@ module Puma
112
122
  env[HIJACK_IO] ||= @io
113
123
  end
114
124
 
125
+ # @!attribute [r] in_data_phase
115
126
  def in_data_phase
116
- !@read_header
127
+ !(@read_header || @read_proxy)
117
128
  end
118
129
 
119
130
  def set_timeout(val)
120
- @timeout_at = Time.now + val
131
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
132
+ end
133
+
134
+ # Number of seconds until the timeout elapses.
135
+ def timeout
136
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
121
137
  end
122
138
 
123
139
  def reset(fast_check=true)
124
140
  @parser.reset
125
141
  @read_header = true
142
+ @read_proxy = !!@expect_proxy_proto
126
143
  @env = @proto_env.dup
127
144
  @body = nil
128
145
  @tempfile = nil
129
146
  @parsed_bytes = 0
130
147
  @ready = false
131
148
  @body_remain = 0
132
- @peerip = nil
149
+ @peerip = nil if @remote_addr_header
133
150
  @in_last_chunk = false
134
151
 
135
152
  if @buffer
153
+ return false unless try_to_parse_proxy_protocol
154
+
136
155
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
137
156
 
138
157
  if @parser.finished?
@@ -145,8 +164,7 @@ module Puma
145
164
  return false
146
165
  else
147
166
  begin
148
- if fast_check &&
149
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
167
+ if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
150
168
  return try_to_finish
151
169
  end
152
170
  rescue IOError
@@ -159,19 +177,45 @@ module Puma
159
177
  def close
160
178
  begin
161
179
  @io.close
162
- rescue IOError
163
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
180
+ rescue IOError, Errno::EBADF
181
+ Puma::Util.purge_interrupt_queue
164
182
  end
165
183
  end
166
184
 
185
+ # If necessary, read the PROXY protocol from the buffer. Returns
186
+ # false if more data is needed.
187
+ def try_to_parse_proxy_protocol
188
+ if @read_proxy
189
+ if @expect_proxy_proto == :v1
190
+ if @buffer.include? "\r\n"
191
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
192
+ if md[1]
193
+ @peerip = md[1].split(" ")[0]
194
+ end
195
+ @buffer = md.post_match
196
+ end
197
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
198
+ # request, this is just HTTP from a non-PROXY client; move on
199
+ @read_proxy = false
200
+ return @buffer.size > 0
201
+ else
202
+ return false
203
+ end
204
+ end
205
+ end
206
+ true
207
+ end
208
+
167
209
  def try_to_finish
168
- return read_body unless @read_header
210
+ return read_body if in_data_phase
169
211
 
170
212
  begin
171
213
  data = @io.read_nonblock(CHUNK_SIZE)
172
214
  rescue IO::WaitReadable
173
215
  return false
174
- rescue SystemCallError, IOError, EOFError
216
+ rescue EOFError
217
+ # Swallow error, don't log
218
+ rescue SystemCallError, IOError
175
219
  raise ConnectionError, "Connection error detected during read"
176
220
  end
177
221
 
@@ -188,6 +232,8 @@ module Puma
188
232
  @buffer = data
189
233
  end
190
234
 
235
+ return false unless try_to_parse_proxy_protocol
236
+
191
237
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
192
238
 
193
239
  if @parser.finished?
@@ -200,68 +246,20 @@ module Puma
200
246
  false
201
247
  end
202
248
 
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
249
+ def eagerly_finish
250
+ return true if @ready
251
+ return false unless @to_io.wait_readable(0)
252
+ try_to_finish
253
+ end
251
254
 
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
255
+ def finish(timeout)
256
+ return if @ready
257
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
258
+ end
258
259
 
259
- def finish
260
- return true if @ready
261
- until try_to_finish
262
- IO.select([@to_io], nil, nil)
263
- end
264
- true
260
+ def timeout!
261
+ write_error(408) if in_data_phase
262
+ raise ConnectionError
265
263
  end
266
264
 
267
265
  def write_error(status_code)
@@ -275,7 +273,7 @@ module Puma
275
273
  return @peerip if @peerip
276
274
 
277
275
  if @remote_addr_header
278
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
276
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
279
277
  @peerip = hdr
280
278
  return hdr
281
279
  end
@@ -283,10 +281,40 @@ module Puma
283
281
  @peerip ||= @io.peeraddr.last
284
282
  end
285
283
 
284
+ def peer_family
285
+ return @peer_family if @peer_family
286
+
287
+ @peer_family ||= begin
288
+ @io.local_address.afamily
289
+ rescue
290
+ Socket::AF_INET
291
+ end
292
+ end
293
+
294
+ # Returns true if the persistent connection can be closed immediately
295
+ # without waiting for the configured idle/shutdown timeout.
296
+ # @version 5.0.0
297
+ #
298
+ def can_close?
299
+ # Allow connection to close if we're not in the middle of parsing a request.
300
+ @parsed_bytes == 0
301
+ end
302
+
303
+ def expect_proxy_proto=(val)
304
+ if val
305
+ if @read_header
306
+ @read_proxy = true
307
+ end
308
+ else
309
+ @read_proxy = false
310
+ end
311
+ @expect_proxy_proto = val
312
+ end
313
+
286
314
  private
287
315
 
288
316
  def setup_body
289
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
317
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
290
318
 
291
319
  if @env[HTTP_EXPECT] == CONTINUE
292
320
  # TODO allow a hook here to check the headers before
@@ -330,7 +358,7 @@ module Puma
330
358
 
331
359
  if cl
332
360
  # cannot contain characters that are not \d
333
- if cl =~ CONTENT_LENGTH_VALUE_INVALID
361
+ if CONTENT_LENGTH_VALUE_INVALID.match? cl
334
362
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
335
363
  end
336
364
  else
@@ -351,6 +379,7 @@ module Puma
351
379
 
352
380
  if remain > MAX_BODY
353
381
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
382
+ @body.unlink
354
383
  @body.binmode
355
384
  @tempfile = @body
356
385
  else
@@ -363,7 +392,7 @@ module Puma
363
392
 
364
393
  @body_remain = remain
365
394
 
366
- return false
395
+ false
367
396
  end
368
397
 
369
398
  def read_body
@@ -430,7 +459,7 @@ module Puma
430
459
  end
431
460
 
432
461
  if decode_chunk(chunk)
433
- @env[CONTENT_LENGTH] = @chunked_content_length
462
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
434
463
  return true
435
464
  end
436
465
  end
@@ -442,17 +471,18 @@ module Puma
442
471
  @prev_chunk = ""
443
472
 
444
473
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
474
+ @body.unlink
445
475
  @body.binmode
446
476
  @tempfile = @body
447
-
448
477
  @chunked_content_length = 0
449
478
 
450
479
  if decode_chunk(body)
451
- @env[CONTENT_LENGTH] = @chunked_content_length
480
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
452
481
  return true
453
482
  end
454
483
  end
455
484
 
485
+ # @version 5.0.0
456
486
  def write_chunk(str)
457
487
  @chunked_content_length += @body.write(str)
458
488
  end
@@ -466,7 +496,15 @@ module Puma
466
496
  chunk = chunk[@partial_part_left..-1]
467
497
  @partial_part_left = 0
468
498
  else
469
- write_chunk(chunk) if @partial_part_left > 2 # don't include the last \r\n
499
+ if @partial_part_left > 2
500
+ if @partial_part_left == chunk.size + 1
501
+ # Don't include the last \r
502
+ write_chunk(chunk[0..(@partial_part_left-3)])
503
+ else
504
+ # don't include the last \r\n
505
+ write_chunk(chunk)
506
+ end
507
+ end
470
508
  @partial_part_left -= chunk.size
471
509
  return false
472
510
  end
@@ -485,7 +523,7 @@ module Puma
485
523
  # Puma doesn't process chunk extensions, but should parse if they're
486
524
  # present, which is the reason for the semicolon regex
487
525
  chunk_hex = line.strip[/\A[^;]+/]
488
- if chunk_hex =~ CHUNK_SIZE_INVALID
526
+ if CHUNK_SIZE_INVALID.match? chunk_hex
489
527
  raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
490
528
  end
491
529
  len = chunk_hex.to_i(16)
@@ -548,7 +586,7 @@ module Puma
548
586
 
549
587
  def set_ready
550
588
  if @body_read_start
551
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
589
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
552
590
  end
553
591
  @requests_served += 1
554
592
  @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