puma 3.12.2 → 4.2.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +106 -6
  3. data/README.md +91 -43
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +20 -10
  10. data/docs/restart.md +4 -2
  11. data/docs/systemd.md +27 -9
  12. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  13. data/ext/puma_http11/extconf.rb +8 -0
  14. data/ext/puma_http11/http11_parser.c +37 -62
  15. data/ext/puma_http11/http11_parser_common.rl +3 -3
  16. data/ext/puma_http11/mini_ssl.c +78 -8
  17. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  18. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
  19. data/lib/puma.rb +8 -0
  20. data/lib/puma/accept_nonblock.rb +7 -1
  21. data/lib/puma/app/status.rb +35 -29
  22. data/lib/puma/binder.rb +39 -5
  23. data/lib/puma/cli.rb +4 -0
  24. data/lib/puma/client.rb +221 -199
  25. data/lib/puma/cluster.rb +53 -30
  26. data/lib/puma/configuration.rb +4 -3
  27. data/lib/puma/const.rb +22 -25
  28. data/lib/puma/control_cli.rb +21 -4
  29. data/lib/puma/dsl.rb +297 -75
  30. data/lib/puma/events.rb +4 -1
  31. data/lib/puma/io_buffer.rb +1 -6
  32. data/lib/puma/launcher.rb +95 -53
  33. data/lib/puma/minissl.rb +35 -17
  34. data/lib/puma/plugin.rb +5 -2
  35. data/lib/puma/plugin/tmp_restart.rb +2 -0
  36. data/lib/puma/rack/builder.rb +2 -0
  37. data/lib/puma/rack/urlmap.rb +2 -0
  38. data/lib/puma/rack_default.rb +2 -0
  39. data/lib/puma/reactor.rb +109 -57
  40. data/lib/puma/runner.rb +4 -3
  41. data/lib/puma/server.rb +59 -62
  42. data/lib/puma/single.rb +3 -3
  43. data/lib/puma/thread_pool.rb +14 -32
  44. data/lib/puma/util.rb +1 -6
  45. data/lib/rack/handler/puma.rb +3 -3
  46. data/tools/docker/Dockerfile +16 -0
  47. data/tools/jungle/init.d/puma +6 -6
  48. data/tools/trickletest.rb +0 -1
  49. metadata +20 -8
  50. data/lib/puma/compat.rb +0 -14
  51. data/lib/puma/daemon_ext.rb +0 -33
  52. data/lib/puma/delegation.rb +0 -13
  53. data/lib/puma/java_io_buffer.rb +0 -47
  54. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
@@ -14,6 +14,7 @@ module Puma
14
14
  @options = cli.options
15
15
  @app = nil
16
16
  @control = nil
17
+ @started_at = Time.now
17
18
  end
18
19
 
19
20
  def daemon?
@@ -52,12 +53,12 @@ module Puma
52
53
 
53
54
  uri = URI.parse str
54
55
 
55
- app = Puma::App::Status.new @launcher
56
-
57
56
  if token = @options[:control_auth_token]
58
- app.auth_token = token unless token.empty? or token == :none
57
+ token = nil if token.empty? || token == 'none'
59
58
  end
60
59
 
60
+ app = Puma::App::Status.new @launcher, token
61
+
61
62
  control = Puma::Server.new app, @launcher.events
62
63
  control.min_threads = 0
63
64
  control.max_threads = 1
@@ -6,21 +6,16 @@ require 'puma/thread_pool'
6
6
  require 'puma/const'
7
7
  require 'puma/events'
8
8
  require 'puma/null_io'
9
- require 'puma/compat'
10
9
  require 'puma/reactor'
11
10
  require 'puma/client'
12
11
  require 'puma/binder'
13
- require 'puma/delegation'
14
12
  require 'puma/accept_nonblock'
15
13
  require 'puma/util'
16
14
 
17
15
  require 'puma/puma_http11'
18
16
 
19
- unless Puma.const_defined? "IOBuffer"
20
- require 'puma/io_buffer'
21
- end
22
-
23
17
  require 'socket'
18
+ require 'forwardable'
24
19
 
25
20
  module Puma
26
21
 
@@ -28,7 +23,7 @@ module Puma
28
23
  #
29
24
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
30
25
  # to generate one or more `Puma::Server` instances capable of handling requests.
31
- # Each Puma process will contain one `Puma::Server` instacne.
26
+ # Each Puma process will contain one `Puma::Server` instance.
32
27
  #
33
28
  # The `Puma::Server` instance pulls requests from the socket, adds them to a
34
29
  # `Puma::Reactor` where they get eventually passed to a `Puma::ThreadPool`.
@@ -37,7 +32,7 @@ module Puma
37
32
  class Server
38
33
 
39
34
  include Puma::Const
40
- extend Puma::Delegation
35
+ extend Forwardable
41
36
 
42
37
  attr_reader :thread
43
38
  attr_reader :events
@@ -79,7 +74,6 @@ module Puma
79
74
  @first_data_timeout = options.fetch(:first_data_timeout, FIRST_DATA_TIMEOUT)
80
75
 
81
76
  @binder = Binder.new(events)
82
- @own_binder = true
83
77
 
84
78
  @leak_stack_on_error = true
85
79
 
@@ -95,14 +89,10 @@ module Puma
95
89
 
96
90
  attr_accessor :binder, :leak_stack_on_error, :early_hints
97
91
 
98
- forward :add_tcp_listener, :@binder
99
- forward :add_ssl_listener, :@binder
100
- forward :add_unix_listener, :@binder
101
- forward :connected_port, :@binder
92
+ def_delegators :@binder, :add_tcp_listener, :add_ssl_listener, :add_unix_listener, :connected_port
102
93
 
103
94
  def inherit_binder(bind)
104
95
  @binder = bind
105
- @own_binder = false
106
96
  end
107
97
 
108
98
  def tcp_mode!
@@ -214,7 +204,10 @@ module Puma
214
204
  @events.fire :state, :running
215
205
 
216
206
  if background
217
- @thread = Thread.new { handle_servers_lopez_mode }
207
+ @thread = Thread.new do
208
+ Puma.set_thread_name "server"
209
+ handle_servers_lopez_mode
210
+ end
218
211
  return @thread
219
212
  else
220
213
  handle_servers_lopez_mode
@@ -270,10 +263,11 @@ module Puma
270
263
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
271
264
  end
272
265
 
273
- @notify.close
274
-
275
- if @status != :restart and @own_binder
276
- @binder.close
266
+ # Prevent can't modify frozen IOError (RuntimeError)
267
+ begin
268
+ @notify.close
269
+ rescue IOError
270
+ # no biggy
277
271
  end
278
272
  end
279
273
 
@@ -323,7 +317,7 @@ module Puma
323
317
 
324
318
  @events.ssl_error self, addr, cert, e
325
319
  rescue HttpParserError => e
326
- client.write_400
320
+ client.write_error(400)
327
321
  client.close
328
322
 
329
323
  @events.parse_error self, client.env, e
@@ -357,7 +351,10 @@ module Puma
357
351
  @events.fire :state, :running
358
352
 
359
353
  if background
360
- @thread = Thread.new { handle_servers }
354
+ @thread = Thread.new do
355
+ Puma.set_thread_name "server"
356
+ handle_servers
357
+ end
361
358
  return @thread
362
359
  else
363
360
  handle_servers
@@ -398,7 +395,10 @@ module Puma
398
395
  end
399
396
 
400
397
  pool << client
401
- pool.wait_until_not_full
398
+ busy_threads = pool.wait_until_not_full
399
+ if busy_threads == 0
400
+ @options[:out_of_band].each(&:call) if @options[:out_of_band]
401
+ end
402
402
  end
403
403
  rescue SystemCallError
404
404
  # nothing
@@ -430,10 +430,6 @@ module Puma
430
430
  ensure
431
431
  @check.close
432
432
  @notify.close
433
-
434
- if @status != :restart and @own_binder
435
- @binder.close
436
- end
437
433
  end
438
434
 
439
435
  @events.fire :state, :done
@@ -470,8 +466,6 @@ module Puma
470
466
  clean_thread_locals = @options[:clean_thread_locals]
471
467
  close_socket = true
472
468
 
473
- requests = 0
474
-
475
469
  while true
476
470
  case handle_request(client, buffer)
477
471
  when false
@@ -485,19 +479,7 @@ module Puma
485
479
 
486
480
  ThreadPool.clean_thread_locals if clean_thread_locals
487
481
 
488
- requests += 1
489
-
490
- check_for_more_data = @status == :run
491
-
492
- if requests >= MAX_FAST_INLINE
493
- # This will mean that reset will only try to use the data it already
494
- # has buffered and won't try to read more data. What this means is that
495
- # every client, independent of their request speed, gets treated like a slow
496
- # one once every MAX_FAST_INLINE requests.
497
- check_for_more_data = false
498
- end
499
-
500
- unless client.reset(check_for_more_data)
482
+ unless client.reset(@status == :run)
501
483
  close_socket = false
502
484
  client.set_timeout @persistent_timeout
503
485
  @reactor.add client
@@ -526,7 +508,7 @@ module Puma
526
508
  rescue HttpParserError => e
527
509
  lowlevel_error(e, client.env)
528
510
 
529
- client.write_400
511
+ client.write_error(400)
530
512
 
531
513
  @events.parse_error self, client.env, e
532
514
 
@@ -534,7 +516,7 @@ module Puma
534
516
  rescue StandardError => e
535
517
  lowlevel_error(e, client.env)
536
518
 
537
- client.write_500
519
+ client.write_error(500)
538
520
 
539
521
  @events.unknown_error self, e, "Read"
540
522
 
@@ -609,19 +591,26 @@ module Puma
609
591
  end
610
592
 
611
593
  def default_server_port(env)
612
- return PORT_443 if env[HTTPS_KEY] == 'on' || env[HTTPS_KEY] == 'https'
613
- env['HTTP_X_FORWARDED_PROTO'] == 'https' ? PORT_443 : PORT_80
594
+ if ['on', HTTPS].include?(env[HTTPS_KEY]) || env[HTTP_X_FORWARDED_PROTO].to_s[0...5] == HTTPS || env[HTTP_X_FORWARDED_SCHEME] == HTTPS || env[HTTP_X_FORWARDED_SSL] == "on"
595
+ PORT_443
596
+ else
597
+ PORT_80
598
+ end
614
599
  end
615
600
 
616
- # Given the request +env+ from +client+ and a partial request body
617
- # in +body+, finish reading the body if there is one and invoke
618
- # the rack app. Then construct the response and write it back to
619
- # +client+
601
+ # Takes the request +req+, invokes the Rack application to construct
602
+ # the response and writes it back to +req.io+.
603
+ #
604
+ # The second parameter +lines+ is a IO-like object unique to this thread.
605
+ # This is normally an instance of Puma::IOBuffer.
620
606
  #
621
- # +cl+ is the previously fetched Content-Length header if there
622
- # was one. This is an optimization to keep from having to look
623
- # it up again.
607
+ # It'll return +false+ when the connection is closed, this doesn't mean
608
+ # that the response wasn't successful.
624
609
  #
610
+ # It'll return +:async+ if the connection remains open but will be handled
611
+ # elsewhere, i.e. the connection has been hijacked by the Rack application.
612
+ #
613
+ # Finally, it'll return +true+ on keep-alive connections.
625
614
  def handle_request(req, lines)
626
615
  env = req.env
627
616
  client = req.io
@@ -644,23 +633,27 @@ module Puma
644
633
  head = env[REQUEST_METHOD] == HEAD
645
634
 
646
635
  env[RACK_INPUT] = body
647
- env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP
636
+ env[RACK_URL_SCHEME] = default_server_port(env) == PORT_443 ? HTTPS : HTTP
648
637
 
649
638
  if @early_hints
650
639
  env[EARLY_HINTS] = lambda { |headers|
651
- fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
640
+ begin
641
+ fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
652
642
 
653
- headers.each_pair do |k, vs|
654
- if vs.respond_to?(:to_s) && !vs.to_s.empty?
655
- vs.to_s.split(NEWLINE).each do |v|
656
- fast_write client, "#{k}: #{v}\r\n"
643
+ headers.each_pair do |k, vs|
644
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
645
+ vs.to_s.split(NEWLINE).each do |v|
646
+ fast_write client, "#{k}: #{v}\r\n"
647
+ end
648
+ else
649
+ fast_write client, "#{k}: #{vs}\r\n"
657
650
  end
658
- else
659
- fast_write client, "#{k}: #{vs}\r\n"
660
651
  end
661
- end
662
652
 
663
- fast_write client, "\r\n".freeze
653
+ fast_write client, "\r\n".freeze
654
+ rescue ConnectionError
655
+ # noop, if we lost the socket we just won't send the early hints
656
+ end
664
657
  }
665
658
  end
666
659
 
@@ -956,6 +949,10 @@ module Puma
956
949
  @events.debug "Drained #{count} additional connections."
957
950
  end
958
951
 
952
+ if @status != :restart
953
+ @binder.close
954
+ end
955
+
959
956
  if @thread_pool
960
957
  if timeout = @options[:force_shutdown_after]
961
958
  @thread_pool.shutdown timeout.to_i
@@ -18,7 +18,7 @@ module Puma
18
18
  r = @server.running || 0
19
19
  t = @server.pool_capacity || 0
20
20
  m = @server.max_threads || 0
21
- %Q!{ "backlog": #{b}, "running": #{r}, "pool_capacity": #{t}, "max_threads": #{m} }!
21
+ %Q!{ "started_at": "#{@started_at.utc.iso8601}", "backlog": #{b}, "running": #{r}, "pool_capacity": #{t}, "max_threads": #{m} }!
22
22
  end
23
23
 
24
24
  def restart
@@ -26,7 +26,7 @@ module Puma
26
26
  end
27
27
 
28
28
  def stop
29
- @server.stop false
29
+ @server.stop(false) if @server
30
30
  end
31
31
 
32
32
  def halt
@@ -36,7 +36,7 @@ module Puma
36
36
  def stop_blocked
37
37
  log "- Gracefully stopping, waiting for requests to finish"
38
38
  @control.stop(true) if @control
39
- @server.stop(true)
39
+ @server.stop(true) if @server
40
40
  end
41
41
 
42
42
  def jruby_daemon?
@@ -87,8 +87,7 @@ module Puma
87
87
  @spawned += 1
88
88
 
89
89
  th = Thread.new(@spawned) do |spawned|
90
- # Thread name is new in Ruby 2.3
91
- Thread.current.name = 'puma %03i' % spawned if Thread.current.respond_to?(:name=)
90
+ Puma.set_thread_name 'threadpool %03i' % spawned
92
91
  todo = @todo
93
92
  block = @block
94
93
  mutex = @mutex
@@ -194,6 +193,9 @@ module Puma
194
193
  # method would not block and another request would be added into the reactor
195
194
  # by the server. This would continue until a fully bufferend request
196
195
  # makes it through the reactor and can then be processed by the thread pool.
196
+ #
197
+ # Returns the current number of busy threads, or +nil+ if shutting down.
198
+ #
197
199
  def wait_until_not_full
198
200
  @mutex.synchronize do
199
201
  while true
@@ -203,7 +205,8 @@ module Puma
203
205
  # is work queued that cannot be handled by waiting
204
206
  # threads, then accept more work until we would
205
207
  # spin up the max number of threads.
206
- return if @todo.size - @waiting < @max - @spawned
208
+ busy_threads = @spawned - @waiting + @todo.size
209
+ return busy_threads if @max > busy_threads
207
210
 
208
211
  @not_full.wait @mutex
209
212
  end
@@ -240,10 +243,12 @@ module Puma
240
243
  end
241
244
  end
242
245
 
243
- class AutoTrim
244
- def initialize(pool, timeout)
246
+ class Automaton
247
+ def initialize(pool, timeout, thread_name, message)
245
248
  @pool = pool
246
249
  @timeout = timeout
250
+ @thread_name = thread_name
251
+ @message = message
247
252
  @running = false
248
253
  end
249
254
 
@@ -251,8 +256,9 @@ module Puma
251
256
  @running = true
252
257
 
253
258
  @thread = Thread.new do
259
+ Puma.set_thread_name @thread_name
254
260
  while @running
255
- @pool.trim
261
+ @pool.public_send(@message)
256
262
  sleep @timeout
257
263
  end
258
264
  end
@@ -265,36 +271,12 @@ module Puma
265
271
  end
266
272
 
267
273
  def auto_trim!(timeout=30)
268
- @auto_trim = AutoTrim.new(self, timeout)
274
+ @auto_trim = Automaton.new(self, timeout, "threadpool trimmer", :trim)
269
275
  @auto_trim.start!
270
276
  end
271
277
 
272
- class Reaper
273
- def initialize(pool, timeout)
274
- @pool = pool
275
- @timeout = timeout
276
- @running = false
277
- end
278
-
279
- def start!
280
- @running = true
281
-
282
- @thread = Thread.new do
283
- while @running
284
- @pool.reap
285
- sleep @timeout
286
- end
287
- end
288
- end
289
-
290
- def stop
291
- @running = false
292
- @thread.wakeup
293
- end
294
- end
295
-
296
278
  def auto_reap!(timeout=5)
297
- @reaper = Reaper.new(self, timeout)
279
+ @reaper = Automaton.new(self, timeout, "threadpool reaper", :reap)
298
280
  @reaper.start!
299
281
  end
300
282
 
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
- major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
3
2
 
4
- if major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
5
- require 'puma/rack/backports/uri/common_193'
6
- else
7
- require 'uri/common'
8
- end
3
+ require 'uri/common'
9
4
 
10
5
  module Puma
11
6
  module Util
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/handler'
2
4
 
3
5
  module Rack
@@ -59,8 +61,6 @@ module Rack
59
61
  conf
60
62
  end
61
63
 
62
-
63
-
64
64
  def self.run(app, options = {})
65
65
  conf = self.config(app, options)
66
66
 
@@ -86,7 +86,7 @@ module Rack
86
86
  "Verbose" => "Don't report each request (default: false)"
87
87
  }
88
88
  end
89
- private
89
+
90
90
  def self.set_host_port_to_config(host, port, config)
91
91
  config.clear_binds! if host || port
92
92
 
@@ -0,0 +1,16 @@
1
+ # Use this Dockerfile to create minimal reproductions of issues
2
+
3
+ FROM ruby:2.6
4
+
5
+ # throw errors if Gemfile has been modified since Gemfile.lock
6
+ RUN bundle config --global frozen 1
7
+
8
+ WORKDIR /usr/src/app
9
+
10
+ COPY . .
11
+ RUN gem install bundler
12
+ RUN bundle install
13
+ RUN bundle exec rake compile
14
+
15
+ EXPOSE 9292
16
+ CMD bundle exec bin/puma test/rackup/hello.ru