puma 7.0.3 → 7.2.0

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.
data/lib/puma/server.rb CHANGED
@@ -13,14 +13,12 @@ require_relative 'binder'
13
13
  require_relative 'util'
14
14
  require_relative 'request'
15
15
  require_relative 'configuration'
16
+ require_relative 'cluster_accept_loop_delay'
16
17
 
17
18
  require 'socket'
18
19
  require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
19
20
 
20
21
  module Puma
21
- # Add `Thread#puma_server` and `Thread#puma_server=`
22
- Thread.attr_accessor(:puma_server)
23
-
24
22
  # The HTTP Server itself. Serves out a single Rack app.
25
23
  #
26
24
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
@@ -58,7 +56,6 @@ module Puma
58
56
  attr_accessor :app
59
57
  attr_accessor :binder
60
58
 
61
-
62
59
  # Create a server for the rack app +app+.
63
60
  #
64
61
  # +log_writer+ is a Puma::LogWriter object used to log info and error messages.
@@ -110,6 +107,10 @@ module Puma
110
107
  @enable_keep_alives &&= @queue_requests
111
108
  @io_selector_backend = @options[:io_selector_backend]
112
109
  @http_content_length_limit = @options[:http_content_length_limit]
110
+ @cluster_accept_loop_delay = ClusterAcceptLoopDelay.new(
111
+ workers: @options[:workers],
112
+ max_delay: @options[:wait_for_less_busy_worker] || 0 # Real default is in Configuration::DEFAULTS, this is for unit testing
113
+ )
113
114
 
114
115
  if @options[:fiber_per_request]
115
116
  singleton_class.prepend(FiberPerRequest)
@@ -245,11 +246,6 @@ module Puma
245
246
  @thread_pool&.pool_capacity
246
247
  end
247
248
 
248
- # @!attribute [r] busy_threads
249
- def busy_threads
250
- @thread_pool&.busy_threads
251
- end
252
-
253
249
  # Runs the server.
254
250
  #
255
251
  # If +background+ is true (the default) then a thread is spun
@@ -263,10 +259,14 @@ module Puma
263
259
 
264
260
  @status = :run
265
261
 
266
- @thread_pool = ThreadPool.new(thread_name, options) { |client| process_client client }
262
+ @thread_pool = ThreadPool.new(thread_name, options, server: self) { |client| process_client client }
267
263
 
268
264
  if @queue_requests
269
- @reactor = Reactor.new(@io_selector_backend) { |c| reactor_wakeup c }
265
+ @reactor = Reactor.new(@io_selector_backend) { |c|
266
+ # Inversion of control, the reactor is calling a method on the server when it
267
+ # is done buffering a request or receives a new request from a keepalive connection.
268
+ self.reactor_wakeup(c)
269
+ }
270
270
  @reactor.run
271
271
  end
272
272
 
@@ -291,12 +291,15 @@ module Puma
291
291
  # This method is called from the Reactor thread when a queued Client receives data,
292
292
  # times out, or when the Reactor is shutting down.
293
293
  #
294
+ # While the code lives in the Server, the logic is executed on the reactor thread, independently
295
+ # from the server.
296
+ #
294
297
  # It is responsible for ensuring that a request has been completely received
295
298
  # before it starts to be processed by the ThreadPool. This may be known as read buffering.
296
299
  # If read buffering is not done, and no other read buffering is performed (such as by an application server
297
300
  # such as nginx) then the application would be subject to a slow client attack.
298
301
  #
299
- # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline).
302
+ # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/main/docs/architecture.md).
300
303
  #
301
304
  # The method checks to see if it has the full header and body with
302
305
  # the `Puma::Client#try_to_finish` method. If the full request has been sent,
@@ -325,7 +328,7 @@ module Puma
325
328
  end
326
329
  rescue StandardError => e
327
330
  client_error(e, client)
328
- client.close
331
+ close_client_safely(client)
329
332
  true
330
333
  end
331
334
 
@@ -339,7 +342,6 @@ module Puma
339
342
  pool = @thread_pool
340
343
  queue_requests = @queue_requests
341
344
  drain = options[:drain_on_shutdown] ? 0 : nil
342
- max_flt = @max_threads.to_f
343
345
 
344
346
  addr_send_name, addr_value = case options[:remote_address]
345
347
  when :value
@@ -384,15 +386,13 @@ module Puma
384
386
  # clients until the code is finished.
385
387
  pool.wait_while_out_of_band_running
386
388
 
387
- # only use delay when clustered and busy
388
- if pool.busy_threads >= @max_threads
389
- if @clustered
390
- delay = 0.0001 * ((@reactor&.reactor_size || 0) + pool.busy_threads * 1.5)/max_flt
391
- sleep delay
392
- else
393
- # use small sleep for busy single worker
394
- sleep 0.0001
395
- end
389
+ # A well rested herd (cluster) runs faster
390
+ if @cluster_accept_loop_delay.on? && (busy_threads_plus_todo = pool.busy_threads) > 0
391
+ delay = @cluster_accept_loop_delay.calculate(
392
+ max_threads: @max_threads,
393
+ busy_threads_plus_todo: busy_threads_plus_todo
394
+ )
395
+ sleep(delay)
396
396
  end
397
397
 
398
398
  io = begin
@@ -401,11 +401,9 @@ module Puma
401
401
  next
402
402
  end
403
403
  drain += 1 if shutting_down?
404
- pool << Client.new(io, @binder.env(sock)).tap { |c|
405
- c.listener = sock
406
- c.http_content_length_limit = @http_content_length_limit
407
- c.send(addr_send_name, addr_value) if addr_value
408
- }
404
+ client = new_client(io, sock)
405
+ client.send(addr_send_name, addr_value) if addr_value
406
+ pool << client
409
407
  end
410
408
  end
411
409
  rescue IOError, Errno::EBADF
@@ -442,6 +440,14 @@ module Puma
442
440
  @events.fire :state, :done
443
441
  end
444
442
 
443
+ # :nodoc:
444
+ def new_client(io, sock)
445
+ client = Client.new(io, @binder.env(sock))
446
+ client.listener = sock
447
+ client.http_content_length_limit = @http_content_length_limit
448
+ client
449
+ end
450
+
445
451
  # :nodoc:
446
452
  def handle_check
447
453
  cmd = @check.read(1)
@@ -472,9 +478,6 @@ module Puma
472
478
  #
473
479
  # Return true if one or more requests were processed.
474
480
  def process_client(client)
475
- # Advertise this server into the thread
476
- Thread.current.puma_server = self
477
-
478
481
  close_socket = true
479
482
 
480
483
  requests = 0
@@ -493,31 +496,41 @@ module Puma
493
496
  client.finish(@first_data_timeout)
494
497
  end
495
498
 
496
- @requests_count += 1
497
- case handle_request(client, requests + 1)
498
- when false
499
- when :async
500
- close_socket = false
501
- when true
502
- requests += 1
499
+ can_loop = true
500
+ while can_loop
501
+ can_loop = false
502
+ @requests_count += 1
503
+ case handle_request(client, requests + 1)
504
+ when :close
505
+ when :async
506
+ close_socket = false
507
+ when :keep_alive
508
+ requests += 1
503
509
 
504
- client.reset
510
+ client.reset
505
511
 
506
- # This indicates data exists in the client read buffer and there may be
507
- # additional requests on it, so process them
508
- next_request_ready = if client.has_back_to_back_requests?
509
- with_force_shutdown(client) { client.process_back_to_back_requests }
510
- else
511
- with_force_shutdown(client) { client.eagerly_finish }
512
- end
512
+ # This indicates data exists in the client read buffer and there may be
513
+ # additional requests on it, so process them
514
+ next_request_ready = if client.has_back_to_back_requests?
515
+ with_force_shutdown(client) { client.process_back_to_back_requests }
516
+ else
517
+ with_force_shutdown(client) { client.eagerly_finish }
518
+ end
513
519
 
514
- if next_request_ready
515
- @thread_pool << client
516
- close_socket = false
517
- elsif @queue_requests
518
- client.set_timeout @persistent_timeout
519
- if @reactor.add client
520
- close_socket = false
520
+ if next_request_ready
521
+ # When Puma has spare threads, allow this one to be monopolized
522
+ # Perf optimization for https://github.com/puma/puma/issues/3788
523
+ if @thread_pool.waiting > 0
524
+ can_loop = true
525
+ else
526
+ @thread_pool << client
527
+ close_socket = false
528
+ end
529
+ elsif @queue_requests
530
+ client.set_timeout @persistent_timeout
531
+ if @reactor.add client
532
+ close_socket = false
533
+ end
521
534
  end
522
535
  end
523
536
  end
@@ -529,16 +542,21 @@ module Puma
529
542
  ensure
530
543
  client.io_buffer.reset
531
544
 
532
- begin
533
- client.close if close_socket
534
- rescue IOError, SystemCallError
535
- # Already closed
536
- rescue StandardError => e
537
- @log_writer.unknown_error e, nil, "Client"
538
- end
545
+ close_client_safely(client) if close_socket
539
546
  end
540
547
  end
541
548
 
549
+ # :nodoc:
550
+ def close_client_safely(client)
551
+ client.close
552
+ rescue IOError, SystemCallError
553
+ # Already closed
554
+ rescue MiniSSL::SSLError => e
555
+ @log_writer.ssl_error e, client.io
556
+ rescue StandardError => e
557
+ @log_writer.unknown_error e, nil, "Client"
558
+ end
559
+
542
560
  # Triggers a client timeout if the thread-pool shuts down
543
561
  # during execution of the provided block.
544
562
  def with_force_shutdown(client, &block)
@@ -692,7 +710,7 @@ module Puma
692
710
 
693
711
  def reset_max
694
712
  @reactor.reactor_max = 0 if @reactor
695
- @thread_pool.reset_max
713
+ @thread_pool&.reset_max
696
714
  end
697
715
 
698
716
  # below are 'delegations' to binder
data/lib/puma/single.rb CHANGED
@@ -49,8 +49,8 @@ module Puma
49
49
 
50
50
  start_control
51
51
 
52
- @server = server = start_server
53
- server_thread = server.run
52
+ @server = start_server
53
+ server_thread = @server.run
54
54
 
55
55
  log "Use Ctrl-C to stop"
56
56
 
@@ -32,10 +32,11 @@ module Puma
32
32
  "#{k}: \"#{v}\"\n" : "#{k}: #{v}\n")
33
33
  end
34
34
  end
35
+
35
36
  if permission
36
- File.write path, contents, mode: 'wb:UTF-8'
37
- else
38
37
  File.write path, contents, mode: 'wb:UTF-8', perm: permission
38
+ else
39
+ File.write path, contents, mode: 'wb:UTF-8'
39
40
  end
40
41
  end
41
42
 
@@ -5,6 +5,10 @@ require 'thread'
5
5
  require_relative 'io_buffer'
6
6
 
7
7
  module Puma
8
+
9
+ # Add `Thread#puma_server` and `Thread#puma_server=`
10
+ Thread.attr_accessor(:puma_server)
11
+
8
12
  # Internal Docs for A simple thread pool management object.
9
13
  #
10
14
  # Each Puma "worker" has a thread pool to process requests.
@@ -33,7 +37,9 @@ module Puma
33
37
  # The block passed is the work that will be performed in each
34
38
  # thread.
35
39
  #
36
- def initialize(name, options = {}, &block)
40
+ def initialize(name, options = {}, server: nil, &block)
41
+ @server = server
42
+
37
43
  @not_empty = ConditionVariable.new
38
44
  @not_full = ConditionVariable.new
39
45
  @mutex = Mutex.new
@@ -134,6 +140,9 @@ module Puma
134
140
  trigger_before_thread_start_hooks
135
141
  th = Thread.new(@spawned) do |spawned|
136
142
  Puma.set_thread_name '%s tp %03i' % [@name, spawned]
143
+ # Advertise server into the thread
144
+ Thread.current.puma_server = @server
145
+
137
146
  todo = @todo
138
147
  block = @block
139
148
  mutex = @mutex
data/tools/Dockerfile CHANGED
@@ -1,18 +1,26 @@
1
1
  # Use this Dockerfile to create minimal reproductions of issues
2
+ # Build (MRI): docker build -f tools/Dockerfile .
3
+ # Build (JRuby): docker build -f tools/Dockerfile --build-arg RUBY_IMAGE=jruby:9.4 .
2
4
 
3
- FROM ruby:3.2
5
+ ARG RUBY_IMAGE=ruby:latest
6
+ FROM ${RUBY_IMAGE}
4
7
 
5
- RUN apt-get update && apt-get install -y ragel
8
+ # Set BUNDLE_FROZEN=false if you need to update Gemfile.lock during a build.
9
+ ARG BUNDLE_FROZEN=true
6
10
 
7
- # throw errors if Gemfile has been modified since Gemfile.lock
8
- RUN bundle config --global frozen 1
11
+ RUN apt-get update \
12
+ && apt-get install -y --no-install-recommends ragel procps git \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Only freeze Bundler and compile native extensions when using MRI.
16
+ RUN if [ "$(ruby -e 'print RUBY_ENGINE')" = "ruby" ] && [ "${BUNDLE_FROZEN}" = "true" ]; then bundle config --global frozen 1; fi
9
17
 
10
18
  WORKDIR /usr/src/app
11
19
 
12
20
  COPY . .
13
21
 
14
22
  RUN bundle install
15
- RUN bundle exec rake compile
23
+ RUN if [ "$(ruby -e 'print RUBY_ENGINE')" = "ruby" ]; then bundle exec rake compile; fi
16
24
 
17
25
  EXPOSE 9292
18
26
  CMD ["bundle", "exec", "bin/puma", "test/rackup/hello.ru"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.3
4
+ version: 7.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Phoenix
@@ -66,7 +66,6 @@ files:
66
66
  - docs/testing_benchmarks_local_files.md
67
67
  - docs/testing_test_rackup_ci_files.md
68
68
  - ext/puma_http11/PumaHttp11Service.java
69
- - ext/puma_http11/ext_help.h
70
69
  - ext/puma_http11/extconf.rb
71
70
  - ext/puma_http11/http11_parser.c
72
71
  - ext/puma_http11/http11_parser.h
@@ -87,6 +86,7 @@ files:
87
86
  - lib/puma/cluster.rb
88
87
  - lib/puma/cluster/worker.rb
89
88
  - lib/puma/cluster/worker_handle.rb
89
+ - lib/puma/cluster_accept_loop_delay.rb
90
90
  - lib/puma/commonlogger.rb
91
91
  - lib/puma/configuration.rb
92
92
  - lib/puma/const.rb
@@ -127,10 +127,11 @@ licenses:
127
127
  - BSD-3-Clause
128
128
  metadata:
129
129
  bug_tracker_uri: https://github.com/puma/puma/issues
130
- changelog_uri: https://github.com/puma/puma/blob/master/History.md
130
+ changelog_uri: https://github.com/puma/puma/blob/main/History.md
131
131
  homepage_uri: https://puma.io
132
132
  source_code_uri: https://github.com/puma/puma
133
133
  rubygems_mfa_required: 'true'
134
+ msys2_mingw_dependencies: openssl
134
135
  rdoc_options: []
135
136
  require_paths:
136
137
  - lib
@@ -145,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
146
  - !ruby/object:Gem::Version
146
147
  version: '0'
147
148
  requirements: []
148
- rubygems_version: 3.6.9
149
+ rubygems_version: 4.0.3
149
150
  specification_version: 4
150
151
  summary: A Ruby/Rack web server built for parallelism.
151
152
  test_files: []
@@ -1,15 +0,0 @@
1
- #ifndef ext_help_h
2
- #define ext_help_h
3
-
4
- #define RAISE_NOT_NULL(T) if(T == NULL) rb_raise(rb_eArgError, "%s", "NULL found for " # T " when shouldn't be.");
5
- #define DATA_GET(from,type,data_type,name) TypedData_Get_Struct(from,type,data_type,name); RAISE_NOT_NULL(name);
6
- #define REQUIRE_TYPE(V, T) if(TYPE(V) != T) rb_raise(rb_eTypeError, "%s", "Wrong argument type for " # V " required " # T);
7
- #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
8
-
9
- #ifdef DEBUG
10
- #define TRACE() fprintf(stderr, "> %s:%d:%s\n", __FILE__, __LINE__, __FUNCTION__)
11
- #else
12
- #define TRACE()
13
- #endif
14
-
15
- #endif