puma 3.12.0 → 4.3.8

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +164 -0
  3. data/README.md +76 -48
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/plugins.md +20 -10
  7. data/docs/restart.md +4 -2
  8. data/docs/systemd.md +27 -9
  9. data/docs/tcp_mode.md +96 -0
  10. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  11. data/ext/puma_http11/extconf.rb +13 -0
  12. data/ext/puma_http11/http11_parser.c +40 -63
  13. data/ext/puma_http11/http11_parser.java.rl +21 -37
  14. data/ext/puma_http11/http11_parser.rl +3 -1
  15. data/ext/puma_http11/http11_parser_common.rl +3 -3
  16. data/ext/puma_http11/mini_ssl.c +86 -4
  17. data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
  18. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  19. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  20. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
  21. data/ext/puma_http11/puma_http11.c +3 -0
  22. data/lib/puma.rb +8 -0
  23. data/lib/puma/accept_nonblock.rb +7 -1
  24. data/lib/puma/app/status.rb +37 -29
  25. data/lib/puma/binder.rb +47 -68
  26. data/lib/puma/cli.rb +6 -0
  27. data/lib/puma/client.rb +244 -199
  28. data/lib/puma/cluster.rb +55 -30
  29. data/lib/puma/commonlogger.rb +2 -0
  30. data/lib/puma/configuration.rb +6 -3
  31. data/lib/puma/const.rb +32 -18
  32. data/lib/puma/control_cli.rb +41 -14
  33. data/lib/puma/detect.rb +2 -0
  34. data/lib/puma/dsl.rb +311 -77
  35. data/lib/puma/events.rb +6 -1
  36. data/lib/puma/io_buffer.rb +3 -6
  37. data/lib/puma/jruby_restart.rb +2 -0
  38. data/lib/puma/launcher.rb +99 -55
  39. data/lib/puma/minissl.rb +37 -17
  40. data/lib/puma/minissl/context_builder.rb +76 -0
  41. data/lib/puma/null_io.rb +2 -0
  42. data/lib/puma/plugin.rb +7 -2
  43. data/lib/puma/plugin/tmp_restart.rb +2 -0
  44. data/lib/puma/rack/builder.rb +4 -1
  45. data/lib/puma/rack/urlmap.rb +2 -0
  46. data/lib/puma/rack_default.rb +2 -0
  47. data/lib/puma/reactor.rb +112 -57
  48. data/lib/puma/runner.rb +13 -3
  49. data/lib/puma/server.rb +119 -48
  50. data/lib/puma/single.rb +5 -3
  51. data/lib/puma/state_file.rb +2 -0
  52. data/lib/puma/tcp_logger.rb +2 -0
  53. data/lib/puma/thread_pool.rb +17 -33
  54. data/lib/puma/util.rb +2 -6
  55. data/lib/rack/handler/puma.rb +6 -3
  56. data/tools/docker/Dockerfile +16 -0
  57. data/tools/jungle/init.d/puma +6 -6
  58. data/tools/trickletest.rb +0 -1
  59. metadata +26 -14
  60. data/lib/puma/compat.rb +0 -14
  61. data/lib/puma/convenient.rb +0 -23
  62. data/lib/puma/daemon_ext.rb +0 -31
  63. data/lib/puma/delegation.rb +0 -11
  64. data/lib/puma/java_io_buffer.rb +0 -45
  65. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
data/lib/puma/server.rb CHANGED
@@ -1,24 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'stringio'
2
4
 
3
5
  require 'puma/thread_pool'
4
6
  require 'puma/const'
5
7
  require 'puma/events'
6
8
  require 'puma/null_io'
7
- require 'puma/compat'
8
9
  require 'puma/reactor'
9
10
  require 'puma/client'
10
11
  require 'puma/binder'
11
- require 'puma/delegation'
12
12
  require 'puma/accept_nonblock'
13
13
  require 'puma/util'
14
14
 
15
15
  require 'puma/puma_http11'
16
16
 
17
- unless Puma.const_defined? "IOBuffer"
18
- require 'puma/io_buffer'
19
- end
20
-
21
17
  require 'socket'
18
+ require 'forwardable'
22
19
 
23
20
  module Puma
24
21
 
@@ -26,7 +23,7 @@ module Puma
26
23
  #
27
24
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
28
25
  # to generate one or more `Puma::Server` instances capable of handling requests.
29
- # Each Puma process will contain one `Puma::Server` instacne.
26
+ # Each Puma process will contain one `Puma::Server` instance.
30
27
  #
31
28
  # The `Puma::Server` instance pulls requests from the socket, adds them to a
32
29
  # `Puma::Reactor` where they get eventually passed to a `Puma::ThreadPool`.
@@ -35,7 +32,7 @@ module Puma
35
32
  class Server
36
33
 
37
34
  include Puma::Const
38
- extend Puma::Delegation
35
+ extend Forwardable
39
36
 
40
37
  attr_reader :thread
41
38
  attr_reader :events
@@ -77,7 +74,6 @@ module Puma
77
74
  @first_data_timeout = options.fetch(:first_data_timeout, FIRST_DATA_TIMEOUT)
78
75
 
79
76
  @binder = Binder.new(events)
80
- @own_binder = true
81
77
 
82
78
  @leak_stack_on_error = true
83
79
 
@@ -93,14 +89,10 @@ module Puma
93
89
 
94
90
  attr_accessor :binder, :leak_stack_on_error, :early_hints
95
91
 
96
- forward :add_tcp_listener, :@binder
97
- forward :add_ssl_listener, :@binder
98
- forward :add_unix_listener, :@binder
99
- forward :connected_port, :@binder
92
+ def_delegators :@binder, :add_tcp_listener, :add_ssl_listener, :add_unix_listener, :connected_port
100
93
 
101
94
  def inherit_binder(bind)
102
95
  @binder = bind
103
- @own_binder = false
104
96
  end
105
97
 
106
98
  def tcp_mode!
@@ -212,7 +204,10 @@ module Puma
212
204
  @events.fire :state, :running
213
205
 
214
206
  if background
215
- @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
216
211
  return @thread
217
212
  else
218
213
  handle_servers_lopez_mode
@@ -268,10 +263,11 @@ module Puma
268
263
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
269
264
  end
270
265
 
271
- @notify.close
272
-
273
- if @status != :restart and @own_binder
274
- @binder.close
266
+ # Prevent can't modify frozen IOError (RuntimeError)
267
+ begin
268
+ @notify.close
269
+ rescue IOError
270
+ # no biggy
275
271
  end
276
272
  end
277
273
 
@@ -321,7 +317,7 @@ module Puma
321
317
 
322
318
  @events.ssl_error self, addr, cert, e
323
319
  rescue HttpParserError => e
324
- client.write_400
320
+ client.write_error(400)
325
321
  client.close
326
322
 
327
323
  @events.parse_error self, client.env, e
@@ -355,7 +351,10 @@ module Puma
355
351
  @events.fire :state, :running
356
352
 
357
353
  if background
358
- @thread = Thread.new { handle_servers }
354
+ @thread = Thread.new do
355
+ Puma.set_thread_name "server"
356
+ handle_servers
357
+ end
359
358
  return @thread
360
359
  else
361
360
  handle_servers
@@ -396,7 +395,10 @@ module Puma
396
395
  end
397
396
 
398
397
  pool << client
399
- 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
400
402
  end
401
403
  rescue SystemCallError
402
404
  # nothing
@@ -428,10 +430,6 @@ module Puma
428
430
  ensure
429
431
  @check.close
430
432
  @notify.close
431
-
432
- if @status != :restart and @own_binder
433
- @binder.close
434
- end
435
433
  end
436
434
 
437
435
  @events.fire :state, :done
@@ -468,6 +466,8 @@ module Puma
468
466
  clean_thread_locals = @options[:clean_thread_locals]
469
467
  close_socket = true
470
468
 
469
+ requests = 0
470
+
471
471
  while true
472
472
  case handle_request(client, buffer)
473
473
  when false
@@ -481,7 +481,24 @@ module Puma
481
481
 
482
482
  ThreadPool.clean_thread_locals if clean_thread_locals
483
483
 
484
- unless client.reset(@status == :run)
484
+ requests += 1
485
+
486
+ # Closing keepalive sockets after they've made a reasonable
487
+ # number of requests allows Puma to service many connections
488
+ # fairly, even when the number of concurrent connections exceeds
489
+ # the size of the threadpool. It also allows cluster mode Pumas
490
+ # to keep load evenly distributed across workers, because clients
491
+ # are randomly assigned a new worker when opening a new connection.
492
+ #
493
+ # Previously, Puma would kick connections in this conditional back
494
+ # to the reactor. However, because this causes the todo set to increase
495
+ # in size, the wait_until_full mutex would never unlock, leaving
496
+ # any additional connections unserviced.
497
+ break if requests >= MAX_FAST_INLINE
498
+
499
+ check_for_more_data = @status == :run
500
+
501
+ unless client.reset(check_for_more_data)
485
502
  close_socket = false
486
503
  client.set_timeout @persistent_timeout
487
504
  @reactor.add client
@@ -510,7 +527,7 @@ module Puma
510
527
  rescue HttpParserError => e
511
528
  lowlevel_error(e, client.env)
512
529
 
513
- client.write_400
530
+ client.write_error(400)
514
531
 
515
532
  @events.parse_error self, client.env, e
516
533
 
@@ -518,7 +535,7 @@ module Puma
518
535
  rescue StandardError => e
519
536
  lowlevel_error(e, client.env)
520
537
 
521
- client.write_500
538
+ client.write_error(500)
522
539
 
523
540
  @events.unknown_error self, e, "Read"
524
541
 
@@ -593,19 +610,26 @@ module Puma
593
610
  end
594
611
 
595
612
  def default_server_port(env)
596
- return PORT_443 if env[HTTPS_KEY] == 'on' || env[HTTPS_KEY] == 'https'
597
- env['HTTP_X_FORWARDED_PROTO'] == 'https' ? PORT_443 : PORT_80
613
+ 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"
614
+ PORT_443
615
+ else
616
+ PORT_80
617
+ end
598
618
  end
599
619
 
600
- # Given the request +env+ from +client+ and a partial request body
601
- # in +body+, finish reading the body if there is one and invoke
602
- # the rack app. Then construct the response and write it back to
603
- # +client+
620
+ # Takes the request +req+, invokes the Rack application to construct
621
+ # the response and writes it back to +req.io+.
622
+ #
623
+ # The second parameter +lines+ is a IO-like object unique to this thread.
624
+ # This is normally an instance of Puma::IOBuffer.
625
+ #
626
+ # It'll return +false+ when the connection is closed, this doesn't mean
627
+ # that the response wasn't successful.
604
628
  #
605
- # +cl+ is the previously fetched Content-Length header if there
606
- # was one. This is an optimization to keep from having to look
607
- # it up again.
629
+ # It'll return +:async+ if the connection remains open but will be handled
630
+ # elsewhere, i.e. the connection has been hijacked by the Rack application.
608
631
  #
632
+ # Finally, it'll return +true+ on keep-alive connections.
609
633
  def handle_request(req, lines)
610
634
  env = req.env
611
635
  client = req.io
@@ -628,26 +652,62 @@ module Puma
628
652
  head = env[REQUEST_METHOD] == HEAD
629
653
 
630
654
  env[RACK_INPUT] = body
631
- env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP
655
+ env[RACK_URL_SCHEME] = default_server_port(env) == PORT_443 ? HTTPS : HTTP
632
656
 
633
657
  if @early_hints
634
658
  env[EARLY_HINTS] = lambda { |headers|
635
- fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
659
+ begin
660
+ fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
636
661
 
637
- headers.each_pair do |k, vs|
638
- if vs.respond_to?(:to_s) && !vs.to_s.empty?
639
- vs.to_s.split(NEWLINE).each do |v|
640
- fast_write client, "#{k}: #{v}\r\n"
662
+ headers.each_pair do |k, vs|
663
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
664
+ vs.to_s.split(NEWLINE).each do |v|
665
+ next if possible_header_injection?(v)
666
+ fast_write client, "#{k}: #{v}\r\n"
667
+ end
668
+ else
669
+ fast_write client, "#{k}: #{vs}\r\n"
641
670
  end
642
- else
643
- fast_write client, "#{k}: #{vs}\r\n"
644
671
  end
645
- end
646
672
 
647
- fast_write client, "\r\n".freeze
673
+ fast_write client, "\r\n".freeze
674
+ rescue ConnectionError
675
+ # noop, if we lost the socket we just won't send the early hints
676
+ end
648
677
  }
649
678
  end
650
679
 
680
+ # Fixup any headers with , in the name to have _ now. We emit
681
+ # headers with , in them during the parse phase to avoid ambiguity
682
+ # with the - to _ conversion for critical headers. But here for
683
+ # compatibility, we'll convert them back. This code is written to
684
+ # avoid allocation in the common case (ie there are no headers
685
+ # with , in their names), that's why it has the extra conditionals.
686
+
687
+ to_delete = nil
688
+ to_add = nil
689
+
690
+ env.each do |k,v|
691
+ if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
692
+ if to_delete
693
+ to_delete << k
694
+ else
695
+ to_delete = [k]
696
+ end
697
+
698
+ unless to_add
699
+ to_add = {}
700
+ end
701
+
702
+ to_add[k.tr(",", "_")] = v
703
+ end
704
+ end
705
+
706
+ if to_delete
707
+ to_delete.each { |k| env.delete(k) }
708
+ env.merge! to_add
709
+ end
710
+
651
711
  # A rack extension. If the app writes #call'ables to this
652
712
  # array, we will invoke them when the request is done.
653
713
  #
@@ -735,6 +795,7 @@ module Puma
735
795
  headers.each do |k, vs|
736
796
  case k.downcase
737
797
  when CONTENT_LENGTH2
798
+ next if possible_header_injection?(vs)
738
799
  content_length = vs
739
800
  next
740
801
  when TRANSFER_ENCODING
@@ -747,6 +808,7 @@ module Puma
747
808
 
748
809
  if vs.respond_to?(:to_s) && !vs.to_s.empty?
749
810
  vs.to_s.split(NEWLINE).each do |v|
811
+ next if possible_header_injection?(v)
750
812
  lines.append k, colon, v, line_ending
751
813
  end
752
814
  else
@@ -940,6 +1002,10 @@ module Puma
940
1002
  @events.debug "Drained #{count} additional connections."
941
1003
  end
942
1004
 
1005
+ if @status != :restart
1006
+ @binder.close
1007
+ end
1008
+
943
1009
  if @thread_pool
944
1010
  if timeout = @options[:force_shutdown_after]
945
1011
  @thread_pool.shutdown timeout.to_i
@@ -1013,5 +1079,10 @@ module Puma
1013
1079
  def shutting_down?
1014
1080
  @status == :stop || @status == :restart
1015
1081
  end
1082
+
1083
+ def possible_header_injection?(header_value)
1084
+ HTTP_INJECTION_REGEX =~ header_value.to_s
1085
+ end
1086
+ private :possible_header_injection?
1016
1087
  end
1017
1088
  end
data/lib/puma/single.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/runner'
2
4
  require 'puma/detect'
3
5
  require 'puma/plugin'
@@ -16,7 +18,7 @@ module Puma
16
18
  r = @server.running || 0
17
19
  t = @server.pool_capacity || 0
18
20
  m = @server.max_threads || 0
19
- %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} }!
20
22
  end
21
23
 
22
24
  def restart
@@ -24,7 +26,7 @@ module Puma
24
26
  end
25
27
 
26
28
  def stop
27
- @server.stop false
29
+ @server.stop(false) if @server
28
30
  end
29
31
 
30
32
  def halt
@@ -34,7 +36,7 @@ module Puma
34
36
  def stop_blocked
35
37
  log "- Gracefully stopping, waiting for requests to finish"
36
38
  @control.stop(true) if @control
37
- @server.stop(true)
39
+ @server.stop(true) if @server
38
40
  end
39
41
 
40
42
  def jruby_daemon?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module Puma
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  class TCPLogger
3
5
  def initialize(logger, app, quiet=false)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
3
5
  module Puma
@@ -85,8 +87,7 @@ module Puma
85
87
  @spawned += 1
86
88
 
87
89
  th = Thread.new(@spawned) do |spawned|
88
- # Thread name is new in Ruby 2.3
89
- Thread.current.name = 'puma %03i' % spawned if Thread.current.respond_to?(:name=)
90
+ Puma.set_thread_name 'threadpool %03i' % spawned
90
91
  todo = @todo
91
92
  block = @block
92
93
  mutex = @mutex
@@ -188,10 +189,13 @@ module Puma
188
189
  # request, it might not be added to the `@todo` array right away.
189
190
  # For example if a slow client has only sent a header, but not a body
190
191
  # then the `@todo` array would stay the same size as the reactor works
191
- # to try to buffer the request. In tha scenario the next call to this
192
+ # to try to buffer the request. In that scenario the next call to this
192
193
  # method would not block and another request would be added into the reactor
193
194
  # by the server. This would continue until a fully bufferend request
194
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
+ #
195
199
  def wait_until_not_full
196
200
  @mutex.synchronize do
197
201
  while true
@@ -201,7 +205,8 @@ module Puma
201
205
  # is work queued that cannot be handled by waiting
202
206
  # threads, then accept more work until we would
203
207
  # spin up the max number of threads.
204
- return if @todo.size - @waiting < @max - @spawned
208
+ busy_threads = @spawned - @waiting + @todo.size
209
+ return busy_threads if @max > busy_threads
205
210
 
206
211
  @not_full.wait @mutex
207
212
  end
@@ -238,10 +243,12 @@ module Puma
238
243
  end
239
244
  end
240
245
 
241
- class AutoTrim
242
- def initialize(pool, timeout)
246
+ class Automaton
247
+ def initialize(pool, timeout, thread_name, message)
243
248
  @pool = pool
244
249
  @timeout = timeout
250
+ @thread_name = thread_name
251
+ @message = message
245
252
  @running = false
246
253
  end
247
254
 
@@ -249,8 +256,9 @@ module Puma
249
256
  @running = true
250
257
 
251
258
  @thread = Thread.new do
259
+ Puma.set_thread_name @thread_name
252
260
  while @running
253
- @pool.trim
261
+ @pool.public_send(@message)
254
262
  sleep @timeout
255
263
  end
256
264
  end
@@ -263,36 +271,12 @@ module Puma
263
271
  end
264
272
 
265
273
  def auto_trim!(timeout=30)
266
- @auto_trim = AutoTrim.new(self, timeout)
274
+ @auto_trim = Automaton.new(self, timeout, "threadpool trimmer", :trim)
267
275
  @auto_trim.start!
268
276
  end
269
277
 
270
- class Reaper
271
- def initialize(pool, timeout)
272
- @pool = pool
273
- @timeout = timeout
274
- @running = false
275
- end
276
-
277
- def start!
278
- @running = true
279
-
280
- @thread = Thread.new do
281
- while @running
282
- @pool.reap
283
- sleep @timeout
284
- end
285
- end
286
- end
287
-
288
- def stop
289
- @running = false
290
- @thread.wakeup
291
- end
292
- end
293
-
294
278
  def auto_reap!(timeout=5)
295
- @reaper = Reaper.new(self, timeout)
279
+ @reaper = Automaton.new(self, timeout, "threadpool reaper", :reap)
296
280
  @reaper.start!
297
281
  end
298
282