puma 2.2.0 → 8.0.2

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 (136) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +3334 -0
  3. data/LICENSE +23 -20
  4. data/README.md +387 -100
  5. data/bin/puma-wild +25 -0
  6. data/bin/pumactl +1 -1
  7. data/docs/5.0-Upgrade.md +98 -0
  8. data/docs/6.0-Upgrade.md +56 -0
  9. data/docs/7.0-Upgrade.md +52 -0
  10. data/docs/8.0-Upgrade.md +100 -0
  11. data/docs/architecture.md +74 -0
  12. data/docs/compile_options.md +55 -0
  13. data/docs/deployment.md +137 -0
  14. data/docs/fork_worker.md +41 -0
  15. data/docs/grpc.md +62 -0
  16. data/docs/images/favicon.svg +1 -0
  17. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  18. data/docs/images/puma-connection-flow.png +0 -0
  19. data/docs/images/puma-general-arch.png +0 -0
  20. data/docs/images/running-puma.svg +1 -0
  21. data/docs/images/standard-logo.svg +1 -0
  22. data/docs/java_options.md +54 -0
  23. data/docs/jungle/README.md +9 -0
  24. data/docs/jungle/rc.d/README.md +74 -0
  25. data/docs/jungle/rc.d/puma +61 -0
  26. data/docs/jungle/rc.d/puma.conf +10 -0
  27. data/docs/kubernetes.md +73 -0
  28. data/docs/nginx.md +5 -5
  29. data/docs/plugins.md +42 -0
  30. data/docs/rails_dev_mode.md +28 -0
  31. data/docs/restart.md +65 -0
  32. data/docs/signals.md +98 -0
  33. data/docs/stats.md +148 -0
  34. data/docs/systemd.md +253 -0
  35. data/docs/testing_benchmarks_local_files.md +150 -0
  36. data/docs/testing_test_rackup_ci_files.md +36 -0
  37. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  38. data/ext/puma_http11/extconf.rb +59 -2
  39. data/ext/puma_http11/http11_parser.c +319 -487
  40. data/ext/puma_http11/http11_parser.h +15 -13
  41. data/ext/puma_http11/http11_parser.java.rl +64 -94
  42. data/ext/puma_http11/http11_parser.rl +27 -24
  43. data/ext/puma_http11/http11_parser_common.rl +8 -8
  44. data/ext/puma_http11/mini_ssl.c +696 -38
  45. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  46. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  47. data/ext/puma_http11/org/jruby/puma/Http11.java +241 -145
  48. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +174 -221
  49. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +413 -193
  50. data/ext/puma_http11/puma_http11.c +183 -175
  51. data/lib/puma/app/status.rb +77 -29
  52. data/lib/puma/binder.rb +349 -119
  53. data/lib/puma/cli.rb +163 -801
  54. data/lib/puma/client.rb +627 -144
  55. data/lib/puma/client_env.rb +171 -0
  56. data/lib/puma/cluster/worker.rb +183 -0
  57. data/lib/puma/cluster/worker_handle.rb +127 -0
  58. data/lib/puma/cluster.rb +634 -0
  59. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  60. data/lib/puma/commonlogger.rb +115 -0
  61. data/lib/puma/configuration.rb +420 -198
  62. data/lib/puma/const.rb +266 -115
  63. data/lib/puma/control_cli.rb +219 -120
  64. data/lib/puma/detect.rb +56 -2
  65. data/lib/puma/dsl.rb +1562 -0
  66. data/lib/puma/error_logger.rb +115 -0
  67. data/lib/puma/events.rb +44 -60
  68. data/lib/puma/io_buffer.rb +48 -5
  69. data/lib/puma/jruby_restart.rb +2 -51
  70. data/lib/puma/json_serialization.rb +96 -0
  71. data/lib/puma/launcher/bundle_pruner.rb +102 -0
  72. data/lib/puma/launcher.rb +501 -0
  73. data/lib/puma/log_writer.rb +153 -0
  74. data/lib/puma/minissl/context_builder.rb +96 -0
  75. data/lib/puma/minissl.rb +355 -37
  76. data/lib/puma/null_io.rb +79 -12
  77. data/lib/puma/plugin/systemd.rb +90 -0
  78. data/lib/puma/plugin/tmp_restart.rb +36 -0
  79. data/lib/puma/plugin.rb +111 -0
  80. data/lib/puma/rack/builder.rb +297 -0
  81. data/lib/puma/rack/urlmap.rb +93 -0
  82. data/lib/puma/rack_default.rb +21 -4
  83. data/lib/puma/reactor.rb +102 -133
  84. data/lib/puma/response.rb +532 -0
  85. data/lib/puma/runner.rb +211 -0
  86. data/lib/puma/sd_notify.rb +146 -0
  87. data/lib/puma/server.rb +582 -445
  88. data/lib/puma/server_plugin_control.rb +32 -0
  89. data/lib/puma/single.rb +72 -0
  90. data/lib/puma/state_file.rb +69 -0
  91. data/lib/puma/thread_pool.rb +389 -57
  92. data/lib/puma/util.rb +125 -0
  93. data/lib/puma.rb +80 -6
  94. data/lib/rack/handler/puma.rb +125 -47
  95. data/tools/Dockerfile +26 -0
  96. data/tools/trickletest.rb +44 -0
  97. metadata +88 -148
  98. data/COPYING +0 -55
  99. data/Gemfile +0 -10
  100. data/History.txt +0 -302
  101. data/Manifest.txt +0 -60
  102. data/Rakefile +0 -165
  103. data/TODO +0 -5
  104. data/docs/config.md +0 -0
  105. data/ext/puma_http11/ext_help.h +0 -15
  106. data/ext/puma_http11/io_buffer.c +0 -154
  107. data/lib/puma/accept_nonblock.rb +0 -23
  108. data/lib/puma/capistrano.rb +0 -34
  109. data/lib/puma/compat.rb +0 -11
  110. data/lib/puma/daemon_ext.rb +0 -20
  111. data/lib/puma/delegation.rb +0 -11
  112. data/lib/puma/java_io_buffer.rb +0 -45
  113. data/lib/puma/rack_patch.rb +0 -25
  114. data/puma.gemspec +0 -45
  115. data/test/test_app_status.rb +0 -88
  116. data/test/test_cli.rb +0 -171
  117. data/test/test_config.rb +0 -16
  118. data/test/test_http10.rb +0 -27
  119. data/test/test_http11.rb +0 -126
  120. data/test/test_integration.rb +0 -154
  121. data/test/test_iobuffer.rb +0 -38
  122. data/test/test_minissl.rb +0 -25
  123. data/test/test_null_io.rb +0 -31
  124. data/test/test_persistent.rb +0 -238
  125. data/test/test_puma_server.rb +0 -224
  126. data/test/test_rack_handler.rb +0 -10
  127. data/test/test_rack_server.rb +0 -141
  128. data/test/test_thread_pool.rb +0 -146
  129. data/test/test_unix_socket.rb +0 -39
  130. data/test/test_ws.rb +0 -89
  131. data/tools/jungle/init.d/README.md +0 -54
  132. data/tools/jungle/init.d/puma +0 -332
  133. data/tools/jungle/init.d/run-puma +0 -3
  134. data/tools/jungle/upstart/README.md +0 -61
  135. data/tools/jungle/upstart/puma-manager.conf +0 -31
  136. data/tools/jungle/upstart/puma.conf +0 -52
data/lib/puma/server.rb CHANGED
@@ -1,104 +1,195 @@
1
- require 'rack'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'stringio'
3
4
 
4
- require 'puma/thread_pool'
5
- require 'puma/const'
6
- require 'puma/events'
7
- require 'puma/null_io'
8
- require 'puma/compat'
9
- require 'puma/reactor'
10
- require 'puma/client'
11
- require 'puma/binder'
12
- require 'puma/delegation'
13
- require 'puma/accept_nonblock'
14
- require 'puma/util'
15
-
16
- require 'puma/puma_http11'
17
-
18
- unless Puma.const_defined? "IOBuffer"
19
- require 'puma/io_buffer'
20
- end
5
+ require_relative 'thread_pool'
6
+ require_relative 'const'
7
+ require_relative 'log_writer'
8
+ require_relative 'events'
9
+ require_relative 'null_io'
10
+ require_relative 'reactor'
11
+ require_relative 'client'
12
+ require_relative 'binder'
13
+ require_relative 'util'
14
+ require_relative 'response'
15
+ require_relative 'configuration'
16
+ require_relative 'cluster_accept_loop_delay'
21
17
 
22
18
  require 'socket'
19
+ require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
23
20
 
24
21
  module Puma
25
-
26
22
  # The HTTP Server itself. Serves out a single Rack app.
23
+ #
24
+ # This class is used by the `Puma::Single` and `Puma::Cluster` classes
25
+ # to generate one or more `Puma::Server` instances capable of handling requests.
26
+ # Each Puma process will contain one `Puma::Server` instance.
27
+ #
28
+ # The `Puma::Server` instance pulls requests from the socket, adds them to a
29
+ # `Puma::Reactor` where they get eventually passed to a `Puma::ThreadPool`.
30
+ #
31
+ # Each `Puma::Server` will have one reactor and one thread pool.
27
32
  class Server
33
+ module FiberPerRequest
34
+ def handle_request(processor, client, requests)
35
+ Fiber.new do
36
+ super
37
+ end.resume
38
+ end
39
+ end
28
40
 
29
- include Puma::Const
30
- extend Puma::Delegation
41
+ include Const
42
+ include Response
31
43
 
44
+ attr_reader :options
32
45
  attr_reader :thread
46
+ attr_reader :log_writer
33
47
  attr_reader :events
34
- attr_accessor :app
48
+ attr_reader :min_threads, :max_threads # for #stats
49
+ attr_reader :requests_count # @version 5.0.0
35
50
 
36
- attr_accessor :min_threads
37
- attr_accessor :max_threads
38
- attr_accessor :persistent_timeout
39
- attr_accessor :auto_trim_time
51
+ # @todo the following may be deprecated in the future
52
+ attr_reader :auto_trim_time, :early_hints, :first_data_timeout,
53
+ :leak_stack_on_error,
54
+ :persistent_timeout, :reaping_time
55
+
56
+ attr_accessor :app
57
+ attr_accessor :binder
40
58
 
41
59
  # Create a server for the rack app +app+.
42
60
  #
43
- # +events+ is an object which will be called when certain error events occur
44
- # to be handled. See Puma::Events for the list of current methods to implement.
61
+ # +log_writer+ is a Puma::LogWriter object used to log info and error messages.
62
+ #
63
+ # +events+ is a Puma::Events object used to notify application status events.
45
64
  #
46
65
  # Server#run returns a thread that you can join on to wait for the server
47
- # to do it's work.
66
+ # to do its work.
67
+ #
68
+ # @note Several instance variables exist so they are available for testing,
69
+ # and have default values set via +fetch+. Normally the values are set via
70
+ # `::Puma::Configuration.puma_default_options`.
48
71
  #
49
- def initialize(app, events=Events::DEFAULT)
72
+ # @note The `events` parameter is set to nil, and set to `Events.new` in code.
73
+ # Often `options` needs to be passed, but `events` does not. Using nil allows
74
+ # calling code to not require events.rb.
75
+ #
76
+ def initialize(app, events = nil, options = {})
50
77
  @app = app
51
- @events = events
52
-
53
- @check, @notify = Puma::Util.pipe
78
+ @events = events || Events.new
54
79
 
80
+ @check, @notify = nil
55
81
  @status = :stop
56
82
 
57
- @min_threads = 0
58
- @max_threads = 16
59
- @auto_trim_time = 1
60
-
61
83
  @thread = nil
62
84
  @thread_pool = nil
85
+ @reactor = nil
86
+
87
+ @env_set_http_version = nil
88
+
89
+ @options = if options.is_a?(UserFileDefaultOptions)
90
+ options
91
+ else
92
+ UserFileDefaultOptions.new(options, Configuration::DEFAULTS)
93
+ end
63
94
 
64
- @persistent_timeout = PERSISTENT_TIMEOUT
95
+ @clustered = (@options.fetch :workers, 0) > 0
96
+ @worker_write = @options[:worker_write]
97
+ @log_writer = @options.fetch :log_writer, LogWriter.stdio
98
+ @early_hints = @options[:early_hints]
99
+ @first_data_timeout = @options[:first_data_timeout]
100
+ @persistent_timeout = @options[:persistent_timeout]
101
+ @idle_timeout = @options[:idle_timeout]
102
+ @min_threads = @options[:min_threads]
103
+ @max_threads = @options[:max_threads]
104
+ @queue_requests = @options[:queue_requests]
105
+ @max_keep_alive = @options[:max_keep_alive]
106
+ @enable_keep_alives = @options[:enable_keep_alives]
107
+ @enable_keep_alives &&= @queue_requests
108
+ @io_selector_backend = @options[:io_selector_backend]
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
+ )
114
+
115
+ if @options[:fiber_per_request]
116
+ singleton_class.prepend(FiberPerRequest)
117
+ end
65
118
 
66
- @binder = Binder.new(events)
67
- @own_binder = true
119
+ # make this a hash, since we prefer `key?` over `include?`
120
+ @supported_http_methods =
121
+ if @options[:supported_http_methods] == :any
122
+ :any
123
+ else
124
+ if (ary = @options[:supported_http_methods])
125
+ ary
126
+ else
127
+ SUPPORTED_HTTP_METHODS
128
+ end.sort.product([nil]).to_h.freeze
129
+ end
68
130
 
69
- @first_data_timeout = FIRST_DATA_TIMEOUT
131
+ temp = !!(@options[:environment] =~ /\A(development|test)\z/)
132
+ @leak_stack_on_error = @options[:environment] ? temp : true
70
133
 
71
- @leak_stack_on_error = true
134
+ @binder = Binder.new(log_writer, @options)
72
135
 
73
136
  ENV['RACK_ENV'] ||= "development"
74
- end
75
137
 
76
- attr_accessor :binder, :leak_stack_on_error
138
+ @mode = :http
139
+
140
+ @precheck_closing = true
141
+
142
+ @requests_count = 0
77
143
 
78
- forward :add_tcp_listener, :@binder
79
- forward :add_ssl_listener, :@binder
80
- forward :add_unix_listener, :@binder
144
+ @idle_timeout_reached = false
145
+ end
81
146
 
82
147
  def inherit_binder(bind)
83
148
  @binder = bind
84
- @own_binder = false
149
+ end
150
+
151
+ class << self
152
+ # @!attribute [r] current
153
+ def current
154
+ Thread.current.puma_server
155
+ end
156
+
157
+ # :nodoc:
158
+ # @version 5.0.0
159
+ def tcp_cork_supported?
160
+ Socket.const_defined?(:TCP_CORK) && Socket.const_defined?(:IPPROTO_TCP)
161
+ end
162
+
163
+ # :nodoc:
164
+ # @version 5.0.0
165
+ def closed_socket_supported?
166
+ Socket.const_defined?(:TCP_INFO) && Socket.const_defined?(:IPPROTO_TCP)
167
+ end
168
+ private :tcp_cork_supported?
169
+ private :closed_socket_supported?
85
170
  end
86
171
 
87
172
  # On Linux, use TCP_CORK to better control how the TCP stack
88
173
  # packetizes our stream. This improves both latency and throughput.
174
+ # socket parameter may be an MiniSSL::Socket, so use to_io
89
175
  #
90
- if RUBY_PLATFORM =~ /linux/
176
+ if tcp_cork_supported?
91
177
  # 6 == Socket::IPPROTO_TCP
92
178
  # 3 == TCP_CORK
93
179
  # 1/0 == turn on/off
94
180
  def cork_socket(socket)
95
- socket.setsockopt(6, 3, 1) if socket.kind_of? TCPSocket
181
+ skt = socket.to_io
182
+ begin
183
+ skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 1) if skt.kind_of? TCPSocket
184
+ rescue IOError, SystemCallError
185
+ end
96
186
  end
97
187
 
98
188
  def uncork_socket(socket)
189
+ skt = socket.to_io
99
190
  begin
100
- socket.setsockopt(6, 3, 0) if socket.kind_of? TCPSocket
101
- rescue IOError
191
+ skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 0) if skt.kind_of? TCPSocket
192
+ rescue IOError, SystemCallError
102
193
  end
103
194
  end
104
195
  else
@@ -109,12 +200,50 @@ module Puma
109
200
  end
110
201
  end
111
202
 
203
+ if closed_socket_supported?
204
+ UNPACK_TCP_STATE_FROM_TCP_INFO = "C".freeze
205
+
206
+ def closed_socket?(socket)
207
+ skt = socket.to_io
208
+ return false unless skt.kind_of?(TCPSocket) && @precheck_closing
209
+
210
+ begin
211
+ tcp_info = skt.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO)
212
+ rescue IOError, SystemCallError
213
+ @precheck_closing = false
214
+ false
215
+ else
216
+ state = tcp_info.unpack(UNPACK_TCP_STATE_FROM_TCP_INFO)[0]
217
+ # TIME_WAIT: 6, CLOSE: 7, CLOSE_WAIT: 8, LAST_ACK: 9, CLOSING: 11
218
+ (state >= 6 && state <= 9) || state == 11
219
+ end
220
+ end
221
+ else
222
+ def closed_socket?(socket)
223
+ false
224
+ end
225
+ end
226
+
227
+ # @!attribute [r] backlog
112
228
  def backlog
113
- @thread_pool and @thread_pool.backlog
229
+ @thread_pool&.backlog
114
230
  end
115
231
 
232
+ # @!attribute [r] running
116
233
  def running
117
- @thread_pool and @thread_pool.spawned
234
+ @thread_pool&.spawned
235
+ end
236
+
237
+ # This number represents the number of requests that
238
+ # the server is capable of taking right now.
239
+ #
240
+ # For example if the number is 5 then it means
241
+ # there are 5 threads sitting idle ready to take
242
+ # a request. If one request comes in, then the
243
+ # value would be 4 until it finishes processing.
244
+ # @!attribute [r] pool_capacity
245
+ def pool_capacity
246
+ @thread_pool&.pool_capacity
118
247
  end
119
248
 
120
249
  # Runs the server.
@@ -123,101 +252,210 @@ module Puma
123
252
  # up in the background to handle requests. Otherwise requests
124
253
  # are handled synchronously.
125
254
  #
126
- def run(background=true)
255
+ def run(background=true, thread_name: 'srv')
127
256
  BasicSocket.do_not_reverse_lookup = true
128
257
 
258
+ @events.fire :state, :booting
259
+
129
260
  @status = :run
130
261
 
131
- @thread_pool = ThreadPool.new(@min_threads,
132
- @max_threads,
133
- IOBuffer) do |client, buffer|
134
- process_now = false
262
+ @thread_pool = ThreadPool.new(thread_name, options, server: self) do |processor, client|
263
+ process_client(processor, client)
264
+ end
135
265
 
136
- begin
137
- process_now = client.eagerly_finish
138
- rescue HttpParserError => e
139
- client.write_400
140
- client.close
141
-
142
- @events.parse_error self, client.env, e
143
- rescue ConnectionError
144
- client.close
145
- else
146
- if process_now
147
- process_client client, buffer
148
- else
149
- client.set_timeout @first_data_timeout
150
- @reactor.add client
151
- end
152
- end
266
+ if @queue_requests
267
+ @reactor = Reactor.new(@io_selector_backend) { |c|
268
+ # Inversion of control, the reactor is calling a method on the server when it
269
+ # is done buffering a request or receives a new request from a keepalive connection.
270
+ self.reactor_wakeup(c)
271
+ }
272
+ @reactor.run
153
273
  end
154
274
 
155
- @reactor = Reactor.new self, @thread_pool
275
+ @thread_pool.auto_reap! if options[:reaping_time]
276
+ @thread_pool.auto_trim! if @min_threads != @max_threads && options[:auto_trim_time]
156
277
 
157
- @reactor.run_in_thread
278
+ @check, @notify = Puma::Util.pipe unless @notify
158
279
 
159
- if @auto_trim_time
160
- @thread_pool.auto_trim!(@auto_trim_time)
161
- end
280
+ @events.fire :state, :running
162
281
 
163
282
  if background
164
- @thread = Thread.new { handle_servers }
283
+ @thread = Thread.new do
284
+ Puma.set_thread_name thread_name
285
+ handle_servers
286
+ end
165
287
  return @thread
166
288
  else
167
289
  handle_servers
168
290
  end
169
291
  end
170
292
 
293
+ # This method is called from the Reactor thread when a queued Client receives data,
294
+ # times out, or when the Reactor is shutting down.
295
+ #
296
+ # While the code lives in the Server, the logic is executed on the reactor thread, independently
297
+ # from the server.
298
+ #
299
+ # It is responsible for ensuring that a request has been completely received
300
+ # before it starts to be processed by the ThreadPool. This may be known as read buffering.
301
+ # If read buffering is not done, and no other read buffering is performed (such as by an application server
302
+ # such as nginx) then the application would be subject to a slow client attack.
303
+ #
304
+ # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/main/docs/architecture.md).
305
+ #
306
+ # The method checks to see if it has the full header and body with
307
+ # the `Puma::Client#try_to_finish` method. If the full request has been sent,
308
+ # then the request is passed to the ThreadPool (`@thread_pool << client`)
309
+ # so that a "processor thread" can pick up the request and begin to execute application logic.
310
+ # The Client is then removed from the reactor (return `true`).
311
+ #
312
+ # If a client object times out, a 408 response is written, its connection is closed,
313
+ # and the object is removed from the reactor (return `true`).
314
+ #
315
+ # If the Reactor is shutting down, all Clients are either timed out or passed to the
316
+ # ThreadPool, depending on their current state (#can_close?).
317
+ #
318
+ # Otherwise, if the full request is not ready then the client will remain in the reactor
319
+ # (return `false`). When the client sends more data to the socket the `Puma::Client` object
320
+ # will wake up and again be checked to see if it's ready to be passed to the thread pool.
321
+ def reactor_wakeup(client)
322
+ shutdown = !@queue_requests
323
+ if client.try_to_finish || (shutdown && !client.can_close?)
324
+ @thread_pool << client
325
+ elsif shutdown || client.timeout == 0
326
+ client.timeout!
327
+ else
328
+ client.set_timeout(@first_data_timeout)
329
+ false
330
+ end
331
+ rescue StandardError => e
332
+ client_error(e, client)
333
+ close_client_safely(client)
334
+ true
335
+ end
336
+
171
337
  def handle_servers
338
+ @env_set_http_version = Object.const_defined?(:Rack) && ::Rack.respond_to?(:release) &&
339
+ Gem::Version.new(::Rack.release) < Gem::Version.new('3.1.0')
340
+
172
341
  begin
173
342
  check = @check
174
343
  sockets = [check] + @binder.ios
175
344
  pool = @thread_pool
345
+ queue_requests = @queue_requests
346
+ drain = options[:drain_on_shutdown] ? 0 : nil
347
+
348
+ addr_send_name, addr_value = case options[:remote_address]
349
+ when :value
350
+ [:peerip=, options[:remote_address_value]]
351
+ when :header
352
+ [:remote_addr_header=, options[:remote_address_header]]
353
+ when :proxy_protocol
354
+ [:expect_proxy_proto=, options[:remote_address_proxy_protocol]]
355
+ else
356
+ [nil, nil]
357
+ end
176
358
 
177
- while @status == :run
359
+ while @status == :run || (drain && shutting_down?)
178
360
  begin
179
- ios = IO.select sockets
361
+ ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : @idle_timeout)
362
+ unless ios
363
+ unless shutting_down?
364
+ @idle_timeout_reached = true
365
+
366
+ if @clustered
367
+ @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
368
+ next
369
+ else
370
+ @log_writer.log "- Idle timeout reached"
371
+ @status = :stop
372
+ end
373
+ end
374
+
375
+ break
376
+ end
377
+
378
+ if @idle_timeout_reached && @clustered
379
+ @idle_timeout_reached = false
380
+ @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
381
+ end
382
+
180
383
  ios.first.each do |sock|
181
384
  if sock == check
182
385
  break if handle_check
183
386
  else
184
- begin
185
- if io = sock.accept_nonblock
186
- c = Client.new io, @binder.env(sock)
187
- pool << c
188
- end
189
- rescue SystemCallError
387
+ # if ThreadPool out_of_band code is running, we don't want to add
388
+ # clients until the code is finished.
389
+ pool.wait_while_out_of_band_running
390
+
391
+ # A well rested herd (cluster) runs faster
392
+ if @cluster_accept_loop_delay.on? && (busy_threads_plus_todo = pool.busy_threads) > 0
393
+ delay = @cluster_accept_loop_delay.calculate(
394
+ max_threads: @max_threads,
395
+ busy_threads_plus_todo: busy_threads_plus_todo
396
+ )
397
+ sleep(delay)
398
+ end
399
+
400
+ io = begin
401
+ sock.accept_nonblock
402
+ rescue IO::WaitReadable
403
+ next
190
404
  end
405
+ drain += 1 if shutting_down?
406
+
407
+ client = new_client(io, sock)
408
+ client.send(addr_send_name, addr_value) if addr_value
409
+ pool << client
191
410
  end
192
411
  end
193
- rescue Errno::ECONNABORTED
194
- # client closed the socket even before accept
195
- client.close rescue nil
196
- rescue Object => e
197
- @events.unknown_error self, e, "Listen loop"
412
+ rescue IOError, Errno::EBADF
413
+ # In the case that any of the sockets are unexpectedly close.
414
+ raise
415
+ rescue StandardError => e
416
+ @log_writer.unknown_error e, nil, "Listen loop"
198
417
  end
199
418
  end
200
419
 
201
- graceful_shutdown if @status == :stop || @status == :restart
202
- @reactor.clear! if @status == :restart
420
+ @log_writer.debug { "Drained #{drain} additional connections." } if drain
421
+ @events.fire :state, @status
422
+
423
+ if queue_requests
424
+ @queue_requests = false
425
+ @reactor.shutdown
426
+ end
203
427
 
204
- @reactor.shutdown
428
+ graceful_shutdown if @status == :stop || @status == :restart
205
429
  rescue Exception => e
206
- STDERR.puts "Exception handling servers: #{e.message} (#{e.class})"
207
- STDERR.puts e.backtrace
430
+ @log_writer.unknown_error e, nil, "Exception handling servers"
208
431
  ensure
209
- @check.close
210
- @notify.close
211
-
212
- if @status != :restart and @own_binder
213
- @binder.close
432
+ # Errno::EBADF is infrequently raised
433
+ [@check, @notify].each do |io|
434
+ begin
435
+ io.close unless io.closed?
436
+ rescue Errno::EBADF
437
+ end
214
438
  end
439
+ @notify = nil
440
+ @check = nil
215
441
  end
442
+
443
+ @events.fire :state, :done
444
+ end
445
+
446
+ # :nodoc:
447
+ def new_client(io, sock)
448
+ client = Client.new(io, @binder.env(sock))
449
+ client.listener = sock
450
+ client.env_set_http_version = @env_set_http_version
451
+ client.http_content_length_limit = @http_content_length_limit
452
+ client.supported_http_methods = @supported_http_methods
453
+ client
216
454
  end
217
455
 
218
456
  # :nodoc:
219
457
  def handle_check
220
- cmd = @check.read(1)
458
+ cmd = @check.read(1)
221
459
 
222
460
  case cmd
223
461
  when STOP_COMMAND
@@ -231,406 +469,305 @@ module Puma
231
469
  return true
232
470
  end
233
471
 
234
- return false
472
+ false
235
473
  end
236
474
 
237
- # Given a connection on +client+, handle the incoming requests.
475
+ # Given a connection on +client+, handle the incoming requests,
476
+ # or queue the connection in the Reactor if no request is available.
477
+ #
478
+ # This method is called from a ThreadPool processor thread.
238
479
  #
239
- # This method support HTTP Keep-Alive so it may, depending on if the client
480
+ # This method supports HTTP Keep-Alive so it may, depending on if the client
240
481
  # indicates that it supports keep alive, wait for another request before
241
482
  # returning.
242
483
  #
243
- def process_client(client, buffer)
484
+ # Return true if one or more requests were processed.
485
+ def process_client(processor, client)
486
+ close_socket = true
487
+
488
+ requests = 0
489
+
244
490
  begin
245
- close_socket = true
491
+ if @queue_requests && !client.eagerly_finish
246
492
 
247
- while true
248
- case handle_request(client, buffer)
249
- when false
250
- return
251
- when :async
493
+ client.set_timeout(@first_data_timeout)
494
+ if @reactor.add client
252
495
  close_socket = false
253
- return
254
- when true
255
- buffer.reset
256
-
257
- unless client.reset(@status == :run)
258
- close_socket = false
259
- client.set_timeout @persistent_timeout
260
- @reactor.add client
261
- return
262
- end
496
+ return false
263
497
  end
264
498
  end
265
499
 
266
- # The client disconnected while we were reading data
267
- rescue ConnectionError
268
- # Swallow them. The ensure tries to close +client+ down
269
-
270
- # The client doesn't know HTTP well
271
- rescue HttpParserError => e
272
- client.write_400
500
+ with_force_shutdown(client) do
501
+ client.finish(@first_data_timeout)
502
+ end
273
503
 
274
- @events.parse_error self, client.env, e
504
+ can_loop = true
505
+ while can_loop
506
+ can_loop = false
507
+ @requests_count += 1
508
+ case handle_request(processor, client, requests + 1)
509
+ when :close
510
+ when :async
511
+ close_socket = false
512
+ when :keep_alive
513
+ requests += 1
275
514
 
276
- # Server error
277
- rescue StandardError => e
278
- client.write_500
515
+ client.reset
279
516
 
280
- @events.unknown_error self, e, "Read"
517
+ # This indicates data exists in the client read buffer and there may be
518
+ # additional requests on it, so process them
519
+ next_request_ready = if client.has_back_to_back_requests?
520
+ with_force_shutdown(client) { client.process_back_to_back_requests }
521
+ else
522
+ with_force_shutdown(client) { client.eagerly_finish }
523
+ end
281
524
 
525
+ if next_request_ready
526
+ # When Puma has spare threads, allow this one to be monopolized
527
+ # Perf optimization for https://github.com/puma/puma/issues/3788
528
+ if @thread_pool.waiting > 0
529
+ can_loop = true
530
+ else
531
+ @thread_pool << client
532
+ close_socket = false
533
+ end
534
+ elsif @queue_requests
535
+ client.set_timeout @persistent_timeout
536
+ if @reactor.add client
537
+ close_socket = false
538
+ end
539
+ end
540
+ end
541
+ end
542
+ true
543
+ rescue StandardError => e
544
+ client_error(e, client, requests)
545
+ # The ensure tries to close +client+ down
546
+ requests > 0
282
547
  ensure
283
- buffer.reset
548
+ client.io_buffer.reset
284
549
 
285
- begin
286
- client.close if close_socket
287
- rescue IOError, SystemCallError
288
- # Already closed
289
- rescue StandardError => e
290
- @events.unknown_error self, e, "Client"
291
- end
550
+ close_client_safely(client) if close_socket
292
551
  end
293
552
  end
294
553
 
295
- # Given a Hash +env+ for the request read from +client+, add
296
- # and fixup keys to comply with Rack's env guidelines.
297
- #
298
- def normalize_env(env, client)
299
- if host = env[HTTP_HOST]
300
- if colon = host.index(":")
301
- env[SERVER_NAME] = host[0, colon]
302
- env[SERVER_PORT] = host[colon+1, host.bytesize]
303
- else
304
- env[SERVER_NAME] = host
305
- env[SERVER_PORT] = default_server_port(env)
306
- end
307
- else
308
- env[SERVER_NAME] = LOCALHOST
309
- env[SERVER_PORT] = default_server_port(env)
310
- end
311
-
312
- unless env[REQUEST_PATH]
313
- # it might be a dumbass full host request header
314
- uri = URI.parse(env[REQUEST_URI])
315
- env[REQUEST_PATH] = uri.path
316
-
317
- raise "No REQUEST PATH" unless env[REQUEST_PATH]
318
- end
319
-
320
- env[PATH_INFO] = env[REQUEST_PATH]
554
+ # :nodoc:
555
+ def close_client_safely(client)
556
+ client.close
557
+ rescue IOError, SystemCallError
558
+ # Already closed
559
+ rescue MiniSSL::SSLError => e
560
+ @log_writer.ssl_error e, client.io
561
+ rescue StandardError => e
562
+ @log_writer.unknown_error e, nil, "Client"
563
+ end
321
564
 
322
- # From http://www.ietf.org/rfc/rfc3875 :
323
- # "Script authors should be aware that the REMOTE_ADDR and
324
- # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
325
- # may not identify the ultimate source of the request.
326
- # They identify the client for the immediate request to the
327
- # server; that client may be a proxy, gateway, or other
328
- # intermediary acting on behalf of the actual source client."
329
- #
330
- env[REMOTE_ADDR] = client.peeraddr.last
565
+ # Triggers a client timeout if the thread-pool shuts down
566
+ # during execution of the provided block.
567
+ def with_force_shutdown(client, &block)
568
+ @thread_pool.with_force_shutdown(&block)
569
+ rescue ThreadPool::ForceShutdown
570
+ client.timeout!
331
571
  end
332
572
 
333
- def default_server_port(env)
334
- env['HTTP_X_FORWARDED_PROTO'] == 'https' ? PORT_443 : PORT_80
573
+ # :nocov:
574
+
575
+ # Handle various error types thrown by Client I/O operations.
576
+ def client_error(e, client, requests = 1)
577
+ # Swallow, do not log
578
+ return if [ConnectionError, EOFError].include?(e.class)
579
+
580
+ case e
581
+ when MiniSSL::SSLError
582
+ lowlevel_error(e, client.env)
583
+ @log_writer.ssl_error e, client.io
584
+ when HttpParserError
585
+ response_to_error(client, requests, e, client.error_status_code || 400)
586
+ @log_writer.parse_error e, client
587
+ when HttpParserError501
588
+ response_to_error(client, requests, e, 501)
589
+ @log_writer.parse_error e, client
590
+ else
591
+ response_to_error(client, requests, e, 500)
592
+ @log_writer.unknown_error e, nil, "Read"
593
+ end
335
594
  end
336
595
 
337
- # Given the request +env+ from +client+ and a partial request body
338
- # in +body+, finish reading the body if there is one and invoke
339
- # the rack app. Then construct the response and write it back to
340
- # +client+
341
- #
342
- # +cl+ is the previously fetched Content-Length header if there
343
- # was one. This is an optimization to keep from having to look
344
- # it up again.
596
+ # A fallback rack response if +@app+ raises as exception.
345
597
  #
346
- def handle_request(req, lines)
347
- env = req.env
348
- client = req.io
349
-
350
- normalize_env env, client
351
-
352
- env[PUMA_SOCKET] = client
353
-
354
- env[HIJACK_P] = true
355
- env[HIJACK] = req
356
-
357
- body = req.body
358
-
359
- head = env[REQUEST_METHOD] == HEAD
360
-
361
- env[RACK_INPUT] = body
362
- env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP
363
-
364
- # A rack extension. If the app writes #call'ables to this
365
- # array, we will invoke them when the request is done.
366
- #
367
- after_reply = env[RACK_AFTER_REPLY] = []
368
-
369
- begin
370
- begin
371
- status, headers, res_body = @app.call(env)
372
-
373
- return :async if req.hijacked
374
-
375
- status = status.to_i
376
-
377
- if status == -1
378
- unless headers.empty? and res_body == []
379
- raise "async response must have empty headers and body"
380
- end
381
-
382
- return :async
383
- end
384
- rescue StandardError => e
385
- @events.unknown_error self, e, "Rack app"
386
-
387
- status, headers, res_body = lowlevel_error(e)
388
- end
389
-
390
- content_length = nil
391
- no_body = head
392
-
393
- if res_body.kind_of? Array and res_body.size == 1
394
- content_length = res_body[0].bytesize
395
- end
396
-
397
- cork_socket client
398
-
399
- line_ending = LINE_END
400
- colon = COLON
401
-
402
- if env[HTTP_VERSION] == HTTP_11
403
- allow_chunked = true
404
- keep_alive = env[HTTP_CONNECTION] != CLOSE
405
- include_keepalive_header = false
406
-
407
- # An optimization. The most common response is 200, so we can
408
- # reply with the proper 200 status without having to compute
409
- # the response header.
410
- #
411
- if status == 200
412
- lines << HTTP_11_200
413
- else
414
- lines.append "HTTP/1.1 ", status.to_s, " ",
415
- HTTP_STATUS_CODES[status], line_ending
416
-
417
- no_body ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
418
- end
598
+ def lowlevel_error(e, env, status=500)
599
+ if handler = options[:lowlevel_error_handler]
600
+ if handler.arity == 1
601
+ return handler.call(e)
602
+ elsif handler.arity == 2
603
+ return handler.call(e, env)
419
604
  else
420
- allow_chunked = false
421
- keep_alive = env[HTTP_CONNECTION] == KEEP_ALIVE
422
- include_keepalive_header = keep_alive
423
-
424
- # Same optimization as above for HTTP/1.1
425
- #
426
- if status == 200
427
- lines << HTTP_10_200
428
- else
429
- lines.append "HTTP/1.0 ", status.to_s, " ",
430
- HTTP_STATUS_CODES[status], line_ending
431
-
432
- no_body ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
433
- end
434
- end
435
-
436
- response_hijack = nil
437
-
438
- headers.each do |k, vs|
439
- case k
440
- when CONTENT_LENGTH2
441
- content_length = vs
442
- next
443
- when TRANSFER_ENCODING
444
- allow_chunked = false
445
- content_length = nil
446
- when CONTENT_TYPE
447
- next if no_body
448
- when HIJACK
449
- response_hijack = vs
450
- next
451
- end
452
-
453
- vs.split(NEWLINE).each do |v|
454
- lines.append k, colon, v, line_ending
455
- end
456
- end
457
-
458
- if no_body
459
- lines << line_ending
460
- fast_write client, lines.to_s
461
- return keep_alive
462
- end
463
-
464
- if include_keepalive_header
465
- lines << CONNECTION_KEEP_ALIVE
466
- elsif !keep_alive
467
- lines << CONNECTION_CLOSE
605
+ return handler.call(e, env, status)
468
606
  end
607
+ end
469
608
 
470
- unless response_hijack
471
- if content_length
472
- lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
473
- chunked = false
474
- elsif allow_chunked
475
- lines << TRANSFER_ENCODING_CHUNKED
476
- chunked = true
477
- end
478
- end
479
-
480
- lines << line_ending
481
-
482
- fast_write client, lines.to_s
483
-
484
- if response_hijack
485
- response_hijack.call client
486
- return :async
487
- end
488
-
489
- res_body.each do |part|
490
- if chunked
491
- client.syswrite part.bytesize.to_s(16)
492
- client.syswrite line_ending
493
- fast_write client, part
494
- client.syswrite line_ending
495
- else
496
- fast_write client, part
497
- end
498
-
499
- client.flush
500
- end
501
-
502
- if chunked
503
- client.syswrite CLOSE_CHUNKED
504
- client.flush
505
- end
506
-
507
- ensure
508
- uncork_socket client
509
-
510
- body.close
511
- res_body.close if res_body.respond_to? :close
512
-
513
- after_reply.each { |o| o.call }
609
+ if @leak_stack_on_error
610
+ backtrace = e.backtrace.nil? ? '<no backtrace available>' : e.backtrace.join("\n")
611
+ [status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{backtrace}"]]
612
+ else
613
+ [status, {}, [""]]
514
614
  end
615
+ end
515
616
 
516
- return keep_alive
617
+ def response_to_error(client, requests, err, status_code)
618
+ # @todo remove sometime later
619
+ if status_code == 413
620
+ status = 413
621
+ res_body = ["Payload Too Large"]
622
+ headers = {}
623
+ headers[CONTENT_LENGTH2] = 17
624
+ else
625
+ status, headers, res_body = lowlevel_error(err, client.env, status_code)
626
+ end
627
+ prepare_response(status, headers, res_body, requests, client)
517
628
  end
629
+ private :response_to_error
518
630
 
519
- # Given the requset +env+ from +client+ and the partial body +body+
520
- # plus a potential Content-Length value +cl+, finish reading
521
- # the body and return it.
522
- #
523
- # If the body is larger than MAX_BODY, a Tempfile object is used
524
- # for the body, otherwise a StringIO is used.
631
+ # Wait for all outstanding requests to finish.
525
632
  #
526
- def read_body(env, client, body, cl)
527
- content_length = cl.to_i
528
-
529
- remain = content_length - body.bytesize
633
+ def graceful_shutdown
634
+ if @status != :restart
635
+ @binder.close
636
+ end
530
637
 
531
- return StringIO.new(body) if remain <= 0
638
+ @thread_pool.shutdown(options[:force_shutdown_after])
639
+ end
532
640
 
533
- # Use a Tempfile if there is a lot of data left
534
- if remain > MAX_BODY
535
- stream = Tempfile.new(Const::PUMA_TMP_BASE)
536
- stream.binmode
641
+ def notify_safely(message)
642
+ @notify << message
643
+ rescue IOError, NoMethodError, Errno::EPIPE, Errno::EBADF
644
+ # The server, in another thread, is shutting down
645
+ rescue RuntimeError => e
646
+ # Temporary workaround for https://bugs.ruby-lang.org/issues/13239
647
+ if e.message.include?('IOError')
648
+ # ignore
537
649
  else
538
- # The body[0,0] trick is to get an empty string in the same
539
- # encoding as body.
540
- stream = StringIO.new body[0,0]
650
+ raise e
541
651
  end
652
+ end
653
+ private :notify_safely
542
654
 
543
- stream.write body
655
+ # Stops the acceptor thread and then causes the processor threads to finish
656
+ # off the request queue before finally exiting.
544
657
 
545
- # Read an odd sized chunk so we can read even sized ones
546
- # after this
547
- chunk = client.readpartial(remain % CHUNK_SIZE)
658
+ def stop(sync=false)
659
+ notify_safely(STOP_COMMAND)
660
+ @thread.join if @thread && sync
661
+ end
548
662
 
549
- # No chunk means a closed socket
550
- unless chunk
551
- stream.close
552
- return nil
553
- end
663
+ def halt(sync=false)
664
+ notify_safely(HALT_COMMAND)
665
+ @thread.join if @thread && sync
666
+ end
554
667
 
555
- remain -= stream.write(chunk)
668
+ def begin_restart(sync=false)
669
+ notify_safely(RESTART_COMMAND)
670
+ @thread.join if @thread && sync
671
+ end
556
672
 
557
- # Raed the rest of the chunks
558
- while remain > 0
559
- chunk = client.readpartial(CHUNK_SIZE)
560
- unless chunk
561
- stream.close
562
- return nil
563
- end
673
+ def shutting_down?
674
+ @status == :stop || @status == :restart
675
+ end
564
676
 
565
- remain -= stream.write(chunk)
566
- end
677
+ # List of methods invoked by #stats.
678
+ # @version 5.0.0
679
+ STAT_METHODS = [
680
+ :backlog,
681
+ :running,
682
+ :pool_capacity,
683
+ :busy_threads,
684
+ :backlog_max,
685
+ :max_threads,
686
+ :requests_count,
687
+ :reactor_max,
688
+ ].freeze
689
+
690
+ # Returns a hash of stats about the running server for reporting purposes.
691
+ # @version 5.0.0
692
+ # @!attribute [r] stats
693
+ # @return [Hash] hash containing stat info from `Server` and `ThreadPool`
694
+ def stats
695
+ stats = @thread_pool&.stats || {}
696
+ stats[:max_threads] = @max_threads
697
+ stats[:requests_count] = @requests_count
698
+ stats[:reactor_max] = @reactor.reactor_max if @reactor
699
+ reset_max
700
+ stats
701
+ end
702
+
703
+ def reset_max
704
+ @reactor.reactor_max = 0 if @reactor
705
+ @thread_pool&.reset_max
706
+ end
707
+
708
+ # below are 'delegations' to binder
709
+ # remove in Puma 7?
567
710
 
568
- stream.rewind
569
711
 
570
- return stream
712
+ def add_tcp_listener(host, port, optimize_for_latency = true, backlog = 1024)
713
+ @binder.add_tcp_listener host, port, optimize_for_latency, backlog
571
714
  end
572
715
 
573
- # A fallback rack response if +@app+ raises as exception.
574
- #
575
- def lowlevel_error(e)
576
- if @leak_stack_on_error
577
- [500, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{e.backtrace.join("\n")}"]]
578
- else
579
- [500, {}, ["A really lowlevel plumbing error occured. Please contact your local Maytag(tm) repair man.\n"]]
580
- end
716
+ def add_ssl_listener(host, port, ctx, optimize_for_latency = true,
717
+ backlog = 1024)
718
+ @binder.add_ssl_listener host, port, ctx, optimize_for_latency, backlog
581
719
  end
582
720
 
583
- # Wait for all outstanding requests to finish.
584
- #
585
- def graceful_shutdown
586
- @thread_pool.shutdown if @thread_pool
721
+ def add_unix_listener(path, umask = nil, mode = nil, backlog = 1024)
722
+ @binder.add_unix_listener path, umask, mode, backlog
587
723
  end
588
724
 
589
- # Stops the acceptor thread and then causes the worker threads to finish
590
- # off the request queue before finally exiting.
725
+ # Updates the minimum and maximum number of threads in the thread pool.
591
726
  #
592
- def stop(sync=false)
593
- begin
594
- @notify << STOP_COMMAND
595
- rescue IOError
596
- # The server, in another thread, is shutting down
727
+ # This method allows dynamic adjustment of the thread pool size while the server
728
+ # is running. It validates the provided values and updates both the thread pool
729
+ # and the server's thread configuration.
730
+ #
731
+ # @param min [Integer] The minimum number of threads to maintain in the pool.
732
+ # Defaults to the current minimum if not specified. Must be greater than 0
733
+ # and less than or equal to max.
734
+ # @param max [Integer] The maximum number of threads allowed in the pool.
735
+ # Defaults to the current maximum if not specified. Must be greater than or
736
+ # equal to min.
737
+ #
738
+ # @return [void]
739
+ #
740
+ # @note If validation fails, a warning message is logged and no changes are made.
741
+ #
742
+ # @example Update both min and max threads
743
+ # server.update_thread_pool_min_max(min: 2, max: 8)
744
+ #
745
+ # @example Update only the minimum threads
746
+ # server.update_thread_pool_min_max(min: 4)
747
+ #
748
+ # @example Update only the maximum threads
749
+ # server.update_thread_pool_min_max(max: 16)
750
+ #
751
+ def update_thread_pool_min_max(min: @min_threads, max: @max_threads)
752
+ if min > max
753
+ @log_writer.log "`min' value cannot be greater than `max' value."
754
+ return
597
755
  end
598
756
 
599
- @thread.join if @thread && sync
600
- end
601
-
602
- def halt(sync=false)
603
- begin
604
- @notify << HALT_COMMAND
605
- rescue IOError
606
- # The server, in another thread, is shutting down
757
+ if min < 0
758
+ @log_writer.log "`min' value cannot be less than 0"
759
+ return
607
760
  end
608
761
 
609
- @thread.join if @thread && sync
610
- end
611
-
612
- def begin_restart
613
- begin
614
- @notify << RESTART_COMMAND
615
- rescue IOError
616
- # The server, in another thread, is shutting down
762
+ @thread_pool&.with_mutex do
763
+ @thread_pool.min, @thread_pool.max = min, max
764
+ @min_threads, @max_threads = min, max
617
765
  end
618
766
  end
619
767
 
620
- def fast_write(io, str)
621
- n = 0
622
- while true
623
- begin
624
- n = io.syswrite str
625
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
626
- IO.select(nil, [io], nil, 1)
627
- retry
628
- end
629
-
630
- return if n == str.bytesize
631
- str = str.byteslice(n..-1)
632
- end
768
+ # @!attribute [r] connected_ports
769
+ def connected_ports
770
+ @binder.connected_ports
633
771
  end
634
- private :fast_write
635
772
  end
636
773
  end