puma 4.3.5 → 6.0.1

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1639 -519
  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/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 +2 -2
  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/docs/testing_benchmarks_local_files.md +150 -0
  26. data/docs/testing_test_rackup_ci_files.md +36 -0
  27. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  28. data/ext/puma_http11/ext_help.h +1 -1
  29. data/ext/puma_http11/extconf.rb +56 -11
  30. data/ext/puma_http11/http11_parser.c +69 -58
  31. data/ext/puma_http11/http11_parser.h +2 -2
  32. data/ext/puma_http11/http11_parser.java.rl +3 -3
  33. data/ext/puma_http11/http11_parser.rl +3 -3
  34. data/ext/puma_http11/http11_parser_common.rl +3 -3
  35. data/ext/puma_http11/mini_ssl.c +322 -130
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +52 -52
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
  40. data/ext/puma_http11/puma_http11.c +47 -57
  41. data/lib/puma/app/status.rb +53 -37
  42. data/lib/puma/binder.rb +232 -119
  43. data/lib/puma/cli.rb +33 -33
  44. data/lib/puma/client.rb +197 -101
  45. data/lib/puma/cluster/worker.rb +175 -0
  46. data/lib/puma/cluster/worker_handle.rb +97 -0
  47. data/lib/puma/cluster.rb +224 -229
  48. data/lib/puma/commonlogger.rb +2 -2
  49. data/lib/puma/configuration.rb +112 -87
  50. data/lib/puma/const.rb +30 -25
  51. data/lib/puma/control_cli.rb +99 -79
  52. data/lib/puma/detect.rb +31 -2
  53. data/lib/puma/dsl.rb +426 -110
  54. data/lib/puma/error_logger.rb +112 -0
  55. data/lib/puma/events.rb +16 -115
  56. data/lib/puma/io_buffer.rb +44 -2
  57. data/lib/puma/jruby_restart.rb +2 -59
  58. data/lib/puma/json_serialization.rb +96 -0
  59. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  60. data/lib/puma/launcher.rb +170 -148
  61. data/lib/puma/log_writer.rb +137 -0
  62. data/lib/puma/minissl/context_builder.rb +35 -19
  63. data/lib/puma/minissl.rb +213 -55
  64. data/lib/puma/null_io.rb +18 -1
  65. data/lib/puma/plugin/tmp_restart.rb +1 -1
  66. data/lib/puma/plugin.rb +3 -12
  67. data/lib/puma/rack/builder.rb +5 -9
  68. data/lib/puma/rack/urlmap.rb +0 -0
  69. data/lib/puma/rack_default.rb +1 -1
  70. data/lib/puma/reactor.rb +85 -369
  71. data/lib/puma/request.rb +644 -0
  72. data/lib/puma/runner.rb +83 -77
  73. data/lib/puma/server.rb +303 -773
  74. data/lib/puma/single.rb +18 -74
  75. data/lib/puma/state_file.rb +45 -8
  76. data/lib/puma/systemd.rb +47 -0
  77. data/lib/puma/thread_pool.rb +136 -68
  78. data/lib/puma/util.rb +21 -4
  79. data/lib/puma.rb +54 -5
  80. data/lib/rack/handler/puma.rb +11 -12
  81. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  82. data/tools/trickletest.rb +0 -0
  83. metadata +36 -28
  84. data/docs/tcp_mode.md +0 -96
  85. data/ext/puma_http11/io_buffer.c +0 -155
  86. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  87. data/lib/puma/accept_nonblock.rb +0 -29
  88. data/lib/puma/tcp_logger.rb +0 -41
  89. data/tools/jungle/README.md +0 -19
  90. data/tools/jungle/init.d/README.md +0 -61
  91. data/tools/jungle/init.d/puma +0 -421
  92. data/tools/jungle/init.d/run-puma +0 -18
  93. data/tools/jungle/upstart/README.md +0 -61
  94. data/tools/jungle/upstart/puma-manager.conf +0 -31
  95. 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
 
@@ -23,6 +24,11 @@ module Puma
23
24
 
24
25
  class ConnectionError < RuntimeError; end
25
26
 
27
+ class HttpParserError501 < IOError; end
28
+
29
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
30
+
31
+
26
32
  # An instance of this class represents a unique request from a client.
27
33
  # For example, this could be a web request from a browser or from CURL.
28
34
  #
@@ -35,7 +41,21 @@ module Puma
35
41
  # Instances of this class are responsible for knowing if
36
42
  # the header and body are fully buffered via the `try_to_finish` method.
37
43
  # They can be used to "time out" a response via the `timeout_at` reader.
38
- class Client
44
+ #
45
+ class Client # :nodoc:
46
+
47
+ # this tests all values but the last, which must be chunked
48
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
49
+
50
+ # chunked body validation
51
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
52
+ CHUNK_VALID_ENDING = "\r\n".freeze
53
+
54
+ # Content-Length header value validation
55
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
56
+
57
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
58
+
39
59
  # The object used for a request with no body. All requests with
40
60
  # no body share this one object since it has no state.
41
61
  EmptyBody = NullIO.new
@@ -46,16 +66,14 @@ module Puma
46
66
  def initialize(io, env=nil)
47
67
  @io = io
48
68
  @to_io = io.to_io
69
+ @io_buffer = IOBuffer.new
49
70
  @proto_env = env
50
- if !env
51
- @env = nil
52
- else
53
- @env = env.dup
54
- end
71
+ @env = env ? env.dup : nil
55
72
 
56
73
  @parser = HttpParser.new
57
74
  @parsed_bytes = 0
58
75
  @read_header = true
76
+ @read_proxy = false
59
77
  @ready = false
60
78
 
61
79
  @body = nil
@@ -69,7 +87,10 @@ module Puma
69
87
  @hijacked = false
70
88
 
71
89
  @peerip = nil
90
+ @peer_family = nil
91
+ @listener = nil
72
92
  @remote_addr_header = nil
93
+ @expect_proxy_proto = false
73
94
 
74
95
  @body_remain = 0
75
96
 
@@ -77,14 +98,21 @@ module Puma
77
98
  end
78
99
 
79
100
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
80
- :tempfile
101
+ :tempfile, :io_buffer
81
102
 
82
103
  attr_writer :peerip
83
104
 
84
- attr_accessor :remote_addr_header
105
+ attr_accessor :remote_addr_header, :listener
85
106
 
86
107
  def_delegators :@io, :closed?
87
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
88
116
  def inspect
89
117
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
90
118
  end
@@ -96,27 +124,37 @@ module Puma
96
124
  env[HIJACK_IO] ||= @io
97
125
  end
98
126
 
127
+ # @!attribute [r] in_data_phase
99
128
  def in_data_phase
100
- !@read_header
129
+ !(@read_header || @read_proxy)
101
130
  end
102
131
 
103
132
  def set_timeout(val)
104
- @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
105
139
  end
106
140
 
107
141
  def reset(fast_check=true)
108
142
  @parser.reset
143
+ @io_buffer.reset
109
144
  @read_header = true
145
+ @read_proxy = !!@expect_proxy_proto
110
146
  @env = @proto_env.dup
111
147
  @body = nil
112
148
  @tempfile = nil
113
149
  @parsed_bytes = 0
114
150
  @ready = false
115
151
  @body_remain = 0
116
- @peerip = nil
152
+ @peerip = nil if @remote_addr_header
117
153
  @in_last_chunk = false
118
154
 
119
155
  if @buffer
156
+ return false unless try_to_parse_proxy_protocol
157
+
120
158
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
121
159
 
122
160
  if @parser.finished?
@@ -129,8 +167,7 @@ module Puma
129
167
  return false
130
168
  else
131
169
  begin
132
- if fast_check &&
133
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
170
+ if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
134
171
  return try_to_finish
135
172
  end
136
173
  rescue IOError
@@ -143,19 +180,45 @@ module Puma
143
180
  def close
144
181
  begin
145
182
  @io.close
146
- rescue IOError
147
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
183
+ rescue IOError, Errno::EBADF
184
+ Puma::Util.purge_interrupt_queue
148
185
  end
149
186
  end
150
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
+
151
212
  def try_to_finish
152
- return read_body unless @read_header
213
+ return read_body if in_data_phase
153
214
 
154
215
  begin
155
216
  data = @io.read_nonblock(CHUNK_SIZE)
156
- rescue Errno::EAGAIN
217
+ rescue IO::WaitReadable
157
218
  return false
158
- rescue SystemCallError, IOError, EOFError
219
+ rescue EOFError
220
+ # Swallow error, don't log
221
+ rescue SystemCallError, IOError
159
222
  raise ConnectionError, "Connection error detected during read"
160
223
  end
161
224
 
@@ -172,6 +235,8 @@ module Puma
172
235
  @buffer = data
173
236
  end
174
237
 
238
+ return false unless try_to_parse_proxy_protocol
239
+
175
240
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
176
241
 
177
242
  if @parser.finished?
@@ -184,68 +249,20 @@ module Puma
184
249
  false
185
250
  end
186
251
 
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
252
+ def eagerly_finish
253
+ return true if @ready
254
+ return false unless @to_io.wait_readable(0)
255
+ try_to_finish
256
+ end
235
257
 
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
258
+ def finish(timeout)
259
+ return if @ready
260
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
261
+ end
242
262
 
243
- def finish
244
- return true if @ready
245
- until try_to_finish
246
- IO.select([@to_io], nil, nil)
247
- end
248
- true
263
+ def timeout!
264
+ write_error(408) if in_data_phase
265
+ raise ConnectionError
249
266
  end
250
267
 
251
268
  def write_error(status_code)
@@ -259,7 +276,7 @@ module Puma
259
276
  return @peerip if @peerip
260
277
 
261
278
  if @remote_addr_header
262
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
279
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
263
280
  @peerip = hdr
264
281
  return hdr
265
282
  end
@@ -267,10 +284,40 @@ module Puma
267
284
  @peerip ||= @io.peeraddr.last
268
285
  end
269
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
+
270
317
  private
271
318
 
272
319
  def setup_body
273
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
320
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
274
321
 
275
322
  if @env[HTTP_EXPECT] == CONTINUE
276
323
  # TODO allow a hook here to check the headers before
@@ -284,16 +331,27 @@ module Puma
284
331
  body = @parser.body
285
332
 
286
333
  te = @env[TRANSFER_ENCODING2]
287
-
288
334
  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
335
+ te_lwr = te.downcase
336
+ if te.include? ','
337
+ te_ary = te_lwr.split ','
338
+ te_count = te_ary.count CHUNKED
339
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
340
+ if te_ary.last == CHUNKED && te_count == 1 && te_valid
341
+ @env.delete TRANSFER_ENCODING2
342
+ return setup_chunked_body body
343
+ elsif te_count >= 1
344
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
345
+ elsif !te_valid
346
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
294
347
  end
295
- elsif CHUNKED.casecmp(te) == 0
296
- return setup_chunked_body(body)
348
+ elsif te_lwr == CHUNKED
349
+ @env.delete TRANSFER_ENCODING2
350
+ return setup_chunked_body body
351
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
352
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
353
+ else
354
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
297
355
  end
298
356
  end
299
357
 
@@ -301,7 +359,12 @@ module Puma
301
359
 
302
360
  cl = @env[CONTENT_LENGTH]
303
361
 
304
- unless cl
362
+ if cl
363
+ # cannot contain characters that are not \d
364
+ if CONTENT_LENGTH_VALUE_INVALID.match? cl
365
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
366
+ end
367
+ else
305
368
  @buffer = body.empty? ? nil : body
306
369
  @body = EmptyBody
307
370
  set_ready
@@ -319,6 +382,7 @@ module Puma
319
382
 
320
383
  if remain > MAX_BODY
321
384
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
385
+ @body.unlink
322
386
  @body.binmode
323
387
  @tempfile = @body
324
388
  else
@@ -331,7 +395,7 @@ module Puma
331
395
 
332
396
  @body_remain = remain
333
397
 
334
- return false
398
+ false
335
399
  end
336
400
 
337
401
  def read_body
@@ -351,7 +415,7 @@ module Puma
351
415
 
352
416
  begin
353
417
  chunk = @io.read_nonblock(want)
354
- rescue Errno::EAGAIN
418
+ rescue IO::WaitReadable
355
419
  return false
356
420
  rescue SystemCallError, IOError
357
421
  raise ConnectionError, "Connection error detected during read"
@@ -397,7 +461,10 @@ module Puma
397
461
  raise EOFError
398
462
  end
399
463
 
400
- return true if decode_chunk(chunk)
464
+ if decode_chunk(chunk)
465
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
466
+ return true
467
+ end
401
468
  end
402
469
  end
403
470
 
@@ -407,22 +474,40 @@ module Puma
407
474
  @prev_chunk = ""
408
475
 
409
476
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
477
+ @body.unlink
410
478
  @body.binmode
411
479
  @tempfile = @body
480
+ @chunked_content_length = 0
412
481
 
413
- return decode_chunk(body)
482
+ if decode_chunk(body)
483
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
484
+ return true
485
+ end
486
+ end
487
+
488
+ # @version 5.0.0
489
+ def write_chunk(str)
490
+ @chunked_content_length += @body.write(str)
414
491
  end
415
492
 
416
493
  def decode_chunk(chunk)
417
494
  if @partial_part_left > 0
418
495
  if @partial_part_left <= chunk.size
419
496
  if @partial_part_left > 2
420
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
497
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
421
498
  end
422
499
  chunk = chunk[@partial_part_left..-1]
423
500
  @partial_part_left = 0
424
501
  else
425
- @body << 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
426
511
  @partial_part_left -= chunk.size
427
512
  return false
428
513
  end
@@ -438,7 +523,13 @@ module Puma
438
523
  while !io.eof?
439
524
  line = io.gets
440
525
  if line.end_with?("\r\n")
441
- len = line.strip.to_i(16)
526
+ # Puma doesn't process chunk extensions, but should parse if they're
527
+ # present, which is the reason for the semicolon regex
528
+ chunk_hex = line.strip[/\A[^;]+/]
529
+ if CHUNK_SIZE_INVALID.match? chunk_hex
530
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
531
+ end
532
+ len = chunk_hex.to_i(16)
442
533
  if len == 0
443
534
  @in_last_chunk = true
444
535
  @body.rewind
@@ -469,12 +560,17 @@ module Puma
469
560
 
470
561
  case
471
562
  when got == len
472
- @body << part[0..-3] # to skip the ending \r\n
563
+ # proper chunked segment must end with "\r\n"
564
+ if part.end_with? CHUNK_VALID_ENDING
565
+ write_chunk(part[0..-3]) # to skip the ending \r\n
566
+ else
567
+ raise HttpParserError, "Chunk size mismatch"
568
+ end
473
569
  when got <= len - 2
474
- @body << part
570
+ write_chunk(part)
475
571
  @partial_part_left = len - part.size
476
572
  when got == len - 1 # edge where we get just \r but not \n
477
- @body << part[0..-2]
573
+ write_chunk(part[0..-2])
478
574
  @partial_part_left = len - part.size
479
575
  end
480
576
  else
@@ -493,7 +589,7 @@ module Puma
493
589
 
494
590
  def set_ready
495
591
  if @body_read_start
496
- @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
497
593
  end
498
594
  @requests_served += 1
499
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