puma 2.16.0 → 3.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/{History.txt → History.md} +489 -70
  3. data/README.md +143 -174
  4. data/docs/architecture.md +36 -0
  5. data/{DEPLOYMENT.md → docs/deployment.md} +1 -1
  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/nginx.md +2 -2
  10. data/docs/plugins.md +28 -0
  11. data/docs/restart.md +39 -0
  12. data/docs/signals.md +56 -3
  13. data/docs/systemd.md +272 -0
  14. data/ext/puma_http11/extconf.rb +2 -0
  15. data/ext/puma_http11/http11_parser.c +291 -447
  16. data/ext/puma_http11/http11_parser.h +1 -0
  17. data/ext/puma_http11/http11_parser.java.rl +5 -5
  18. data/ext/puma_http11/http11_parser.rl +10 -9
  19. data/ext/puma_http11/http11_parser_common.rl +1 -1
  20. data/ext/puma_http11/io_buffer.c +8 -8
  21. data/ext/puma_http11/mini_ssl.c +64 -6
  22. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +113 -131
  23. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +9 -2
  24. data/ext/puma_http11/puma_http11.c +1 -0
  25. data/lib/puma/app/status.rb +9 -1
  26. data/lib/puma/binder.rb +90 -38
  27. data/lib/puma/cli.rb +134 -491
  28. data/lib/puma/client.rb +142 -4
  29. data/lib/puma/cluster.rb +132 -76
  30. data/lib/puma/commonlogger.rb +19 -20
  31. data/lib/puma/compat.rb +3 -7
  32. data/lib/puma/configuration.rb +206 -67
  33. data/lib/puma/const.rb +21 -31
  34. data/lib/puma/control_cli.rb +92 -103
  35. data/lib/puma/convenient.rb +23 -0
  36. data/lib/puma/daemon_ext.rb +6 -0
  37. data/lib/puma/detect.rb +10 -1
  38. data/lib/puma/dsl.rb +203 -45
  39. data/lib/puma/events.rb +22 -13
  40. data/lib/puma/io_buffer.rb +1 -1
  41. data/lib/puma/jruby_restart.rb +1 -2
  42. data/lib/puma/launcher.rb +431 -0
  43. data/lib/puma/minissl.rb +83 -4
  44. data/lib/puma/null_io.rb +19 -11
  45. data/lib/puma/plugin/tmp_restart.rb +34 -0
  46. data/lib/puma/plugin.rb +115 -0
  47. data/lib/puma/rack/backports/uri/common_193.rb +17 -13
  48. data/lib/puma/rack/builder.rb +3 -0
  49. data/lib/puma/rack/urlmap.rb +9 -8
  50. data/lib/puma/reactor.rb +18 -0
  51. data/lib/puma/runner.rb +43 -15
  52. data/lib/puma/server.rb +141 -35
  53. data/lib/puma/single.rb +16 -6
  54. data/lib/puma/state_file.rb +29 -0
  55. data/lib/puma/tcp_logger.rb +8 -1
  56. data/lib/puma/thread_pool.rb +60 -10
  57. data/lib/puma/util.rb +1 -5
  58. data/lib/puma.rb +13 -4
  59. data/lib/rack/handler/puma.rb +76 -29
  60. data/tools/jungle/README.md +12 -2
  61. data/tools/jungle/init.d/README.md +9 -2
  62. data/tools/jungle/init.d/puma +86 -59
  63. data/tools/jungle/init.d/run-puma +16 -1
  64. data/tools/jungle/rc.d/README.md +74 -0
  65. data/tools/jungle/rc.d/puma +61 -0
  66. data/tools/jungle/rc.d/puma.conf +10 -0
  67. data/tools/jungle/upstart/puma.conf +1 -1
  68. data/tools/trickletest.rb +1 -1
  69. metadata +28 -95
  70. data/COPYING +0 -55
  71. data/Gemfile +0 -13
  72. data/Manifest.txt +0 -74
  73. data/Rakefile +0 -158
  74. data/docs/config.md +0 -0
  75. data/lib/puma/capistrano.rb +0 -94
  76. data/lib/puma/rack/backports/uri/common_18.rb +0 -56
  77. data/lib/puma/rack/backports/uri/common_192.rb +0 -52
  78. data/puma.gemspec +0 -52
data/lib/puma/server.rb CHANGED
@@ -57,19 +57,19 @@ module Puma
57
57
 
58
58
  @min_threads = 0
59
59
  @max_threads = 16
60
- @auto_trim_time = 1
60
+ @auto_trim_time = 30
61
61
  @reaping_time = 1
62
62
 
63
63
  @thread = nil
64
64
  @thread_pool = nil
65
+ @early_hints = nil
65
66
 
66
- @persistent_timeout = PERSISTENT_TIMEOUT
67
+ @persistent_timeout = options.fetch(:persistent_timeout, PERSISTENT_TIMEOUT)
68
+ @first_data_timeout = options.fetch(:first_data_timeout, FIRST_DATA_TIMEOUT)
67
69
 
68
70
  @binder = Binder.new(events)
69
71
  @own_binder = true
70
72
 
71
- @first_data_timeout = FIRST_DATA_TIMEOUT
72
-
73
73
  @leak_stack_on_error = true
74
74
 
75
75
  @options = options
@@ -78,13 +78,16 @@ module Puma
78
78
  ENV['RACK_ENV'] ||= "development"
79
79
 
80
80
  @mode = :http
81
+
82
+ @precheck_closing = true
81
83
  end
82
84
 
83
- attr_accessor :binder, :leak_stack_on_error
85
+ attr_accessor :binder, :leak_stack_on_error, :early_hints
84
86
 
85
87
  forward :add_tcp_listener, :@binder
86
88
  forward :add_ssl_listener, :@binder
87
89
  forward :add_unix_listener, :@binder
90
+ forward :connected_port, :@binder
88
91
 
89
92
  def inherit_binder(bind)
90
93
  @binder = bind
@@ -99,6 +102,8 @@ module Puma
99
102
  # packetizes our stream. This improves both latency and throughput.
100
103
  #
101
104
  if RUBY_PLATFORM =~ /linux/
105
+ UNPACK_TCP_STATE_FROM_TCP_INFO = "C".freeze
106
+
102
107
  # 6 == Socket::IPPROTO_TCP
103
108
  # 3 == TCP_CORK
104
109
  # 1/0 == turn on/off
@@ -106,6 +111,7 @@ module Puma
106
111
  begin
107
112
  socket.setsockopt(6, 3, 1) if socket.kind_of? TCPSocket
108
113
  rescue IOError, SystemCallError
114
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
109
115
  end
110
116
  end
111
117
 
@@ -113,6 +119,24 @@ module Puma
113
119
  begin
114
120
  socket.setsockopt(6, 3, 0) if socket.kind_of? TCPSocket
115
121
  rescue IOError, SystemCallError
122
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
123
+ end
124
+ end
125
+
126
+ def closed_socket?(socket)
127
+ return false unless socket.kind_of? TCPSocket
128
+ return false unless @precheck_closing
129
+
130
+ begin
131
+ tcp_info = socket.getsockopt(Socket::SOL_TCP, Socket::TCP_INFO)
132
+ rescue IOError, SystemCallError
133
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
134
+ @precheck_closing = false
135
+ false
136
+ else
137
+ state = tcp_info.unpack(UNPACK_TCP_STATE_FROM_TCP_INFO)[0]
138
+ # TIME_WAIT: 6, CLOSE: 7, CLOSE_WAIT: 8, LAST_ACK: 9, CLOSING: 11
139
+ (state >= 6 && state <= 9) || state == 11
116
140
  end
117
141
  end
118
142
  else
@@ -121,6 +145,10 @@ module Puma
121
145
 
122
146
  def uncork_socket(socket)
123
147
  end
148
+
149
+ def closed_socket?(socket)
150
+ false
151
+ end
124
152
  end
125
153
 
126
154
  def backlog
@@ -192,7 +220,11 @@ module Puma
192
220
  # nothing
193
221
  rescue Errno::ECONNABORTED
194
222
  # client closed the socket even before accept
195
- io.close rescue nil
223
+ begin
224
+ io.close
225
+ rescue
226
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
227
+ end
196
228
  end
197
229
  end
198
230
  end
@@ -241,6 +273,10 @@ module Puma
241
273
  @thread_pool = ThreadPool.new(@min_threads,
242
274
  @max_threads,
243
275
  IOBuffer) do |client, buffer|
276
+
277
+ # Advertise this server into the thread
278
+ Thread.current[ThreadLocalKey] = self
279
+
244
280
  process_now = false
245
281
 
246
282
  begin
@@ -263,7 +299,7 @@ module Puma
263
299
  client.close
264
300
 
265
301
  @events.parse_error self, client.env, e
266
- rescue ConnectionError
302
+ rescue ConnectionError, EOFError
267
303
  client.close
268
304
  else
269
305
  if process_now
@@ -334,13 +370,17 @@ module Puma
334
370
  end
335
371
 
336
372
  pool << client
337
- pool.wait_until_not_full unless queue_requests
373
+ pool.wait_until_not_full
338
374
  end
339
375
  rescue SystemCallError
340
376
  # nothing
341
377
  rescue Errno::ECONNABORTED
342
378
  # client closed the socket even before accept
343
- io.close rescue nil
379
+ begin
380
+ io.close
381
+ rescue
382
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
383
+ end
344
384
  end
345
385
  end
346
386
  end
@@ -353,7 +393,7 @@ module Puma
353
393
 
354
394
  graceful_shutdown if @status == :stop || @status == :restart
355
395
  if queue_requests
356
- @reactor.clear! if @status == :restart
396
+ @reactor.clear!
357
397
  @reactor.shutdown
358
398
  end
359
399
  rescue Exception => e
@@ -398,6 +438,8 @@ module Puma
398
438
  #
399
439
  def process_client(client, buffer)
400
440
  begin
441
+
442
+ clean_thread_locals = @options[:clean_thread_locals]
401
443
  close_socket = true
402
444
 
403
445
  while true
@@ -411,6 +453,8 @@ module Puma
411
453
  return unless @queue_requests
412
454
  buffer.reset
413
455
 
456
+ ThreadPool.clean_thread_locals if clean_thread_locals
457
+
414
458
  unless client.reset(@status == :run)
415
459
  close_socket = false
416
460
  client.set_timeout @persistent_timeout
@@ -426,6 +470,8 @@ module Puma
426
470
 
427
471
  # SSL handshake error
428
472
  rescue MiniSSL::SSLError => e
473
+ lowlevel_error(e, client.env)
474
+
429
475
  ssl_socket = client.io
430
476
  addr = ssl_socket.peeraddr.last
431
477
  cert = ssl_socket.peercert
@@ -436,12 +482,16 @@ module Puma
436
482
 
437
483
  # The client doesn't know HTTP well
438
484
  rescue HttpParserError => e
485
+ lowlevel_error(e, client.env)
486
+
439
487
  client.write_400
440
488
 
441
489
  @events.parse_error self, client.env, e
442
490
 
443
491
  # Server error
444
492
  rescue StandardError => e
493
+ lowlevel_error(e, client.env)
494
+
445
495
  client.write_500
446
496
 
447
497
  @events.unknown_error self, e, "Read"
@@ -452,6 +502,7 @@ module Puma
452
502
  begin
453
503
  client.close if close_socket
454
504
  rescue IOError, SystemCallError
505
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
455
506
  # Already closed
456
507
  rescue StandardError => e
457
508
  @events.unknown_error self, e, "Client"
@@ -482,6 +533,10 @@ module Puma
482
533
  env[REQUEST_PATH] = uri.path
483
534
 
484
535
  raise "No REQUEST PATH" unless env[REQUEST_PATH]
536
+
537
+ # A nil env value will cause a LintError (and fatal errors elsewhere),
538
+ # so only set the env value if there actually is a value.
539
+ env[QUERY_STRING] = uri.query if uri.query
485
540
  end
486
541
 
487
542
  env[PATH_INFO] = env[REQUEST_PATH]
@@ -529,6 +584,8 @@ module Puma
529
584
  env = req.env
530
585
  client = req.io
531
586
 
587
+ return false if closed_socket?(client)
588
+
532
589
  normalize_env env, req
533
590
 
534
591
  env[PUMA_SOCKET] = client
@@ -547,6 +604,24 @@ module Puma
547
604
  env[RACK_INPUT] = body
548
605
  env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP
549
606
 
607
+ if @early_hints
608
+ env[EARLY_HINTS] = lambda { |headers|
609
+ fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
610
+
611
+ headers.each_pair do |k, vs|
612
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
613
+ vs.to_s.split(NEWLINE).each do |v|
614
+ fast_write client, "#{k}: #{v}\r\n"
615
+ end
616
+ else
617
+ fast_write client, "#{k}: #{vs}\r\n"
618
+ end
619
+ end
620
+
621
+ fast_write client, "\r\n".freeze
622
+ }
623
+ end
624
+
550
625
  # A rack extension. If the app writes #call'ables to this
551
626
  # array, we will invoke them when the request is done.
552
627
  #
@@ -567,10 +642,18 @@ module Puma
567
642
 
568
643
  return :async
569
644
  end
570
- rescue StandardError => e
645
+ rescue ThreadPool::ForceShutdown => e
646
+ @events.log "Detected force shutdown of a thread, returning 503"
571
647
  @events.unknown_error self, e, "Rack app"
572
648
 
573
- status, headers, res_body = lowlevel_error(e)
649
+ status = 503
650
+ headers = {}
651
+ res_body = ["Request was internally terminated early\n"]
652
+
653
+ rescue Exception => e
654
+ @events.unknown_error self, e, "Rack app", env
655
+
656
+ status, headers, res_body = lowlevel_error(e, env)
574
657
  end
575
658
 
576
659
  content_length = nil
@@ -636,7 +719,7 @@ module Puma
636
719
  next
637
720
  end
638
721
 
639
- if vs.respond_to?(:to_s)
722
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
640
723
  vs.to_s.split(NEWLINE).each do |v|
641
724
  lines.append k, colon, v, line_ending
642
725
  end
@@ -680,8 +763,8 @@ module Puma
680
763
 
681
764
  begin
682
765
  res_body.each do |part|
766
+ next if part.bytesize.zero?
683
767
  if chunked
684
- next if part.bytesize.zero?
685
768
  fast_write client, part.bytesize.to_s(16)
686
769
  fast_write client, line_ending
687
770
  fast_write client, part
@@ -719,7 +802,7 @@ module Puma
719
802
  end
720
803
  private :fetch_status_code
721
804
 
722
- # Given the requset +env+ from +client+ and the partial body +body+
805
+ # Given the request +env+ from +client+ and the partial body +body+
723
806
  # plus a potential Content-Length value +cl+, finish reading
724
807
  # the body and return it.
725
808
  #
@@ -775,9 +858,13 @@ module Puma
775
858
 
776
859
  # A fallback rack response if +@app+ raises as exception.
777
860
  #
778
- def lowlevel_error(e)
861
+ def lowlevel_error(e, env)
779
862
  if handler = @options[:lowlevel_error_handler]
780
- return handler.call(e)
863
+ if handler.arity == 1
864
+ return handler.call(e)
865
+ else
866
+ return handler.call(e, env)
867
+ end
781
868
  end
782
869
 
783
870
  if @leak_stack_on_error
@@ -827,38 +914,47 @@ module Puma
827
914
  @events.debug "Drained #{count} additional connections."
828
915
  end
829
916
 
830
- @thread_pool.shutdown if @thread_pool
917
+ if @thread_pool
918
+ if timeout = @options[:force_shutdown_after]
919
+ @thread_pool.shutdown timeout.to_i
920
+ else
921
+ @thread_pool.shutdown
922
+ end
923
+ end
831
924
  end
832
925
 
833
- # Stops the acceptor thread and then causes the worker threads to finish
834
- # off the request queue before finally exiting.
835
- #
836
- def stop(sync=false)
926
+ def notify_safely(message)
837
927
  begin
838
- @notify << STOP_COMMAND
928
+ @notify << message
839
929
  rescue IOError
840
- # The server, in another thread, is shutting down
930
+ # The server, in another thread, is shutting down
931
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
932
+ rescue RuntimeError => e
933
+ # Temporary workaround for https://bugs.ruby-lang.org/issues/13239
934
+ if e.message.include?('IOError')
935
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
936
+ else
937
+ raise e
938
+ end
841
939
  end
940
+ end
941
+ private :notify_safely
842
942
 
943
+ # Stops the acceptor thread and then causes the worker threads to finish
944
+ # off the request queue before finally exiting.
945
+
946
+ def stop(sync=false)
947
+ notify_safely(STOP_COMMAND)
843
948
  @thread.join if @thread && sync
844
949
  end
845
950
 
846
951
  def halt(sync=false)
847
- begin
848
- @notify << HALT_COMMAND
849
- rescue IOError
850
- # The server, in another thread, is shutting down
851
- end
852
-
952
+ notify_safely(HALT_COMMAND)
853
953
  @thread.join if @thread && sync
854
954
  end
855
955
 
856
956
  def begin_restart
857
- begin
858
- @notify << RESTART_COMMAND
859
- rescue IOError
860
- # The server, in another thread, is shutting down
861
- end
957
+ notify_safely(RESTART_COMMAND)
862
958
  end
863
959
 
864
960
  def fast_write(io, str)
@@ -881,5 +977,15 @@ module Puma
881
977
  end
882
978
  end
883
979
  private :fast_write
980
+
981
+ ThreadLocalKey = :puma_server
982
+
983
+ def self.current
984
+ Thread.current[ThreadLocalKey]
985
+ end
986
+
987
+ def shutting_down?
988
+ @status == :stop || @status == :restart
989
+ end
884
990
  end
885
991
  end
data/lib/puma/single.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  require 'puma/runner'
2
+ require 'puma/detect'
3
+ require 'puma/plugin'
2
4
 
3
5
  module Puma
4
6
  class Single < Runner
5
7
  def stats
6
- b = @server.backlog
7
- r = @server.running
8
+ b = @server.backlog || 0
9
+ r = @server.running || 0
8
10
  %Q!{ "backlog": #{b}, "running": #{r} }!
9
11
  end
10
12
 
@@ -27,7 +29,12 @@ module Puma
27
29
  end
28
30
 
29
31
  def jruby_daemon?
30
- daemon? and @cli.jruby?
32
+ daemon? and Puma.jruby?
33
+ end
34
+
35
+ def jruby_daemon_start
36
+ require 'puma/jruby_restart'
37
+ JRubyRestart.daemon_start(@restart_dir, @launcher.restart_args)
31
38
  end
32
39
 
33
40
  def run
@@ -39,6 +46,7 @@ module Puma
39
46
  if JRubyRestart.daemon?
40
47
  # load and bind before redirecting IO so errors show up on stdout/stderr
41
48
  load_and_bind
49
+ redirect_io
42
50
  end
43
51
 
44
52
  already_daemon = JRubyRestart.daemon_init
@@ -66,7 +74,7 @@ module Puma
66
74
  exit 1
67
75
  end
68
76
 
69
- pid = @cli.jruby_daemon_start
77
+ jruby_daemon_start
70
78
  sleep
71
79
  end
72
80
  else
@@ -79,7 +87,9 @@ module Puma
79
87
  load_and_bind
80
88
  end
81
89
 
82
- @cli.write_state
90
+ Plugins.fire_background
91
+
92
+ @launcher.write_state
83
93
 
84
94
  start_control
85
95
 
@@ -90,7 +100,7 @@ module Puma
90
100
  redirect_io
91
101
  end
92
102
 
93
- @cli.events.fire_on_booted!
103
+ @launcher.events.fire_on_booted!
94
104
 
95
105
  begin
96
106
  server.run.join
@@ -0,0 +1,29 @@
1
+ require 'yaml'
2
+
3
+ module Puma
4
+ class StateFile
5
+ def initialize
6
+ @options = {}
7
+ end
8
+
9
+ def save(path)
10
+ File.write path, YAML.dump(@options)
11
+ end
12
+
13
+ def load(path)
14
+ @options = YAML.load File.read(path)
15
+ end
16
+
17
+ FIELDS = %w!control_url control_auth_token pid!
18
+
19
+ FIELDS.each do |f|
20
+ define_method f do
21
+ @options[f]
22
+ end
23
+
24
+ define_method "#{f}=" do |v|
25
+ @options[f] = v
26
+ end
27
+ end
28
+ end
29
+ end
@@ -11,7 +11,14 @@ module Puma
11
11
  def log(who, str)
12
12
  now = Time.now.strftime("%d/%b/%Y %H:%M:%S")
13
13
 
14
- @logger.puts "#{now} - #{who} - #{str}"
14
+ log_str = "#{now} - #{who} - #{str}"
15
+
16
+ case @logger
17
+ when IO
18
+ @logger.puts log_str
19
+ when Events
20
+ @logger.log log_str
21
+ end
15
22
  end
16
23
 
17
24
  def call(env, socket)
@@ -4,6 +4,13 @@ module Puma
4
4
  # A simple thread pool management object.
5
5
  #
6
6
  class ThreadPool
7
+ class ForceShutdown < RuntimeError
8
+ end
9
+
10
+ # How long, after raising the ForceShutdown of a thread during
11
+ # forced shutdown mode, to wait for the thread to try and finish
12
+ # up its work before leaving the thread to die on the vine.
13
+ SHUTDOWN_GRACE_TIME = 5 # seconds
7
14
 
8
15
  # Maintain a minimum of +min+ and maximum of +max+ threads
9
16
  # in the pool.
@@ -45,6 +52,12 @@ module Puma
45
52
  attr_reader :spawned, :trim_requested
46
53
  attr_accessor :clean_thread_locals
47
54
 
55
+ def self.clean_thread_locals
56
+ Thread.current.keys.each do |key| # rubocop: disable Performance/HashEachMethods
57
+ Thread.current[key] = nil unless key == :__recursive_key__
58
+ end
59
+ end
60
+
48
61
  # How many objects have yet to be processed by the pool?
49
62
  #
50
63
  def backlog
@@ -58,7 +71,9 @@ module Puma
58
71
  def spawn_thread
59
72
  @spawned += 1
60
73
 
61
- th = Thread.new do
74
+ th = Thread.new(@spawned) do |spawned|
75
+ # Thread name is new in Ruby 2.3
76
+ Thread.current.name = 'puma %03i' % spawned if Thread.current.respond_to?(:name=)
62
77
  todo = @todo
63
78
  block = @block
64
79
  mutex = @mutex
@@ -77,6 +92,7 @@ module Puma
77
92
  if @trim_requested > 0
78
93
  @trim_requested -= 1
79
94
  continue = false
95
+ not_full.signal
80
96
  break
81
97
  end
82
98
 
@@ -97,14 +113,13 @@ module Puma
97
113
  break unless continue
98
114
 
99
115
  if @clean_thread_locals
100
- Thread.current.keys.each do |key|
101
- Thread.current[key] = nil unless key == :__recursive_key__
102
- end
116
+ ThreadPool.clean_thread_locals
103
117
  end
104
118
 
105
119
  begin
106
120
  block.call(work, *extra)
107
- rescue Exception
121
+ rescue Exception => e
122
+ STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
108
123
  end
109
124
  end
110
125
 
@@ -140,7 +155,15 @@ module Puma
140
155
 
141
156
  def wait_until_not_full
142
157
  @mutex.synchronize do
143
- until @todo.size - @waiting < @max - @spawned or @shutdown
158
+ while true
159
+ return if @shutdown
160
+ return if @waiting > 0
161
+
162
+ # If we can still spin up new threads and there
163
+ # is work queued, then accept more work until we would
164
+ # spin up the max number of threads.
165
+ return if @todo.size < @max - @spawned
166
+
144
167
  @not_full.wait @mutex
145
168
  end
146
169
  end
@@ -170,7 +193,9 @@ module Puma
170
193
  @spawned -= 1
171
194
  end
172
195
 
173
- @workers -= dead_workers
196
+ @workers.delete_if do |w|
197
+ dead_workers.include?(w)
198
+ end
174
199
  end
175
200
  end
176
201
 
@@ -198,7 +223,7 @@ module Puma
198
223
  end
199
224
  end
200
225
 
201
- def auto_trim!(timeout=5)
226
+ def auto_trim!(timeout=30)
202
227
  @auto_trim = AutoTrim.new(self, timeout)
203
228
  @auto_trim.start!
204
229
  end
@@ -234,7 +259,7 @@ module Puma
234
259
 
235
260
  # Tell all threads in the pool to exit and wait for them to finish.
236
261
  #
237
- def shutdown
262
+ def shutdown(timeout=-1)
238
263
  threads = @mutex.synchronize do
239
264
  @shutdown = true
240
265
  @not_empty.broadcast
@@ -246,7 +271,32 @@ module Puma
246
271
  @workers.dup
247
272
  end
248
273
 
249
- threads.each(&:join)
274
+ if timeout == -1
275
+ # Wait for threads to finish without force shutdown.
276
+ threads.each(&:join)
277
+ else
278
+ # Wait for threads to finish after n attempts (+timeout+).
279
+ # If threads are still running, it will forcefully kill them.
280
+ timeout.times do
281
+ threads.delete_if do |t|
282
+ t.join 1
283
+ end
284
+
285
+ if threads.empty?
286
+ break
287
+ else
288
+ sleep 1
289
+ end
290
+ end
291
+
292
+ threads.each do |t|
293
+ t.raise ForceShutdown
294
+ end
295
+
296
+ threads.each do |t|
297
+ t.join SHUTDOWN_GRACE_TIME
298
+ end
299
+ end
250
300
 
251
301
  @spawned = 0
252
302
  @workers = []
data/lib/puma/util.rb CHANGED
@@ -1,10 +1,6 @@
1
1
  major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
2
2
 
3
- if major == 1 && minor < 9
4
- require 'puma/rack/backports/uri/common_18'
5
- elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 328 && RUBY_ENGINE != 'jruby'
6
- require 'puma/rack/backports/uri/common_192'
7
- elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
3
+ if major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
8
4
  require 'puma/rack/backports/uri/common_193'
9
5
  else
10
6
  require 'uri/common'
data/lib/puma.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # Standard libraries
2
2
  require 'socket'
3
3
  require 'tempfile'
4
- require 'yaml'
5
4
  require 'time'
6
5
  require 'etc'
7
6
  require 'uri'
@@ -9,6 +8,16 @@ require 'stringio'
9
8
 
10
9
  require 'thread'
11
10
 
12
- # Ruby Puma
13
- require 'puma/const'
14
- require 'puma/server'
11
+ module Puma
12
+ autoload :Const, 'puma/const'
13
+ autoload :Server, 'puma/server'
14
+ autoload :Launcher, 'puma/launcher'
15
+
16
+ def self.stats_object=(val)
17
+ @get_stats = val
18
+ end
19
+
20
+ def self.stats
21
+ @get_stats.stats
22
+ end
23
+ end