puma 3.9.0 → 3.12.0

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 (53) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +98 -0
  3. data/README.md +140 -230
  4. data/docs/architecture.md +36 -0
  5. data/{DEPLOYMENT.md → docs/deployment.md} +0 -0
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +28 -0
  10. data/docs/restart.md +39 -0
  11. data/docs/signals.md +56 -3
  12. data/docs/systemd.md +112 -37
  13. data/ext/puma_http11/http11_parser.c +84 -84
  14. data/ext/puma_http11/http11_parser.rl +9 -9
  15. data/ext/puma_http11/mini_ssl.c +18 -4
  16. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +13 -16
  17. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +6 -0
  18. data/lib/puma.rb +8 -0
  19. data/lib/puma/app/status.rb +8 -0
  20. data/lib/puma/binder.rb +12 -8
  21. data/lib/puma/cli.rb +20 -7
  22. data/lib/puma/client.rb +28 -0
  23. data/lib/puma/cluster.rb +26 -7
  24. data/lib/puma/configuration.rb +19 -14
  25. data/lib/puma/const.rb +7 -2
  26. data/lib/puma/control_cli.rb +5 -5
  27. data/lib/puma/dsl.rb +34 -7
  28. data/lib/puma/jruby_restart.rb +0 -1
  29. data/lib/puma/launcher.rb +36 -19
  30. data/lib/puma/minissl.rb +49 -27
  31. data/lib/puma/plugin/tmp_restart.rb +0 -1
  32. data/lib/puma/reactor.rb +135 -0
  33. data/lib/puma/runner.rb +12 -1
  34. data/lib/puma/server.rb +84 -25
  35. data/lib/puma/single.rb +12 -3
  36. data/lib/puma/thread_pool.rb +47 -8
  37. data/lib/rack/handler/puma.rb +4 -1
  38. data/tools/jungle/README.md +12 -2
  39. data/tools/jungle/init.d/README.md +2 -0
  40. data/tools/jungle/init.d/puma +2 -2
  41. data/tools/jungle/init.d/run-puma +1 -1
  42. data/tools/jungle/rc.d/README.md +74 -0
  43. data/tools/jungle/rc.d/puma +61 -0
  44. data/tools/jungle/rc.d/puma.conf +10 -0
  45. data/tools/trickletest.rb +1 -1
  46. metadata +21 -94
  47. data/.github/issue_template.md +0 -20
  48. data/Gemfile +0 -12
  49. data/Manifest.txt +0 -78
  50. data/Rakefile +0 -158
  51. data/Release.md +0 -9
  52. data/gemfiles/2.1-Gemfile +0 -12
  53. data/puma.gemspec +0 -52
@@ -1,3 +1,8 @@
1
+ begin
2
+ require 'io/wait'
3
+ rescue LoadError
4
+ end
5
+
1
6
  module Puma
2
7
  module MiniSSL
3
8
  class Socket
@@ -11,6 +16,10 @@ module Puma
11
16
  @socket
12
17
  end
13
18
 
19
+ def closed?
20
+ @socket.closed?
21
+ end
22
+
14
23
  def readpartial(size)
15
24
  while true
16
25
  output = @engine.read
@@ -43,7 +52,22 @@ module Puma
43
52
  output = engine_read_all
44
53
  return output if output
45
54
 
46
- data = @socket.read_nonblock(size)
55
+ begin
56
+ data = @socket.read_nonblock(size, exception: false)
57
+ if data == :wait_readable || data == :wait_writable
58
+ if @socket.to_io.respond_to?(data)
59
+ @socket.to_io.__send__(data)
60
+ elsif data == :wait_readable
61
+ IO.select([@socket.to_io])
62
+ else
63
+ IO.select(nil, [@socket.to_io])
64
+ end
65
+ elsif !data
66
+ return nil
67
+ else
68
+ break
69
+ end
70
+ end while true
47
71
 
48
72
  @engine.inject(data)
49
73
  output = engine_read_all
@@ -57,6 +81,8 @@ module Puma
57
81
  end
58
82
 
59
83
  def write(data)
84
+ return 0 if data.empty?
85
+
60
86
  need = data.bytesize
61
87
 
62
88
  while true
@@ -96,35 +122,29 @@ module Puma
96
122
  @socket.flush
97
123
  end
98
124
 
99
- def close
100
- begin
101
- # Try to setup (so that we can then close them) any
102
- # partially initialized sockets.
103
- while @engine.init?
104
- # Don't let this socket hold this loop forever.
105
- # If it can't send more packets within 1s, then
106
- # give up.
107
- return unless IO.select([@socket], nil, nil, 1)
108
- begin
109
- read_nonblock(1024)
110
- rescue Errno::EAGAIN
111
- end
112
- end
113
-
114
- done = @engine.shutdown
115
-
116
- while true
117
- enc = @engine.extract
118
- @socket.write enc
119
-
120
- notify = @socket.sysread(1024)
125
+ def read_and_drop(timeout = 1)
126
+ return :timeout unless IO.select([@socket], nil, nil, timeout)
127
+ return :eof unless read_nonblock(1024)
128
+ :drop
129
+ rescue Errno::EAGAIN
130
+ # do nothing
131
+ :eagain
132
+ end
121
133
 
122
- @engine.inject notify
123
- done = @engine.shutdown
134
+ def should_drop_bytes?
135
+ @engine.init? || !@engine.shutdown
136
+ end
124
137
 
125
- break if done
138
+ def close
139
+ begin
140
+ # Read any drop any partially initialized sockets and any received bytes during shutdown.
141
+ # Don't let this socket hold this loop forever.
142
+ # If it can't send more packets within 1s, then give up.
143
+ while should_drop_bytes?
144
+ return if [:timeout, :eof].include?(read_and_drop(1))
126
145
  end
127
146
  rescue IOError, SystemCallError
147
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
128
148
  # nothing
129
149
  ensure
130
150
  @socket.close
@@ -160,6 +180,7 @@ module Puma
160
180
  # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
161
181
  attr_reader :keystore
162
182
  attr_accessor :keystore_pass
183
+ attr_accessor :ssl_cipher_list
163
184
 
164
185
  def keystore=(keystore)
165
186
  raise ArgumentError, "No such keystore file '#{keystore}'" unless File.exist? keystore
@@ -175,6 +196,7 @@ module Puma
175
196
  attr_reader :key
176
197
  attr_reader :cert
177
198
  attr_reader :ca
199
+ attr_accessor :ssl_cipher_filter
178
200
 
179
201
  def key=(key)
180
202
  raise ArgumentError, "No such key file '#{key}'" unless File.exist? key
@@ -229,7 +251,7 @@ module Puma
229
251
  end
230
252
 
231
253
  def close
232
- @socket.close
254
+ @socket.close unless @socket.closed? # closed? call is for Windows
233
255
  end
234
256
  end
235
257
  end
@@ -32,4 +32,3 @@ Puma::Plugin.create do
32
32
  end
33
33
  end
34
34
  end
35
-
@@ -2,15 +2,54 @@ require 'puma/util'
2
2
  require 'puma/minissl'
3
3
 
4
4
  module Puma
5
+ # Internal Docs, Not a public interface.
6
+ #
7
+ # The Reactor object is responsible for ensuring that a request has been
8
+ # completely received before it starts to be processed. This may be known as read buffering.
9
+ # If read buffering is not done, and no other read buffering is performed (such as by an application server
10
+ # such as nginx) then the application would be subject to a slow client attack.
11
+ #
12
+ # Each Puma "worker" process has its own Reactor. For example if you start puma with `$ puma -w 5` then
13
+ # it will have 5 workers and each worker will have it's own reactor.
14
+ #
15
+ # For a graphical representation of how the reactor works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline).
16
+ #
17
+ # ## Reactor Flow
18
+ #
19
+ # A request comes into a `Puma::Server` instance, it is then passed to a `Puma::Reactor` instance.
20
+ # The reactor stores the request in an array and calls `IO.select` on the array in a loop.
21
+ #
22
+ # When the request is written to by the client then the `IO.select` will "wake up" and
23
+ # return the references to any objects that caused it to "wake". The reactor
24
+ # then loops through each of these request objects, and sees if they're complete. If they
25
+ # have a full header and body then the reactor passes the request to a thread pool.
26
+ # Once in a thread pool, a "worker thread" can run the the application's Ruby code against the request.
27
+ #
28
+ # If the request is not complete, then it stays in the array, and the next time any
29
+ # data is written to that socket reference, then the loop is woken up and it is checked for completeness again.
30
+ #
31
+ # A detailed example is given in the docs for `run_internal` which is where the bulk
32
+ # of this logic lives.
5
33
  class Reactor
6
34
  DefaultSleepFor = 5
7
35
 
36
+ # Creates an instance of Puma::Reactor
37
+ #
38
+ # The `server` argument is an instance of `Puma::Server`
39
+ # this is used to write a response for "low level errors"
40
+ # when there is an exception inside of the reactor.
41
+ #
42
+ # The `app_pool` is an instance of `Puma::ThreadPool`.
43
+ # Once a request is fully formed (header and body are received)
44
+ # it will be passed to the `app_pool`.
8
45
  def initialize(server, app_pool)
9
46
  @server = server
10
47
  @events = server.events
11
48
  @app_pool = app_pool
12
49
 
13
50
  @mutex = Mutex.new
51
+
52
+ # Read / Write pipes to wake up internal while loop
14
53
  @ready, @trigger = Puma::Util.pipe
15
54
  @input = []
16
55
  @sleep_for = DefaultSleepFor
@@ -21,6 +60,64 @@ module Puma
21
60
 
22
61
  private
23
62
 
63
+
64
+ # Until a request is added via the `add` method this method will internally
65
+ # loop, waiting on the `sockets` array objects. The only object in this
66
+ # array at first is the `@ready` IO object, which is the read end of a pipe
67
+ # connected to `@trigger` object. When `@trigger` is written to, then the loop
68
+ # will break on `IO.select` and return an array.
69
+ #
70
+ # ## When a request is added:
71
+ #
72
+ # When the `add` method is called, an instance of `Puma::Client` is added to the `@input` array.
73
+ # Next the `@ready` pipe is "woken" by writing a string of `"*"` to `@trigger`.
74
+ #
75
+ # When that happens, the internal loop stops blocking at `IO.select` and returns a reference
76
+ # to whatever "woke" it up. On the very first loop, the only thing in `sockets` is `@ready`.
77
+ # When `@trigger` is written-to, the loop "wakes" and the `ready`
78
+ # variable returns an array of arrays that looks like `[[#<IO:fd 10>], [], []]` where the
79
+ # first IO object is the `@ready` object. This first array `[#<IO:fd 10>]`
80
+ # is saved as a `reads` variable.
81
+ #
82
+ # The `reads` variable is iterated through. In the case that the object
83
+ # is the same as the `@ready` input pipe, then we know that there was a `trigger` event.
84
+ #
85
+ # If there was a trigger event, then one byte of `@ready` is read into memory. In the case of the first request,
86
+ # the reactor sees that it's a `"*"` value and the reactor adds the contents of `@input` into the `sockets` array.
87
+ # The while then loop continues to iterate again, but now the `sockets` array contains a `Puma::Client` instance in addition
88
+ # to the `@ready` IO object. For example: `[#<IO:fd 10>, #<Puma::Client:0x3fdc1103bee8 @ready=false>]`.
89
+ #
90
+ # Since the `Puma::Client` in this example has data that has not been read yet,
91
+ # the `IO.select` is immediately able to "wake" and read from the `Puma::Client`. At this point the
92
+ # `ready` output looks like this: `[[#<Puma::Client:0x3fdc1103bee8 @ready=false>], [], []]`.
93
+ #
94
+ # Each element in the first entry is iterated over. The `Puma::Client` object is not
95
+ # the `@ready` pipe, so the reactor checks to see if it has the fully header and body with
96
+ # the `Puma::Client#try_to_finish` method. If the full request has been sent,
97
+ # then the request is passed off to the `@app_pool` thread pool so that a "worker thread"
98
+ # can pick up the request and begin to execute application logic. This is done
99
+ # via `@app_pool << c`. The `Puma::Client` is then removed from the `sockets` array.
100
+ #
101
+ # If the request body is not present then nothing will happen, and the loop will iterate
102
+ # again. When the client sends more data to the socket the `Puma::Client` object will
103
+ # wake up the `IO.select` and it can again be checked to see if it's ready to be
104
+ # passed to the thread pool.
105
+ #
106
+ # ## Time Out Case
107
+ #
108
+ # In addition to being woken via a write to one of the sockets the `IO.select` will
109
+ # periodically "time out" of the sleep. One of the functions of this is to check for
110
+ # any requests that have "timed out". At the end of the loop it's checked to see if
111
+ # the first element in the `@timeout` array has exceed it's allowed time. If so,
112
+ # the client object is removed from the timeout aray, a 408 response is written.
113
+ # Then it's connection is closed, and the object is removed from the `sockets` array
114
+ # that watches for new data.
115
+ #
116
+ # This behavior loops until all the objects that have timed out have been removed.
117
+ #
118
+ # Once all the timeouts have been processed, the next duration of the `IO.select` sleep
119
+ # will be set to be equal to the amount of time it will take for the next timeout to occur.
120
+ # This calculation happens in `calculate_sleep`.
24
121
  def run_internal
25
122
  sockets = @sockets
26
123
 
@@ -28,6 +125,7 @@ module Puma
28
125
  begin
29
126
  ready = IO.select sockets, nil, nil, @sleep_for
30
127
  rescue IOError => e
128
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
31
129
  if sockets.any? { |socket| socket.closed? }
32
130
  STDERR.puts "Error in select: #{e.message} (#{e.class})"
33
131
  STDERR.puts e.backtrace
@@ -162,6 +260,16 @@ module Puma
162
260
  end
163
261
  end
164
262
 
263
+ # The `calculate_sleep` sets the value that the `IO.select` will
264
+ # sleep for in the main reactor loop when no sockets are being written to.
265
+ #
266
+ # The values kept in `@timeouts` are sorted so that the first timeout
267
+ # comes first in the array. When there are no timeouts the default timeout is used.
268
+ #
269
+ # Otherwise a sleep value is set that is the same as the amount of time it
270
+ # would take for the first element to time out.
271
+ #
272
+ # If that value is in the past, then a sleep value of zero is used.
165
273
  def calculate_sleep
166
274
  if @timeouts.empty?
167
275
  @sleep_for = DefaultSleepFor
@@ -176,6 +284,31 @@ module Puma
176
284
  end
177
285
  end
178
286
 
287
+ # This method adds a connection to the reactor
288
+ #
289
+ # Typically called by `Puma::Server` the value passed in
290
+ # is usually a `Puma::Client` object that responds like an IO
291
+ # object.
292
+ #
293
+ # The main body of the reactor loop is in `run_internal` and it
294
+ # will sleep on `IO.select`. When a new connection is added to the
295
+ # reactor it cannot be added directly to the `sockets` aray, because
296
+ # the `IO.select` will not be watching for it yet.
297
+ #
298
+ # Instead what needs to happen is that `IO.select` needs to be woken up,
299
+ # the contents of `@input` added to the `sockets` array, and then
300
+ # another call to `IO.select` needs to happen. Since the `Puma::Client`
301
+ # object can be read immediately, it does not block, but instead returns
302
+ # right away.
303
+ #
304
+ # This behavior is accomplished by writing to `@trigger` which wakes up
305
+ # the `IO.select` and then there is logic to detect the value of `*`,
306
+ # pull the contents from `@input` and add them to the sockets array.
307
+ #
308
+ # If the object passed in has a timeout value in `timeout_at` then
309
+ # it is added to a `@timeouts` array. This array is then re-arranged
310
+ # so that the first element to timeout will be at the front of the
311
+ # array. Then a value to sleep for is derived in the call to `calculate_sleep`
179
312
  def add(c)
180
313
  @mutex.synchronize do
181
314
  @input << c
@@ -195,6 +328,7 @@ module Puma
195
328
  begin
196
329
  @trigger << "c"
197
330
  rescue IOError
331
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
198
332
  end
199
333
  end
200
334
 
@@ -202,6 +336,7 @@ module Puma
202
336
  begin
203
337
  @trigger << "!"
204
338
  rescue IOError
339
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
205
340
  end
206
341
 
207
342
  @thread.join
@@ -2,6 +2,9 @@ require 'puma/server'
2
2
  require 'puma/const'
3
3
 
4
4
  module Puma
5
+ # Generic class that is used by `Puma::Cluster` and `Puma::Single` to
6
+ # serve requests. This class spawns a new instance of `Puma::Server` via
7
+ # a call to `start_server`.
5
8
  class Runner
6
9
  def initialize(cli, events)
7
10
  @launcher = cli
@@ -19,6 +22,10 @@ module Puma
19
22
  @options[:environment] == "development"
20
23
  end
21
24
 
25
+ def test?
26
+ @options[:environment] == "test"
27
+ end
28
+
22
29
  def log(str)
23
30
  @events.log str
24
31
  end
@@ -161,7 +168,11 @@ module Puma
161
168
  server.tcp_mode!
162
169
  end
163
170
 
164
- unless development?
171
+ if @options[:early_hints]
172
+ server.early_hints = true
173
+ end
174
+
175
+ unless development? || test?
165
176
  server.leak_stack_on_error = false
166
177
  end
167
178
 
@@ -23,6 +23,15 @@ require 'socket'
23
23
  module Puma
24
24
 
25
25
  # The HTTP Server itself. Serves out a single Rack app.
26
+ #
27
+ # This class is used by the `Puma::Single` and `Puma::Cluster` classes
28
+ # to generate one or more `Puma::Server` instances capable of handling requests.
29
+ # Each Puma process will contain one `Puma::Server` instacne.
30
+ #
31
+ # The `Puma::Server` instance pulls requests from the socket, adds them to a
32
+ # `Puma::Reactor` where they get eventually passed to a `Puma::ThreadPool`.
33
+ #
34
+ # Each `Puma::Server` will have one reactor and one thread pool.
26
35
  class Server
27
36
 
28
37
  include Puma::Const
@@ -62,14 +71,14 @@ module Puma
62
71
 
63
72
  @thread = nil
64
73
  @thread_pool = nil
74
+ @early_hints = nil
65
75
 
66
76
  @persistent_timeout = options.fetch(:persistent_timeout, PERSISTENT_TIMEOUT)
77
+ @first_data_timeout = options.fetch(:first_data_timeout, FIRST_DATA_TIMEOUT)
67
78
 
68
79
  @binder = Binder.new(events)
69
80
  @own_binder = true
70
81
 
71
- @first_data_timeout = FIRST_DATA_TIMEOUT
72
-
73
82
  @leak_stack_on_error = true
74
83
 
75
84
  @options = options
@@ -82,7 +91,7 @@ module Puma
82
91
  @precheck_closing = true
83
92
  end
84
93
 
85
- attr_accessor :binder, :leak_stack_on_error
94
+ attr_accessor :binder, :leak_stack_on_error, :early_hints
86
95
 
87
96
  forward :add_tcp_listener, :@binder
88
97
  forward :add_ssl_listener, :@binder
@@ -111,6 +120,7 @@ module Puma
111
120
  begin
112
121
  socket.setsockopt(6, 3, 1) if socket.kind_of? TCPSocket
113
122
  rescue IOError, SystemCallError
123
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
114
124
  end
115
125
  end
116
126
 
@@ -118,6 +128,7 @@ module Puma
118
128
  begin
119
129
  socket.setsockopt(6, 3, 0) if socket.kind_of? TCPSocket
120
130
  rescue IOError, SystemCallError
131
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
121
132
  end
122
133
  end
123
134
 
@@ -128,6 +139,7 @@ module Puma
128
139
  begin
129
140
  tcp_info = socket.getsockopt(Socket::SOL_TCP, Socket::TCP_INFO)
130
141
  rescue IOError, SystemCallError
142
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
131
143
  @precheck_closing = false
132
144
  false
133
145
  else
@@ -156,6 +168,18 @@ module Puma
156
168
  @thread_pool and @thread_pool.spawned
157
169
  end
158
170
 
171
+
172
+ # This number represents the number of requests that
173
+ # the server is capable of taking right now.
174
+ #
175
+ # For example if the number is 5 then it means
176
+ # there are 5 threads sitting idle ready to take
177
+ # a request. If one request comes in, then the
178
+ # value would be 4 until it finishes processing.
179
+ def pool_capacity
180
+ @thread_pool and @thread_pool.pool_capacity
181
+ end
182
+
159
183
  # Lopez Mode == raw tcp apps
160
184
 
161
185
  def run_lopez_mode(background=true)
@@ -217,7 +241,11 @@ module Puma
217
241
  # nothing
218
242
  rescue Errno::ECONNABORTED
219
243
  # client closed the socket even before accept
220
- io.close rescue nil
244
+ begin
245
+ io.close
246
+ rescue
247
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
248
+ end
221
249
  end
222
250
  end
223
251
  end
@@ -234,7 +262,12 @@ module Puma
234
262
  STDERR.puts "Exception handling servers: #{e.message} (#{e.class})"
235
263
  STDERR.puts e.backtrace
236
264
  ensure
237
- @check.close
265
+ begin
266
+ @check.close
267
+ rescue
268
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
269
+ end
270
+
238
271
  @notify.close
239
272
 
240
273
  if @status != :restart and @own_binder
@@ -292,7 +325,7 @@ module Puma
292
325
  client.close
293
326
 
294
327
  @events.parse_error self, client.env, e
295
- rescue ConnectionError
328
+ rescue ConnectionError, EOFError
296
329
  client.close
297
330
  else
298
331
  if process_now
@@ -369,7 +402,11 @@ module Puma
369
402
  # nothing
370
403
  rescue Errno::ECONNABORTED
371
404
  # client closed the socket even before accept
372
- io.close rescue nil
405
+ begin
406
+ io.close
407
+ rescue
408
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
409
+ end
373
410
  end
374
411
  end
375
412
  end
@@ -491,6 +528,7 @@ module Puma
491
528
  begin
492
529
  client.close if close_socket
493
530
  rescue IOError, SystemCallError
531
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
494
532
  # Already closed
495
533
  rescue StandardError => e
496
534
  @events.unknown_error self, e, "Client"
@@ -592,6 +630,24 @@ module Puma
592
630
  env[RACK_INPUT] = body
593
631
  env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP
594
632
 
633
+ if @early_hints
634
+ env[EARLY_HINTS] = lambda { |headers|
635
+ fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
636
+
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"
641
+ end
642
+ else
643
+ fast_write client, "#{k}: #{vs}\r\n"
644
+ end
645
+ end
646
+
647
+ fast_write client, "\r\n".freeze
648
+ }
649
+ end
650
+
595
651
  # A rack extension. If the app writes #call'ables to this
596
652
  # array, we will invoke them when the request is done.
597
653
  #
@@ -733,8 +789,8 @@ module Puma
733
789
 
734
790
  begin
735
791
  res_body.each do |part|
792
+ next if part.bytesize.zero?
736
793
  if chunked
737
- next if part.bytesize.zero?
738
794
  fast_write client, part.bytesize.to_s(16)
739
795
  fast_write client, line_ending
740
796
  fast_write client, part
@@ -893,35 +949,38 @@ module Puma
893
949
  end
894
950
  end
895
951
 
896
- # Stops the acceptor thread and then causes the worker threads to finish
897
- # off the request queue before finally exiting.
898
- #
899
- def stop(sync=false)
952
+ def notify_safely(message)
900
953
  begin
901
- @notify << STOP_COMMAND
954
+ @notify << message
902
955
  rescue IOError
903
- # The server, in another thread, is shutting down
956
+ # The server, in another thread, is shutting down
957
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
958
+ rescue RuntimeError => e
959
+ # Temporary workaround for https://bugs.ruby-lang.org/issues/13239
960
+ if e.message.include?('IOError')
961
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
962
+ else
963
+ raise e
964
+ end
904
965
  end
966
+ end
967
+ private :notify_safely
968
+
969
+ # Stops the acceptor thread and then causes the worker threads to finish
970
+ # off the request queue before finally exiting.
905
971
 
972
+ def stop(sync=false)
973
+ notify_safely(STOP_COMMAND)
906
974
  @thread.join if @thread && sync
907
975
  end
908
976
 
909
977
  def halt(sync=false)
910
- begin
911
- @notify << HALT_COMMAND
912
- rescue IOError
913
- # The server, in another thread, is shutting down
914
- end
915
-
978
+ notify_safely(HALT_COMMAND)
916
979
  @thread.join if @thread && sync
917
980
  end
918
981
 
919
982
  def begin_restart
920
- begin
921
- @notify << RESTART_COMMAND
922
- rescue IOError
923
- # The server, in another thread, is shutting down
924
- end
983
+ notify_safely(RESTART_COMMAND)
925
984
  end
926
985
 
927
986
  def fast_write(io, str)