puma 4.3.6 → 5.6.4

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1486 -518
  3. data/LICENSE +23 -20
  4. data/README.md +120 -36
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +63 -26
  7. data/docs/compile_options.md +21 -0
  8. data/docs/deployment.md +60 -69
  9. data/docs/fork_worker.md +33 -0
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/jungle/README.md +9 -0
  14. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  15. data/{tools → docs}/jungle/rc.d/puma +2 -2
  16. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  17. data/docs/kubernetes.md +66 -0
  18. data/docs/nginx.md +1 -1
  19. data/docs/plugins.md +15 -15
  20. data/docs/rails_dev_mode.md +28 -0
  21. data/docs/restart.md +46 -23
  22. data/docs/signals.md +13 -11
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +85 -128
  25. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  26. data/ext/puma_http11/ext_help.h +1 -1
  27. data/ext/puma_http11/extconf.rb +46 -9
  28. data/ext/puma_http11/http11_parser.c +68 -57
  29. data/ext/puma_http11/http11_parser.h +1 -1
  30. data/ext/puma_http11/http11_parser.java.rl +1 -1
  31. data/ext/puma_http11/http11_parser.rl +1 -1
  32. data/ext/puma_http11/http11_parser_common.rl +1 -1
  33. data/ext/puma_http11/mini_ssl.c +275 -122
  34. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  35. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  36. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +51 -51
  37. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +105 -61
  38. data/ext/puma_http11/puma_http11.c +32 -51
  39. data/lib/puma/app/status.rb +47 -36
  40. data/lib/puma/binder.rb +225 -106
  41. data/lib/puma/cli.rb +24 -18
  42. data/lib/puma/client.rb +174 -91
  43. data/lib/puma/cluster/worker.rb +173 -0
  44. data/lib/puma/cluster/worker_handle.rb +94 -0
  45. data/lib/puma/cluster.rb +212 -220
  46. data/lib/puma/commonlogger.rb +2 -2
  47. data/lib/puma/configuration.rb +58 -49
  48. data/lib/puma/const.rb +18 -9
  49. data/lib/puma/control_cli.rb +93 -76
  50. data/lib/puma/detect.rb +29 -2
  51. data/lib/puma/dsl.rb +364 -96
  52. data/lib/puma/error_logger.rb +104 -0
  53. data/lib/puma/events.rb +55 -34
  54. data/lib/puma/io_buffer.rb +9 -2
  55. data/lib/puma/jruby_restart.rb +0 -58
  56. data/lib/puma/json_serialization.rb +96 -0
  57. data/lib/puma/launcher.rb +117 -46
  58. data/lib/puma/minissl/context_builder.rb +14 -9
  59. data/lib/puma/minissl.rb +128 -46
  60. data/lib/puma/null_io.rb +13 -1
  61. data/lib/puma/plugin/tmp_restart.rb +0 -0
  62. data/lib/puma/plugin.rb +3 -12
  63. data/lib/puma/queue_close.rb +26 -0
  64. data/lib/puma/rack/builder.rb +1 -5
  65. data/lib/puma/rack/urlmap.rb +0 -0
  66. data/lib/puma/rack_default.rb +0 -0
  67. data/lib/puma/reactor.rb +85 -369
  68. data/lib/puma/request.rb +472 -0
  69. data/lib/puma/runner.rb +46 -61
  70. data/lib/puma/server.rb +287 -743
  71. data/lib/puma/single.rb +9 -65
  72. data/lib/puma/state_file.rb +47 -8
  73. data/lib/puma/systemd.rb +46 -0
  74. data/lib/puma/thread_pool.rb +125 -57
  75. data/lib/puma/util.rb +20 -1
  76. data/lib/puma.rb +46 -0
  77. data/lib/rack/handler/puma.rb +2 -3
  78. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  79. data/tools/trickletest.rb +0 -0
  80. metadata +28 -24
  81. data/docs/tcp_mode.md +0 -96
  82. data/ext/puma_http11/io_buffer.c +0 -155
  83. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  84. data/lib/puma/accept_nonblock.rb +0 -29
  85. data/lib/puma/tcp_logger.rb +0 -41
  86. data/tools/jungle/README.md +0 -19
  87. data/tools/jungle/init.d/README.md +0 -61
  88. data/tools/jungle/init.d/puma +0 -421
  89. data/tools/jungle/init.d/run-puma +0 -18
  90. data/tools/jungle/upstart/README.md +0 -61
  91. data/tools/jungle/upstart/puma-manager.conf +0 -31
  92. data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/client.rb CHANGED
@@ -23,6 +23,8 @@ module Puma
23
23
 
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
+ class HttpParserError501 < IOError; end
27
+
26
28
  # An instance of this class represents a unique request from a client.
27
29
  # For example, this could be a web request from a browser or from CURL.
28
30
  #
@@ -35,7 +37,21 @@ module Puma
35
37
  # Instances of this class are responsible for knowing if
36
38
  # the header and body are fully buffered via the `try_to_finish` method.
37
39
  # They can be used to "time out" a response via the `timeout_at` reader.
40
+ #
38
41
  class Client
42
+
43
+ # this tests all values but the last, which must be chunked
44
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
45
+
46
+ # chunked body validation
47
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
48
+ CHUNK_VALID_ENDING = "\r\n".freeze
49
+
50
+ # Content-Length header value validation
51
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
52
+
53
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
54
+
39
55
  # The object used for a request with no body. All requests with
40
56
  # no body share this one object since it has no state.
41
57
  EmptyBody = NullIO.new
@@ -56,6 +72,7 @@ module Puma
56
72
  @parser = HttpParser.new
57
73
  @parsed_bytes = 0
58
74
  @read_header = true
75
+ @read_proxy = false
59
76
  @ready = false
60
77
 
61
78
  @body = nil
@@ -69,7 +86,9 @@ module Puma
69
86
  @hijacked = false
70
87
 
71
88
  @peerip = nil
89
+ @listener = nil
72
90
  @remote_addr_header = nil
91
+ @expect_proxy_proto = false
73
92
 
74
93
  @body_remain = 0
75
94
 
@@ -81,10 +100,17 @@ module Puma
81
100
 
82
101
  attr_writer :peerip
83
102
 
84
- attr_accessor :remote_addr_header
103
+ attr_accessor :remote_addr_header, :listener
85
104
 
86
105
  def_delegators :@io, :closed?
87
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
88
114
  def inspect
89
115
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
90
116
  end
@@ -96,27 +122,36 @@ module Puma
96
122
  env[HIJACK_IO] ||= @io
97
123
  end
98
124
 
125
+ # @!attribute [r] in_data_phase
99
126
  def in_data_phase
100
- !@read_header
127
+ !(@read_header || @read_proxy)
101
128
  end
102
129
 
103
130
  def set_timeout(val)
104
- @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
105
137
  end
106
138
 
107
139
  def reset(fast_check=true)
108
140
  @parser.reset
109
141
  @read_header = true
142
+ @read_proxy = !!@expect_proxy_proto
110
143
  @env = @proto_env.dup
111
144
  @body = nil
112
145
  @tempfile = nil
113
146
  @parsed_bytes = 0
114
147
  @ready = false
115
148
  @body_remain = 0
116
- @peerip = nil
149
+ @peerip = nil if @remote_addr_header
117
150
  @in_last_chunk = false
118
151
 
119
152
  if @buffer
153
+ return false unless try_to_parse_proxy_protocol
154
+
120
155
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
121
156
 
122
157
  if @parser.finished?
@@ -129,8 +164,7 @@ module Puma
129
164
  return false
130
165
  else
131
166
  begin
132
- if fast_check &&
133
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
167
+ if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
134
168
  return try_to_finish
135
169
  end
136
170
  rescue IOError
@@ -143,19 +177,45 @@ module Puma
143
177
  def close
144
178
  begin
145
179
  @io.close
146
- rescue IOError
147
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
180
+ rescue IOError, Errno::EBADF
181
+ Puma::Util.purge_interrupt_queue
182
+ end
183
+ end
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
148
205
  end
206
+ true
149
207
  end
150
208
 
151
209
  def try_to_finish
152
- return read_body unless @read_header
210
+ return read_body if in_data_phase
153
211
 
154
212
  begin
155
213
  data = @io.read_nonblock(CHUNK_SIZE)
156
- rescue Errno::EAGAIN
214
+ rescue IO::WaitReadable
157
215
  return false
158
- rescue SystemCallError, IOError, EOFError
216
+ rescue EOFError
217
+ # Swallow error, don't log
218
+ rescue SystemCallError, IOError
159
219
  raise ConnectionError, "Connection error detected during read"
160
220
  end
161
221
 
@@ -172,6 +232,8 @@ module Puma
172
232
  @buffer = data
173
233
  end
174
234
 
235
+ return false unless try_to_parse_proxy_protocol
236
+
175
237
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
176
238
 
177
239
  if @parser.finished?
@@ -184,68 +246,20 @@ module Puma
184
246
  false
185
247
  end
186
248
 
187
- if IS_JRUBY
188
- def jruby_start_try_to_finish
189
- return read_body unless @read_header
190
-
191
- begin
192
- data = @io.sysread_nonblock(CHUNK_SIZE)
193
- rescue OpenSSL::SSL::SSLError => e
194
- return false if e.kind_of? IO::WaitReadable
195
- raise e
196
- end
197
-
198
- # No data means a closed socket
199
- unless data
200
- @buffer = nil
201
- set_ready
202
- raise EOFError
203
- end
204
-
205
- if @buffer
206
- @buffer << data
207
- else
208
- @buffer = data
209
- end
210
-
211
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
212
-
213
- if @parser.finished?
214
- return setup_body
215
- elsif @parsed_bytes >= MAX_HEADER
216
- raise HttpParserError,
217
- "HEADER is longer than allowed, aborting client early."
218
- end
219
-
220
- false
221
- end
222
-
223
- def eagerly_finish
224
- return true if @ready
225
-
226
- if @io.kind_of? OpenSSL::SSL::SSLSocket
227
- return true if jruby_start_try_to_finish
228
- end
229
-
230
- return false unless IO.select([@to_io], nil, nil, 0)
231
- try_to_finish
232
- end
233
-
234
- 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
235
254
 
236
- def eagerly_finish
237
- return true if @ready
238
- return false unless IO.select([@to_io], nil, nil, 0)
239
- try_to_finish
240
- end
241
- end # IS_JRUBY
255
+ def finish(timeout)
256
+ return if @ready
257
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
258
+ end
242
259
 
243
- def finish
244
- return true if @ready
245
- until try_to_finish
246
- IO.select([@to_io], nil, nil)
247
- end
248
- true
260
+ def timeout!
261
+ write_error(408) if in_data_phase
262
+ raise ConnectionError
249
263
  end
250
264
 
251
265
  def write_error(status_code)
@@ -259,7 +273,7 @@ module Puma
259
273
  return @peerip if @peerip
260
274
 
261
275
  if @remote_addr_header
262
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
276
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
263
277
  @peerip = hdr
264
278
  return hdr
265
279
  end
@@ -267,6 +281,26 @@ module Puma
267
281
  @peerip ||= @io.peeraddr.last
268
282
  end
269
283
 
284
+ # Returns true if the persistent connection can be closed immediately
285
+ # without waiting for the configured idle/shutdown timeout.
286
+ # @version 5.0.0
287
+ #
288
+ def can_close?
289
+ # Allow connection to close if we're not in the middle of parsing a request.
290
+ @parsed_bytes == 0
291
+ end
292
+
293
+ def expect_proxy_proto=(val)
294
+ if val
295
+ if @read_header
296
+ @read_proxy = true
297
+ end
298
+ else
299
+ @read_proxy = false
300
+ end
301
+ @expect_proxy_proto = val
302
+ end
303
+
270
304
  private
271
305
 
272
306
  def setup_body
@@ -284,16 +318,27 @@ module Puma
284
318
  body = @parser.body
285
319
 
286
320
  te = @env[TRANSFER_ENCODING2]
287
-
288
321
  if te
289
- if te.include?(",")
290
- te.split(",").each do |part|
291
- if CHUNKED.casecmp(part.strip) == 0
292
- return setup_chunked_body(body)
293
- end
322
+ te_lwr = te.downcase
323
+ if te.include? ','
324
+ te_ary = te_lwr.split ','
325
+ te_count = te_ary.count CHUNKED
326
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
327
+ if te_ary.last == CHUNKED && te_count == 1 && te_valid
328
+ @env.delete TRANSFER_ENCODING2
329
+ return setup_chunked_body body
330
+ elsif te_count >= 1
331
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
332
+ elsif !te_valid
333
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
294
334
  end
295
- elsif CHUNKED.casecmp(te) == 0
296
- return setup_chunked_body(body)
335
+ elsif te_lwr == CHUNKED
336
+ @env.delete TRANSFER_ENCODING2
337
+ return setup_chunked_body body
338
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
339
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
340
+ else
341
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
297
342
  end
298
343
  end
299
344
 
@@ -301,7 +346,12 @@ module Puma
301
346
 
302
347
  cl = @env[CONTENT_LENGTH]
303
348
 
304
- unless cl
349
+ if cl
350
+ # cannot contain characters that are not \d
351
+ if cl =~ CONTENT_LENGTH_VALUE_INVALID
352
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
353
+ end
354
+ else
305
355
  @buffer = body.empty? ? nil : body
306
356
  @body = EmptyBody
307
357
  set_ready
@@ -319,6 +369,7 @@ module Puma
319
369
 
320
370
  if remain > MAX_BODY
321
371
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
372
+ @body.unlink
322
373
  @body.binmode
323
374
  @tempfile = @body
324
375
  else
@@ -331,7 +382,7 @@ module Puma
331
382
 
332
383
  @body_remain = remain
333
384
 
334
- return false
385
+ false
335
386
  end
336
387
 
337
388
  def read_body
@@ -351,7 +402,7 @@ module Puma
351
402
 
352
403
  begin
353
404
  chunk = @io.read_nonblock(want)
354
- rescue Errno::EAGAIN
405
+ rescue IO::WaitReadable
355
406
  return false
356
407
  rescue SystemCallError, IOError
357
408
  raise ConnectionError, "Connection error detected during read"
@@ -397,7 +448,10 @@ module Puma
397
448
  raise EOFError
398
449
  end
399
450
 
400
- return true if decode_chunk(chunk)
451
+ if decode_chunk(chunk)
452
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
453
+ return true
454
+ end
401
455
  end
402
456
  end
403
457
 
@@ -407,22 +461,40 @@ module Puma
407
461
  @prev_chunk = ""
408
462
 
409
463
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
464
+ @body.unlink
410
465
  @body.binmode
411
466
  @tempfile = @body
467
+ @chunked_content_length = 0
468
+
469
+ if decode_chunk(body)
470
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
471
+ return true
472
+ end
473
+ end
412
474
 
413
- return decode_chunk(body)
475
+ # @version 5.0.0
476
+ def write_chunk(str)
477
+ @chunked_content_length += @body.write(str)
414
478
  end
415
479
 
416
480
  def decode_chunk(chunk)
417
481
  if @partial_part_left > 0
418
482
  if @partial_part_left <= chunk.size
419
483
  if @partial_part_left > 2
420
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
484
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
421
485
  end
422
486
  chunk = chunk[@partial_part_left..-1]
423
487
  @partial_part_left = 0
424
488
  else
425
- @body << chunk if @partial_part_left > 2 # don't include the last \r\n
489
+ if @partial_part_left > 2
490
+ if @partial_part_left == chunk.size + 1
491
+ # Don't include the last \r
492
+ write_chunk(chunk[0..(@partial_part_left-3)])
493
+ else
494
+ # don't include the last \r\n
495
+ write_chunk(chunk)
496
+ end
497
+ end
426
498
  @partial_part_left -= chunk.size
427
499
  return false
428
500
  end
@@ -438,7 +510,13 @@ module Puma
438
510
  while !io.eof?
439
511
  line = io.gets
440
512
  if line.end_with?("\r\n")
441
- len = line.strip.to_i(16)
513
+ # Puma doesn't process chunk extensions, but should parse if they're
514
+ # present, which is the reason for the semicolon regex
515
+ chunk_hex = line.strip[/\A[^;]+/]
516
+ if chunk_hex =~ CHUNK_SIZE_INVALID
517
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
518
+ end
519
+ len = chunk_hex.to_i(16)
442
520
  if len == 0
443
521
  @in_last_chunk = true
444
522
  @body.rewind
@@ -469,12 +547,17 @@ module Puma
469
547
 
470
548
  case
471
549
  when got == len
472
- @body << part[0..-3] # to skip the ending \r\n
550
+ # proper chunked segment must end with "\r\n"
551
+ if part.end_with? CHUNK_VALID_ENDING
552
+ write_chunk(part[0..-3]) # to skip the ending \r\n
553
+ else
554
+ raise HttpParserError, "Chunk size mismatch"
555
+ end
473
556
  when got <= len - 2
474
- @body << part
557
+ write_chunk(part)
475
558
  @partial_part_left = len - part.size
476
559
  when got == len - 1 # edge where we get just \r but not \n
477
- @body << part[0..-2]
560
+ write_chunk(part[0..-2])
478
561
  @partial_part_left = len - part.size
479
562
  end
480
563
  else
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ class Cluster < Puma::Runner
5
+ # This class is instantiated by the `Puma::Cluster` and represents a single
6
+ # worker process.
7
+ #
8
+ # At the core of this class is running an instance of `Puma::Server` which
9
+ # gets created via the `start_server` method from the `Puma::Runner` class
10
+ # that this inherits from.
11
+ class Worker < Puma::Runner
12
+ attr_reader :index, :master
13
+
14
+ def initialize(index:, master:, launcher:, pipes:, server: nil)
15
+ super launcher, launcher.events
16
+
17
+ @index = index
18
+ @master = master
19
+ @launcher = launcher
20
+ @options = launcher.options
21
+ @check_pipe = pipes[:check_pipe]
22
+ @worker_write = pipes[:worker_write]
23
+ @fork_pipe = pipes[:fork_pipe]
24
+ @wakeup = pipes[:wakeup]
25
+ @server = server
26
+ end
27
+
28
+ def run
29
+ title = "puma: cluster worker #{index}: #{master}"
30
+ title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
31
+ $0 = title
32
+
33
+ Signal.trap "SIGINT", "IGNORE"
34
+ Signal.trap "SIGCHLD", "DEFAULT"
35
+
36
+ Thread.new do
37
+ Puma.set_thread_name "wrkr check"
38
+ @check_pipe.wait_readable
39
+ log "! Detected parent died, dying"
40
+ exit! 1
41
+ end
42
+
43
+ # If we're not running under a Bundler context, then
44
+ # report the info about the context we will be using
45
+ if !ENV['BUNDLE_GEMFILE']
46
+ if File.exist?("Gemfile")
47
+ log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
48
+ elsif File.exist?("gems.rb")
49
+ log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
50
+ end
51
+ end
52
+
53
+ # Invoke any worker boot hooks so they can get
54
+ # things in shape before booting the app.
55
+ @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
56
+
57
+ begin
58
+ server = @server ||= start_server
59
+ rescue Exception => e
60
+ log "! Unable to start worker"
61
+ log e.backtrace[0]
62
+ exit 1
63
+ end
64
+
65
+ restart_server = Queue.new << true << false
66
+
67
+ fork_worker = @options[:fork_worker] && index == 0
68
+
69
+ if fork_worker
70
+ restart_server.clear
71
+ worker_pids = []
72
+ Signal.trap "SIGCHLD" do
73
+ wakeup! if worker_pids.reject! do |p|
74
+ Process.wait(p, Process::WNOHANG) rescue true
75
+ end
76
+ end
77
+
78
+ Thread.new do
79
+ Puma.set_thread_name "wrkr fork"
80
+ while (idx = @fork_pipe.gets)
81
+ idx = idx.to_i
82
+ if idx == -1 # stop server
83
+ if restart_server.length > 0
84
+ restart_server.clear
85
+ server.begin_restart(true)
86
+ @launcher.config.run_hooks :before_refork, nil, @launcher.events
87
+ Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
88
+ end
89
+ elsif idx == 0 # restart server
90
+ restart_server << true << false
91
+ else # fork worker
92
+ worker_pids << pid = spawn_worker(idx)
93
+ @worker_write << "f#{pid}:#{idx}\n" rescue nil
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ Signal.trap "SIGTERM" do
100
+ @worker_write << "e#{Process.pid}\n" rescue nil
101
+ restart_server.clear
102
+ server.stop
103
+ restart_server << false
104
+ end
105
+
106
+ begin
107
+ @worker_write << "b#{Process.pid}:#{index}\n"
108
+ rescue SystemCallError, IOError
109
+ Puma::Util.purge_interrupt_queue
110
+ STDERR.puts "Master seems to have exited, exiting."
111
+ return
112
+ end
113
+
114
+ while restart_server.pop
115
+ server_thread = server.run
116
+ stat_thread ||= Thread.new(@worker_write) do |io|
117
+ Puma.set_thread_name "stat pld"
118
+ base_payload = "p#{Process.pid}"
119
+
120
+ while true
121
+ begin
122
+ b = server.backlog || 0
123
+ r = server.running || 0
124
+ t = server.pool_capacity || 0
125
+ m = server.max_threads || 0
126
+ rc = server.requests_count || 0
127
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
128
+ io << payload
129
+ rescue IOError
130
+ Puma::Util.purge_interrupt_queue
131
+ break
132
+ end
133
+ sleep @options[:worker_check_interval]
134
+ end
135
+ end
136
+ server_thread.join
137
+ end
138
+
139
+ # Invoke any worker shutdown hooks so they can prevent the worker
140
+ # exiting until any background operations are completed
141
+ @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
142
+ ensure
143
+ @worker_write << "t#{Process.pid}\n" rescue nil
144
+ @worker_write.close
145
+ end
146
+
147
+ private
148
+
149
+ def spawn_worker(idx)
150
+ @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
151
+
152
+ pid = fork do
153
+ new_worker = Worker.new index: idx,
154
+ master: master,
155
+ launcher: @launcher,
156
+ pipes: { check_pipe: @check_pipe,
157
+ worker_write: @worker_write },
158
+ server: @server
159
+ new_worker.run
160
+ end
161
+
162
+ if !pid
163
+ log "! Complete inability to spawn new workers detected"
164
+ log "! Seppuku is the only choice."
165
+ exit! 1
166
+ end
167
+
168
+ @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
169
+ pid
170
+ end
171
+ end
172
+ end
173
+ end