puma 3.12.1 → 4.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.

@@ -23,6 +23,7 @@ import javax.net.ssl.SSLPeerUnverifiedException;
23
23
  import javax.net.ssl.SSLSession;
24
24
  import java.io.FileInputStream;
25
25
  import java.io.IOException;
26
+ import java.nio.Buffer;
26
27
  import java.nio.ByteBuffer;
27
28
  import java.security.KeyManagementException;
28
29
  import java.security.KeyStore;
@@ -65,7 +66,7 @@ public class MiniSSL extends RubyObject {
65
66
 
66
67
  public void clear() { buffer.clear(); }
67
68
  public void compact() { buffer.compact(); }
68
- public void flip() { buffer.flip(); }
69
+ public void flip() { ((Buffer) buffer).flip(); }
69
70
  public boolean hasRemaining() { return buffer.hasRemaining(); }
70
71
  public int position() { return buffer.position(); }
71
72
 
@@ -89,7 +90,7 @@ public class MiniSSL extends RubyObject {
89
90
  public void resize(int newCapacity) {
90
91
  if (newCapacity > buffer.capacity()) {
91
92
  ByteBuffer dstTmp = ByteBuffer.allocate(newCapacity);
92
- buffer.flip();
93
+ flip();
93
94
  dstTmp.put(buffer);
94
95
  buffer = dstTmp;
95
96
  } else {
@@ -101,7 +102,7 @@ public class MiniSSL extends RubyObject {
101
102
  * Drains the buffer to a ByteList, or returns null for an empty buffer
102
103
  */
103
104
  public ByteList asByteList() {
104
- buffer.flip();
105
+ flip();
105
106
  if (!buffer.hasRemaining()) {
106
107
  buffer.clear();
107
108
  return null;
@@ -158,7 +159,13 @@ public class MiniSSL extends RubyObject {
158
159
  sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
159
160
  engine = sslCtx.createSSLEngine();
160
161
 
161
- String[] protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
162
+ String[] protocols;
163
+ if(miniSSLContext.callMethod(threadContext, "no_tlsv1").isTrue()) {
164
+ protocols = new String[] { "TLSv1.1", "TLSv1.2" };
165
+ } else {
166
+ protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
167
+ }
168
+
162
169
  engine.setEnabledProtocols(protocols);
163
170
  engine.setUseClientMode(false);
164
171
 
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+
1
3
  module Puma
2
4
  module App
3
5
  class Status
@@ -60,8 +62,7 @@ module Puma
60
62
  return rack_response(200, OK_STATUS)
61
63
 
62
64
  when /\/gc-stats$/
63
- json = "{" + GC.stat.map { |k, v| "\"#{k}\": #{v}" }.join(",") + "}"
64
- return rack_response(200, json)
65
+ return rack_response(200, GC.stat.to_json)
65
66
 
66
67
  when /\/stats$/
67
68
  return rack_response(200, @cli.stats)
data/lib/puma/binder.rb CHANGED
@@ -50,7 +50,14 @@ module Puma
50
50
 
51
51
  def close
52
52
  @ios.each { |i| i.close }
53
- @unix_paths.each { |i| File.unlink i }
53
+ @unix_paths.each do |i|
54
+ # Errno::ENOENT is intermittently raised
55
+ begin
56
+ unix_socket = UNIXSocket.new i
57
+ unix_socket.close
58
+ rescue Errno::ENOENT
59
+ end
60
+ end
54
61
  end
55
62
 
56
63
  def import_from_env
@@ -187,6 +194,8 @@ module Puma
187
194
  ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
188
195
  end
189
196
 
197
+ ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
198
+
190
199
  if params['verify_mode']
191
200
  ctx.verify_mode = case params['verify_mode']
192
201
  when "peer"
data/lib/puma/client.rb CHANGED
@@ -27,9 +27,10 @@ module Puma
27
27
  # For example a web request from a browser or from CURL. This
28
28
  #
29
29
  # An instance of `Puma::Client` can be used as if it were an IO object
30
- # for example it is passed into `IO.select` inside of the `Puma::Reactor`.
31
- # This is accomplished by the `to_io` method which gets called on any
32
- # non-IO objects being used with the IO api such as `IO.select.
30
+ # by the reactor, that's because the latter is expected to call `#to_io`
31
+ # on any non-IO objects it polls. For example nio4r internally calls
32
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
33
+ # registered.
33
34
  #
34
35
  # Instances of this class are responsible for knowing if
35
36
  # the header and body are fully buffered via the `try_to_finish` method.
@@ -54,6 +55,7 @@ module Puma
54
55
  @ready = false
55
56
 
56
57
  @body = nil
58
+ @body_read_start = nil
57
59
  @buffer = nil
58
60
  @tempfile = nil
59
61
 
@@ -64,6 +66,8 @@ module Puma
64
66
 
65
67
  @peerip = nil
66
68
  @remote_addr_header = nil
69
+
70
+ @body_remain = 0
67
71
  end
68
72
 
69
73
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -102,6 +106,8 @@ module Puma
102
106
  @tempfile = nil
103
107
  @parsed_bytes = 0
104
108
  @ready = false
109
+ @body_remain = 0
110
+ @peerip = nil
105
111
 
106
112
  if @buffer
107
113
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -114,9 +120,16 @@ module Puma
114
120
  end
115
121
 
116
122
  return false
117
- elsif fast_check &&
118
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
119
- return try_to_finish
123
+ else
124
+ begin
125
+ if fast_check &&
126
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
127
+ return try_to_finish
128
+ end
129
+ rescue IOError
130
+ # swallow it
131
+ end
132
+
120
133
  end
121
134
  end
122
135
 
@@ -147,8 +160,11 @@ module Puma
147
160
  def decode_chunk(chunk)
148
161
  if @partial_part_left > 0
149
162
  if @partial_part_left <= chunk.size
150
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
163
+ if @partial_part_left > 2
164
+ @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
165
+ end
151
166
  chunk = chunk[@partial_part_left..-1]
167
+ @partial_part_left = 0
152
168
  else
153
169
  @body << chunk
154
170
  @partial_part_left -= chunk.size
@@ -172,8 +188,7 @@ module Puma
172
188
  rest = io.read
173
189
  rest = rest[2..-1] if rest.start_with?("\r\n")
174
190
  @buffer = rest.empty? ? nil : rest
175
- @requests_served += 1
176
- @ready = true
191
+ set_ready
177
192
  return true
178
193
  end
179
194
 
@@ -211,7 +226,7 @@ module Puma
211
226
  while true
212
227
  begin
213
228
  chunk = @io.read_nonblock(4096)
214
- rescue Errno::EAGAIN
229
+ rescue IO::WaitReadable
215
230
  return false
216
231
  rescue SystemCallError, IOError
217
232
  raise ConnectionError, "Connection error detected during read"
@@ -221,8 +236,7 @@ module Puma
221
236
  unless chunk
222
237
  @body.close
223
238
  @buffer = nil
224
- @requests_served += 1
225
- @ready = true
239
+ set_ready
226
240
  raise EOFError
227
241
  end
228
242
 
@@ -231,6 +245,8 @@ module Puma
231
245
  end
232
246
 
233
247
  def setup_body
248
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
249
+
234
250
  if @env[HTTP_EXPECT] == CONTINUE
235
251
  # TODO allow a hook here to check the headers before
236
252
  # going forward
@@ -255,8 +271,7 @@ module Puma
255
271
  unless cl
256
272
  @buffer = body.empty? ? nil : body
257
273
  @body = EmptyBody
258
- @requests_served += 1
259
- @ready = true
274
+ set_ready
260
275
  return true
261
276
  end
262
277
 
@@ -265,8 +280,7 @@ module Puma
265
280
  if remain <= 0
266
281
  @body = StringIO.new(body)
267
282
  @buffer = nil
268
- @requests_served += 1
269
- @ready = true
283
+ set_ready
270
284
  return true
271
285
  end
272
286
 
@@ -294,15 +308,14 @@ module Puma
294
308
  data = @io.read_nonblock(CHUNK_SIZE)
295
309
  rescue Errno::EAGAIN
296
310
  return false
297
- rescue SystemCallError, IOError
311
+ rescue SystemCallError, IOError, EOFError
298
312
  raise ConnectionError, "Connection error detected during read"
299
313
  end
300
314
 
301
315
  # No data means a closed socket
302
316
  unless data
303
317
  @buffer = nil
304
- @requests_served += 1
305
- @ready = true
318
+ set_ready
306
319
  raise EOFError
307
320
  end
308
321
 
@@ -338,8 +351,7 @@ module Puma
338
351
  # No data means a closed socket
339
352
  unless data
340
353
  @buffer = nil
341
- @requests_served += 1
342
- @ready = true
354
+ set_ready
343
355
  raise EOFError
344
356
  end
345
357
 
@@ -416,8 +428,7 @@ module Puma
416
428
  unless chunk
417
429
  @body.close
418
430
  @buffer = nil
419
- @requests_served += 1
420
- @ready = true
431
+ set_ready
421
432
  raise EOFError
422
433
  end
423
434
 
@@ -426,8 +437,7 @@ module Puma
426
437
  if remain <= 0
427
438
  @body.rewind
428
439
  @buffer = nil
429
- @requests_served += 1
430
- @ready = true
440
+ set_ready
431
441
  return true
432
442
  end
433
443
 
@@ -436,6 +446,14 @@ module Puma
436
446
  false
437
447
  end
438
448
 
449
+ def set_ready
450
+ if @body_read_start
451
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
452
+ end
453
+ @requests_served += 1
454
+ @ready = true
455
+ end
456
+
439
457
  def write_400
440
458
  begin
441
459
  @io << ERROR_400_RESPONSE
data/lib/puma/cluster.rb CHANGED
@@ -19,8 +19,6 @@ module Puma
19
19
  # via the `spawn_workers` method call. Each worker will have it's own
20
20
  # instance of a `Puma::Server`.
21
21
  class Cluster < Runner
22
- WORKER_CHECK_INTERVAL = 5
23
-
24
22
  def initialize(cli, events)
25
23
  super cli, events
26
24
 
@@ -37,7 +35,35 @@ module Puma
37
35
  @workers.each { |x| x.term }
38
36
 
39
37
  begin
40
- @workers.each { |w| Process.waitpid(w.pid) }
38
+ if RUBY_VERSION < '2.6'
39
+ @workers.each do |w|
40
+ begin
41
+ Process.waitpid(w.pid)
42
+ rescue Errno::ECHILD
43
+ # child is already terminated
44
+ end
45
+ end
46
+ else
47
+ # below code is for a bug in Ruby 2.6+, above waitpid call hangs
48
+ t_st = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+ pids = @workers.map(&:pid)
50
+ loop do
51
+ pids.reject! do |w_pid|
52
+ begin
53
+ if Process.waitpid(w_pid, Process::WNOHANG)
54
+ log " worker status: #{$?}"
55
+ true
56
+ end
57
+ rescue Errno::ECHILD
58
+ true # child is already terminated
59
+ end
60
+ end
61
+ break if pids.empty?
62
+ sleep 0.5
63
+ end
64
+ t_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
+ log format(" worker shutdown time: %6.2f", t_end - t_st)
66
+ end
41
67
  rescue Interrupt
42
68
  log "! Cancelled waiting for workers"
43
69
  end
@@ -183,7 +209,7 @@ module Puma
183
209
  def check_workers(force=false)
184
210
  return if !force && @next_check && @next_check >= Time.now
185
211
 
186
- @next_check = Time.now + WORKER_CHECK_INTERVAL
212
+ @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
187
213
 
188
214
  any = false
189
215
 
@@ -200,14 +226,9 @@ module Puma
200
226
  # during this loop by giving the kernel time to kill them.
201
227
  sleep 1 if any
202
228
 
203
- while @workers.any?
204
- pid = Process.waitpid(-1, Process::WNOHANG)
205
- break unless pid
229
+ @workers.reject! { |w| Process.waitpid(w.pid, Process::WNOHANG) }
206
230
 
207
- @workers.delete_if { |w| w.pid == pid }
208
- end
209
-
210
- @workers.delete_if(&:dead?)
231
+ @workers.reject!(&:dead?)
211
232
 
212
233
  cull_workers
213
234
  spawn_workers
@@ -290,7 +311,7 @@ module Puma
290
311
  base_payload = "p#{Process.pid}"
291
312
 
292
313
  while true
293
- sleep WORKER_CHECK_INTERVAL
314
+ sleep Const::WORKER_CHECK_INTERVAL
294
315
  begin
295
316
  b = server.backlog || 0
296
317
  r = server.running || 0
@@ -390,10 +411,13 @@ module Puma
390
411
  log "Early termination of worker"
391
412
  exit! 0
392
413
  else
414
+ @launcher.close_binder_listeners
415
+
393
416
  stop_workers
394
417
  stop
395
418
 
396
- raise SignalException, "SIGTERM"
419
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
420
+ exit 0 # Clean exit, workers were stopped
397
421
  end
398
422
  end
399
423
  end
@@ -487,7 +511,7 @@ module Puma
487
511
 
488
512
  force_check = false
489
513
 
490
- res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
514
+ res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
491
515
 
492
516
  if res
493
517
  req = read.read_nonblock(1)
@@ -186,7 +186,8 @@ module Puma
186
186
  :rackup => DefaultRackup,
187
187
  :logger => STDOUT,
188
188
  :persistent_timeout => Const::PERSISTENT_TIMEOUT,
189
- :first_data_timeout => Const::FIRST_DATA_TIMEOUT
189
+ :first_data_timeout => Const::FIRST_DATA_TIMEOUT,
190
+ :raise_exception_on_sigterm => true
190
191
  }
191
192
  end
192
193
 
data/lib/puma/const.rb CHANGED
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "3.12.1".freeze
104
- CODE_NAME = "Llamas in Pajamas".freeze
103
+ PUMA_VERSION = VERSION = "4.0.1".freeze
104
+ CODE_NAME = "4 Fast 4 Furious".freeze
105
105
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
106
106
 
107
107
  FAST_TRACK_KA_TIMEOUT = 0.2
@@ -227,5 +227,9 @@ module Puma
227
227
  HIJACK_IO = "rack.hijack_io".freeze
228
228
 
229
229
  EARLY_HINTS = "rack.early_hints".freeze
230
+
231
+ # Mininum interval to checks worker health
232
+ WORKER_CHECK_INTERVAL = 5
233
+
230
234
  end
231
235
  end
@@ -206,6 +206,16 @@ module Puma
206
206
  when "phased-restart"
207
207
  Process.kill "SIGUSR1", @pid
208
208
 
209
+ when "status"
210
+ begin
211
+ Process.kill 0, @pid
212
+ puts "Puma is started"
213
+ rescue Errno::ESRCH
214
+ raise "Puma is not running"
215
+ end
216
+
217
+ return
218
+
209
219
  else
210
220
  return
211
221
  end
data/lib/puma/dsl.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'puma/const'
4
+
3
5
  module Puma
4
6
  # The methods that are available for use inside the config file.
5
7
  # These same methods are used in Puma cli and the rack handler
@@ -103,7 +105,12 @@ module Puma
103
105
  end
104
106
 
105
107
  if opts[:no_token]
106
- auth_token = :none
108
+ # We need to use 'none' rather than :none because this value will be
109
+ # passed on to an instance of OptionParser, which doesn't support
110
+ # symbols as option values.
111
+ #
112
+ # See: https://github.com/puma/puma/issues/1193#issuecomment-305995488
113
+ auth_token = 'none'
107
114
  else
108
115
  auth_token = opts[:auth_token]
109
116
  auth_token ||= Configuration.random_token
@@ -295,12 +302,15 @@ module Puma
295
302
 
296
303
  def ssl_bind(host, port, opts)
297
304
  verify = opts.fetch(:verify_mode, 'none')
305
+ no_tlsv1 = opts.fetch(:no_tlsv1, 'false')
306
+ ca_additions = "&ca=#{opts[:ca]}" if ['peer', 'force_peer'].include?(verify)
298
307
 
299
308
  if defined?(JRUBY_VERSION)
300
309
  keystore_additions = "keystore=#{opts[:keystore]}&keystore-pass=#{opts[:keystore_pass]}"
301
- bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&#{keystore_additions}&verify_mode=#{verify}"
310
+ bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&#{keystore_additions}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}#{ca_additions}"
302
311
  else
303
- bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&verify_mode=#{verify}"
312
+ ssl_cipher_filter = "&ssl_cipher_filter=#{opts[:ssl_cipher_filter]}" if opts[:ssl_cipher_filter]
313
+ bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}#{ssl_cipher_filter}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}#{ca_additions}"
304
314
  end
305
315
  end
306
316
 
@@ -375,6 +385,21 @@ module Puma
375
385
 
376
386
  alias_method :after_worker_boot, :after_worker_fork
377
387
 
388
+ # Code to run out-of-band when the worker is idle.
389
+ # These hooks run immediately after a request has finished
390
+ # processing and there are no busy threads on the worker.
391
+ # The worker doesn't accept new requests until this code finishes.
392
+ #
393
+ # This hook is useful for running out-of-band garbage collection
394
+ # or scheduling asynchronous tasks to execute after a response.
395
+ #
396
+ # This can be called multiple times to add hooks.
397
+ #
398
+ def out_of_band(&block)
399
+ @options[:out_of_band] ||= []
400
+ @options[:out_of_band] << block
401
+ end
402
+
378
403
  # The directory to operate out of.
379
404
  def directory(dir)
380
405
  @options[:directory] = dir.to_s
@@ -424,6 +449,16 @@ module Puma
424
449
  @options[:prune_bundler] = answer
425
450
  end
426
451
 
452
+ # In environments where SIGTERM is something expected, instructing
453
+ # puma to shutdown gracefully ( for example in Kubernetes, where
454
+ # rolling restart is guaranteed usually on infrastructure level )
455
+ # SignalException should not be raised for SIGTERM
456
+ #
457
+ # When set to false, if puma process receives SIGTERM, it won't raise SignalException
458
+ def raise_exception_on_sigterm(answer=true)
459
+ @options[:raise_exception_on_sigterm] = answer
460
+ end
461
+
427
462
  # Additional text to display in process listing
428
463
  def tag(string)
429
464
  @options[:tag] = string.to_s
@@ -434,6 +469,13 @@ module Puma
434
469
  # that have not checked in within the given +timeout+.
435
470
  # This mitigates hung processes. Default value is 60 seconds.
436
471
  def worker_timeout(timeout)
472
+ timeout = Integer(timeout)
473
+ min = Const::WORKER_CHECK_INTERVAL
474
+
475
+ if timeout <= min
476
+ raise "The minimum worker_timeout must be greater than the worker reporting interval (#{min})"
477
+ end
478
+
437
479
  @options[:worker_timeout] = Integer(timeout)
438
480
  end
439
481