puma 4.3.8 → 5.6.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1543 -521
  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/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/docs/kubernetes.md +66 -0
  14. data/docs/nginx.md +1 -1
  15. data/docs/plugins.md +15 -15
  16. data/docs/rails_dev_mode.md +28 -0
  17. data/docs/restart.md +46 -23
  18. data/docs/signals.md +13 -11
  19. data/docs/stats.md +142 -0
  20. data/docs/systemd.md +85 -128
  21. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  22. data/ext/puma_http11/ext_help.h +1 -1
  23. data/ext/puma_http11/extconf.rb +51 -9
  24. data/ext/puma_http11/http11_parser.c +68 -57
  25. data/ext/puma_http11/http11_parser.h +1 -1
  26. data/ext/puma_http11/http11_parser.java.rl +1 -1
  27. data/ext/puma_http11/http11_parser.rl +1 -1
  28. data/ext/puma_http11/http11_parser_common.rl +1 -1
  29. data/ext/puma_http11/mini_ssl.c +295 -124
  30. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  31. data/ext/puma_http11/org/jruby/puma/Http11.java +5 -3
  32. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +51 -51
  33. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +109 -67
  34. data/ext/puma_http11/puma_http11.c +32 -51
  35. data/lib/puma/app/status.rb +50 -36
  36. data/lib/puma/binder.rb +225 -106
  37. data/lib/puma/cli.rb +24 -18
  38. data/lib/puma/client.rb +197 -92
  39. data/lib/puma/cluster/worker.rb +173 -0
  40. data/lib/puma/cluster/worker_handle.rb +94 -0
  41. data/lib/puma/cluster.rb +212 -220
  42. data/lib/puma/commonlogger.rb +2 -2
  43. data/lib/puma/configuration.rb +58 -49
  44. data/lib/puma/const.rb +26 -9
  45. data/lib/puma/control_cli.rb +99 -76
  46. data/lib/puma/detect.rb +29 -2
  47. data/lib/puma/dsl.rb +368 -96
  48. data/lib/puma/error_logger.rb +104 -0
  49. data/lib/puma/events.rb +55 -34
  50. data/lib/puma/io_buffer.rb +9 -2
  51. data/lib/puma/jruby_restart.rb +0 -58
  52. data/lib/puma/json_serialization.rb +96 -0
  53. data/lib/puma/launcher.rb +128 -46
  54. data/lib/puma/minissl/context_builder.rb +14 -9
  55. data/lib/puma/minissl.rb +137 -50
  56. data/lib/puma/null_io.rb +18 -1
  57. data/lib/puma/plugin.rb +3 -12
  58. data/lib/puma/queue_close.rb +26 -0
  59. data/lib/puma/rack/builder.rb +1 -5
  60. data/lib/puma/reactor.rb +85 -369
  61. data/lib/puma/request.rb +489 -0
  62. data/lib/puma/runner.rb +46 -61
  63. data/lib/puma/server.rb +292 -751
  64. data/lib/puma/single.rb +9 -65
  65. data/lib/puma/state_file.rb +48 -8
  66. data/lib/puma/systemd.rb +46 -0
  67. data/lib/puma/thread_pool.rb +125 -57
  68. data/lib/puma/util.rb +32 -4
  69. data/lib/puma.rb +48 -0
  70. data/lib/rack/handler/puma.rb +2 -3
  71. data/lib/rack/version_restriction.rb +15 -0
  72. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  73. metadata +29 -24
  74. data/docs/tcp_mode.md +0 -96
  75. data/ext/puma_http11/io_buffer.c +0 -155
  76. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  77. data/lib/puma/accept_nonblock.rb +0 -29
  78. data/lib/puma/tcp_logger.rb +0 -41
  79. data/tools/jungle/README.md +0 -19
  80. data/tools/jungle/init.d/README.md +0 -61
  81. data/tools/jungle/init.d/puma +0 -421
  82. data/tools/jungle/init.d/run-puma +0 -18
  83. data/tools/jungle/upstart/README.md +0 -61
  84. data/tools/jungle/upstart/puma-manager.conf +0 -31
  85. data/tools/jungle/upstart/puma.conf +0 -69
  86. /data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
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,30 @@ 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 = Const::LINE_END
49
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
50
+
51
+ # The maximum number of bytes we'll buffer looking for a valid
52
+ # chunk header.
53
+ MAX_CHUNK_HEADER_SIZE = 4096
54
+
55
+ # The maximum amount of excess data the client sends
56
+ # using chunk size extensions before we abort the connection.
57
+ MAX_CHUNK_EXCESS = 16 * 1024
58
+
59
+ # Content-Length header value validation
60
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
61
+
62
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
63
+
39
64
  # The object used for a request with no body. All requests with
40
65
  # no body share this one object since it has no state.
41
66
  EmptyBody = NullIO.new
@@ -56,6 +81,7 @@ module Puma
56
81
  @parser = HttpParser.new
57
82
  @parsed_bytes = 0
58
83
  @read_header = true
84
+ @read_proxy = false
59
85
  @ready = false
60
86
 
61
87
  @body = nil
@@ -69,7 +95,9 @@ module Puma
69
95
  @hijacked = false
70
96
 
71
97
  @peerip = nil
98
+ @listener = nil
72
99
  @remote_addr_header = nil
100
+ @expect_proxy_proto = false
73
101
 
74
102
  @body_remain = 0
75
103
 
@@ -81,10 +109,17 @@ module Puma
81
109
 
82
110
  attr_writer :peerip
83
111
 
84
- attr_accessor :remote_addr_header
112
+ attr_accessor :remote_addr_header, :listener
85
113
 
86
114
  def_delegators :@io, :closed?
87
115
 
116
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
117
+ # used for MiniSSL::Socket
118
+ def io_ok?
119
+ @to_io.is_a?(::BasicSocket) && !closed?
120
+ end
121
+
122
+ # @!attribute [r] inspect
88
123
  def inspect
89
124
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
90
125
  end
@@ -96,27 +131,36 @@ module Puma
96
131
  env[HIJACK_IO] ||= @io
97
132
  end
98
133
 
134
+ # @!attribute [r] in_data_phase
99
135
  def in_data_phase
100
- !@read_header
136
+ !(@read_header || @read_proxy)
101
137
  end
102
138
 
103
139
  def set_timeout(val)
104
- @timeout_at = Time.now + val
140
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
141
+ end
142
+
143
+ # Number of seconds until the timeout elapses.
144
+ def timeout
145
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
105
146
  end
106
147
 
107
148
  def reset(fast_check=true)
108
149
  @parser.reset
109
150
  @read_header = true
151
+ @read_proxy = !!@expect_proxy_proto
110
152
  @env = @proto_env.dup
111
153
  @body = nil
112
154
  @tempfile = nil
113
155
  @parsed_bytes = 0
114
156
  @ready = false
115
157
  @body_remain = 0
116
- @peerip = nil
158
+ @peerip = nil if @remote_addr_header
117
159
  @in_last_chunk = false
118
160
 
119
161
  if @buffer
162
+ return false unless try_to_parse_proxy_protocol
163
+
120
164
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
121
165
 
122
166
  if @parser.finished?
@@ -129,8 +173,7 @@ module Puma
129
173
  return false
130
174
  else
131
175
  begin
132
- if fast_check &&
133
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
176
+ if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
134
177
  return try_to_finish
135
178
  end
136
179
  rescue IOError
@@ -143,19 +186,45 @@ module Puma
143
186
  def close
144
187
  begin
145
188
  @io.close
146
- rescue IOError
147
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
189
+ rescue IOError, Errno::EBADF
190
+ Puma::Util.purge_interrupt_queue
148
191
  end
149
192
  end
150
193
 
194
+ # If necessary, read the PROXY protocol from the buffer. Returns
195
+ # false if more data is needed.
196
+ def try_to_parse_proxy_protocol
197
+ if @read_proxy
198
+ if @expect_proxy_proto == :v1
199
+ if @buffer.include? "\r\n"
200
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
201
+ if md[1]
202
+ @peerip = md[1].split(" ")[0]
203
+ end
204
+ @buffer = md.post_match
205
+ end
206
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
207
+ # request, this is just HTTP from a non-PROXY client; move on
208
+ @read_proxy = false
209
+ return @buffer.size > 0
210
+ else
211
+ return false
212
+ end
213
+ end
214
+ end
215
+ true
216
+ end
217
+
151
218
  def try_to_finish
152
- return read_body unless @read_header
219
+ return read_body if in_data_phase
153
220
 
154
221
  begin
155
222
  data = @io.read_nonblock(CHUNK_SIZE)
156
223
  rescue IO::WaitReadable
157
224
  return false
158
- rescue SystemCallError, IOError, EOFError
225
+ rescue EOFError
226
+ # Swallow error, don't log
227
+ rescue SystemCallError, IOError
159
228
  raise ConnectionError, "Connection error detected during read"
160
229
  end
161
230
 
@@ -172,6 +241,8 @@ module Puma
172
241
  @buffer = data
173
242
  end
174
243
 
244
+ return false unless try_to_parse_proxy_protocol
245
+
175
246
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
176
247
 
177
248
  if @parser.finished?
@@ -184,68 +255,20 @@ module Puma
184
255
  false
185
256
  end
186
257
 
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
258
+ def eagerly_finish
259
+ return true if @ready
260
+ return false unless @to_io.wait_readable(0)
261
+ try_to_finish
262
+ end
235
263
 
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
264
+ def finish(timeout)
265
+ return if @ready
266
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
267
+ end
242
268
 
243
- def finish
244
- return true if @ready
245
- until try_to_finish
246
- IO.select([@to_io], nil, nil)
247
- end
248
- true
269
+ def timeout!
270
+ write_error(408) if in_data_phase
271
+ raise ConnectionError
249
272
  end
250
273
 
251
274
  def write_error(status_code)
@@ -259,7 +282,7 @@ module Puma
259
282
  return @peerip if @peerip
260
283
 
261
284
  if @remote_addr_header
262
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
285
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
263
286
  @peerip = hdr
264
287
  return hdr
265
288
  end
@@ -267,6 +290,26 @@ module Puma
267
290
  @peerip ||= @io.peeraddr.last
268
291
  end
269
292
 
293
+ # Returns true if the persistent connection can be closed immediately
294
+ # without waiting for the configured idle/shutdown timeout.
295
+ # @version 5.0.0
296
+ #
297
+ def can_close?
298
+ # Allow connection to close if we're not in the middle of parsing a request.
299
+ @parsed_bytes == 0
300
+ end
301
+
302
+ def expect_proxy_proto=(val)
303
+ if val
304
+ if @read_header
305
+ @read_proxy = true
306
+ end
307
+ else
308
+ @read_proxy = false
309
+ end
310
+ @expect_proxy_proto = val
311
+ end
312
+
270
313
  private
271
314
 
272
315
  def setup_body
@@ -284,16 +327,27 @@ module Puma
284
327
  body = @parser.body
285
328
 
286
329
  te = @env[TRANSFER_ENCODING2]
287
-
288
330
  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
331
+ te_lwr = te.downcase
332
+ if te.include? ','
333
+ te_ary = te_lwr.split ','
334
+ te_count = te_ary.count CHUNKED
335
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
336
+ if te_ary.last == CHUNKED && te_count == 1 && te_valid
337
+ @env.delete TRANSFER_ENCODING2
338
+ return setup_chunked_body body
339
+ elsif te_count >= 1
340
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
341
+ elsif !te_valid
342
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
294
343
  end
295
- elsif CHUNKED.casecmp(te) == 0
296
- return setup_chunked_body(body)
344
+ elsif te_lwr == CHUNKED
345
+ @env.delete TRANSFER_ENCODING2
346
+ return setup_chunked_body body
347
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
348
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
349
+ else
350
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
297
351
  end
298
352
  end
299
353
 
@@ -301,7 +355,12 @@ module Puma
301
355
 
302
356
  cl = @env[CONTENT_LENGTH]
303
357
 
304
- unless cl
358
+ if cl
359
+ # cannot contain characters that are not \d, or be empty
360
+ if cl =~ CONTENT_LENGTH_VALUE_INVALID || cl.empty?
361
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
362
+ end
363
+ else
305
364
  @buffer = body.empty? ? nil : body
306
365
  @body = EmptyBody
307
366
  set_ready
@@ -319,6 +378,7 @@ module Puma
319
378
 
320
379
  if remain > MAX_BODY
321
380
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
381
+ @body.unlink
322
382
  @body.binmode
323
383
  @tempfile = @body
324
384
  else
@@ -331,7 +391,7 @@ module Puma
331
391
 
332
392
  @body_remain = remain
333
393
 
334
- return false
394
+ false
335
395
  end
336
396
 
337
397
  def read_body
@@ -398,7 +458,7 @@ module Puma
398
458
  end
399
459
 
400
460
  if decode_chunk(chunk)
401
- @env[CONTENT_LENGTH] = @chunked_content_length
461
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
402
462
  return true
403
463
  end
404
464
  end
@@ -408,19 +468,21 @@ module Puma
408
468
  @chunked_body = true
409
469
  @partial_part_left = 0
410
470
  @prev_chunk = ""
471
+ @excess_cr = 0
411
472
 
412
473
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
474
+ @body.unlink
413
475
  @body.binmode
414
476
  @tempfile = @body
415
-
416
477
  @chunked_content_length = 0
417
478
 
418
479
  if decode_chunk(body)
419
- @env[CONTENT_LENGTH] = @chunked_content_length
480
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
420
481
  return true
421
482
  end
422
483
  end
423
484
 
485
+ # @version 5.0.0
424
486
  def write_chunk(str)
425
487
  @chunked_content_length += @body.write(str)
426
488
  end
@@ -434,7 +496,15 @@ module Puma
434
496
  chunk = chunk[@partial_part_left..-1]
435
497
  @partial_part_left = 0
436
498
  else
437
- 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
438
508
  @partial_part_left -= chunk.size
439
509
  return false
440
510
  end
@@ -449,25 +519,51 @@ module Puma
449
519
 
450
520
  while !io.eof?
451
521
  line = io.gets
452
- if line.end_with?("\r\n")
453
- len = line.strip.to_i(16)
522
+ if line.end_with?(CHUNK_VALID_ENDING)
523
+ # Puma doesn't process chunk extensions, but should parse if they're
524
+ # present, which is the reason for the semicolon regex
525
+ chunk_hex = line.strip[/\A[^;]+/]
526
+ if chunk_hex =~ CHUNK_SIZE_INVALID
527
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
528
+ end
529
+ len = chunk_hex.to_i(16)
454
530
  if len == 0
455
531
  @in_last_chunk = true
456
532
  @body.rewind
457
533
  rest = io.read
458
- last_crlf_size = "\r\n".bytesize
459
- if rest.bytesize < last_crlf_size
534
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
460
535
  @buffer = nil
461
- @partial_part_left = last_crlf_size - rest.bytesize
536
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
462
537
  return false
463
538
  else
464
- @buffer = rest[last_crlf_size..-1]
539
+ # if the next character is a CRLF, set buffer to everything after that CRLF
540
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
541
+ CHUNK_VALID_ENDING_SIZE
542
+ else # we have started a trailer section, which we do not support. skip it!
543
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
544
+ end
545
+
546
+ @buffer = rest[start_of_rest..-1]
465
547
  @buffer = nil if @buffer.empty?
466
548
  set_ready
467
549
  return true
468
550
  end
469
551
  end
470
552
 
553
+ # Track the excess as a function of the size of the
554
+ # header vs the size of the actual data. Excess can
555
+ # go negative (and is expected to) when the body is
556
+ # significant.
557
+ # The additional of chunk_hex.size and 2 compensates
558
+ # for a client sending 1 byte in a chunked body over
559
+ # a long period of time, making sure that that client
560
+ # isn't accidentally eventually punished.
561
+ @excess_cr += (line.size - len - chunk_hex.size - 2)
562
+
563
+ if @excess_cr >= MAX_CHUNK_EXCESS
564
+ raise HttpParserError, "Maximum chunk excess detected"
565
+ end
566
+
471
567
  len += 2
472
568
 
473
569
  part = io.read(len)
@@ -481,7 +577,12 @@ module Puma
481
577
 
482
578
  case
483
579
  when got == len
484
- write_chunk(part[0..-3]) # to skip the ending \r\n
580
+ # proper chunked segment must end with "\r\n"
581
+ if part.end_with? CHUNK_VALID_ENDING
582
+ write_chunk(part[0..-3]) # to skip the ending \r\n
583
+ else
584
+ raise HttpParserError, "Chunk size mismatch"
585
+ end
485
586
  when got <= len - 2
486
587
  write_chunk(part)
487
588
  @partial_part_left = len - part.size
@@ -490,6 +591,10 @@ module Puma
490
591
  @partial_part_left = len - part.size
491
592
  end
492
593
  else
594
+ if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
595
+ raise HttpParserError, "maximum size of chunk header exceeded"
596
+ end
597
+
493
598
  @prev_chunk = line
494
599
  return false
495
600
  end
@@ -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