puma 3.8.2 → 4.0.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 (71) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +157 -0
  3. data/README.md +155 -225
  4. data/docs/architecture.md +37 -0
  5. data/{DEPLOYMENT.md → docs/deployment.md} +24 -4
  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 +41 -0
  11. data/docs/signals.md +56 -3
  12. data/docs/systemd.md +130 -37
  13. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  14. data/ext/puma_http11/http11_parser.c +84 -84
  15. data/ext/puma_http11/http11_parser.rl +9 -9
  16. data/ext/puma_http11/mini_ssl.c +51 -9
  17. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +13 -16
  18. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  19. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +26 -6
  20. data/lib/puma.rb +8 -0
  21. data/lib/puma/app/status.rb +9 -0
  22. data/lib/puma/binder.rb +31 -18
  23. data/lib/puma/cli.rb +22 -7
  24. data/lib/puma/client.rb +67 -18
  25. data/lib/puma/cluster.rb +64 -19
  26. data/lib/puma/commonlogger.rb +2 -0
  27. data/lib/puma/configuration.rb +22 -14
  28. data/lib/puma/const.rb +13 -2
  29. data/lib/puma/control_cli.rb +26 -14
  30. data/lib/puma/convenient.rb +2 -0
  31. data/lib/puma/daemon_ext.rb +2 -0
  32. data/lib/puma/delegation.rb +2 -0
  33. data/lib/puma/detect.rb +2 -0
  34. data/lib/puma/dsl.rb +91 -12
  35. data/lib/puma/events.rb +3 -2
  36. data/lib/puma/io_buffer.rb +3 -6
  37. data/lib/puma/jruby_restart.rb +2 -1
  38. data/lib/puma/launcher.rb +51 -30
  39. data/lib/puma/minissl.rb +79 -28
  40. data/lib/puma/null_io.rb +2 -0
  41. data/lib/puma/plugin.rb +2 -0
  42. data/lib/puma/plugin/tmp_restart.rb +0 -1
  43. data/lib/puma/rack/builder.rb +2 -1
  44. data/lib/puma/reactor.rb +218 -30
  45. data/lib/puma/runner.rb +17 -4
  46. data/lib/puma/server.rb +113 -49
  47. data/lib/puma/single.rb +16 -5
  48. data/lib/puma/state_file.rb +2 -0
  49. data/lib/puma/tcp_logger.rb +2 -0
  50. data/lib/puma/thread_pool.rb +59 -6
  51. data/lib/puma/util.rb +2 -6
  52. data/lib/rack/handler/puma.rb +13 -2
  53. data/tools/jungle/README.md +12 -2
  54. data/tools/jungle/init.d/README.md +2 -0
  55. data/tools/jungle/init.d/puma +7 -7
  56. data/tools/jungle/init.d/run-puma +1 -1
  57. data/tools/jungle/rc.d/README.md +74 -0
  58. data/tools/jungle/rc.d/puma +61 -0
  59. data/tools/jungle/rc.d/puma.conf +10 -0
  60. data/tools/trickletest.rb +1 -1
  61. metadata +25 -87
  62. data/.github/issue_template.md +0 -20
  63. data/Gemfile +0 -12
  64. data/Manifest.txt +0 -78
  65. data/Rakefile +0 -158
  66. data/Release.md +0 -9
  67. data/gemfiles/2.1-Gemfile +0 -12
  68. data/lib/puma/compat.rb +0 -14
  69. data/lib/puma/java_io_buffer.rb +0 -45
  70. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  71. data/puma.gemspec +0 -52
data/lib/puma.rb CHANGED
@@ -12,4 +12,12 @@ module Puma
12
12
  autoload :Const, 'puma/const'
13
13
  autoload :Server, 'puma/server'
14
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
15
23
  end
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+
1
3
  module Puma
2
4
  module App
3
5
  class Status
@@ -55,6 +57,13 @@ module Puma
55
57
  return rack_response(200, OK_STATUS)
56
58
  end
57
59
 
60
+ when /\/gc$/
61
+ GC.start
62
+ return rack_response(200, OK_STATUS)
63
+
64
+ when /\/gc-stats$/
65
+ return rack_response(200, GC.stat.to_json)
66
+
58
67
  when /\/stats$/
59
68
  return rack_response(200, @cli.stats)
60
69
  else
data/lib/puma/binder.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'socket'
3
5
 
@@ -48,7 +50,13 @@ module Puma
48
50
 
49
51
  def close
50
52
  @ios.each { |i| i.close }
51
- @unix_paths.each { |i| File.unlink i }
53
+ @unix_paths.each do |i|
54
+ # Errno::ENOENT is intermittently raised
55
+ begin
56
+ File.unlink i
57
+ rescue Errno::ENOENT
58
+ end
59
+ end
52
60
  end
53
61
 
54
62
  def import_from_env
@@ -90,19 +98,19 @@ module Puma
90
98
  case uri.scheme
91
99
  when "tcp"
92
100
  if fd = @inherited_fds.delete(str)
93
- logger.log "* Inherited #{str}"
94
101
  io = inherit_tcp_listener uri.host, uri.port, fd
102
+ logger.log "* Inherited #{str}"
95
103
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
96
- logger.log "* Activated #{str}"
97
104
  io = inherit_tcp_listener uri.host, uri.port, sock
105
+ logger.log "* Activated #{str}"
98
106
  else
99
107
  params = Util.parse_query uri.query
100
108
 
101
109
  opt = params.key?('low_latency')
102
110
  bak = params.fetch('backlog', 1024).to_i
103
111
 
104
- logger.log "* Listening on #{str}"
105
112
  io = add_tcp_listener uri.host, uri.port, opt, bak
113
+ logger.log "* Listening on #{str}"
106
114
  end
107
115
 
108
116
  @listeners << [str, io] if io
@@ -110,17 +118,15 @@ module Puma
110
118
  path = "#{uri.host}#{uri.path}".gsub("%20", " ")
111
119
 
112
120
  if fd = @inherited_fds.delete(str)
113
- logger.log "* Inherited #{str}"
114
121
  io = inherit_unix_listener path, fd
122
+ logger.log "* Inherited #{str}"
115
123
  elsif sock = @activated_sockets.delete([ :unix, path ])
116
- logger.log "* Activated #{str}"
117
124
  io = inherit_unix_listener path, sock
125
+ logger.log "* Activated #{str}"
118
126
  else
119
- logger.log "* Listening on #{str}"
120
-
121
127
  umask = nil
122
128
  mode = nil
123
- backlog = nil
129
+ backlog = 1024
124
130
 
125
131
  if uri.query
126
132
  params = Util.parse_query uri.query
@@ -139,6 +145,7 @@ module Puma
139
145
  end
140
146
 
141
147
  io = add_unix_listener path, umask, mode, backlog
148
+ logger.log "* Listening on #{str}"
142
149
  end
143
150
 
144
151
  @listeners << [str, io]
@@ -162,6 +169,7 @@ module Puma
162
169
  end
163
170
 
164
171
  ctx.keystore_pass = params['keystore-pass']
172
+ ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
165
173
  else
166
174
  unless params['key']
167
175
  @events.error "Please specify the SSL key via 'key='"
@@ -182,8 +190,11 @@ module Puma
182
190
  end
183
191
 
184
192
  ctx.ca = params['ca'] if params['ca']
193
+ ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
185
194
  end
186
195
 
196
+ ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
197
+
187
198
  if params['verify_mode']
188
199
  ctx.verify_mode = case params['verify_mode']
189
200
  when "peer"
@@ -202,11 +213,11 @@ module Puma
202
213
  logger.log "* Inherited #{str}"
203
214
  io = inherit_ssl_listener fd, ctx
204
215
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
205
- logger.log "* Activated #{str}"
206
216
  io = inherit_ssl_listener sock, ctx
217
+ logger.log "* Activated #{str}"
207
218
  else
208
- logger.log "* Listening on #{str}"
209
219
  io = add_ssl_listener uri.host, uri.port, ctx
220
+ logger.log "* Listening on #{str}"
210
221
  end
211
222
 
212
223
  @listeners << [str, io] if io
@@ -245,9 +256,10 @@ module Puma
245
256
  end
246
257
  end
247
258
 
248
- def localhost_addresses
249
- addrs = TCPSocket.gethostbyname "localhost"
250
- addrs[3..-1].uniq
259
+ def loopback_addresses
260
+ Socket.ip_address_list.select do |addrinfo|
261
+ addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
262
+ end.map { |addrinfo| addrinfo.ip_address }.uniq
251
263
  end
252
264
 
253
265
  # Tell the server to listen on host +host+, port +port+.
@@ -259,7 +271,7 @@ module Puma
259
271
  #
260
272
  def add_tcp_listener(host, port, optimize_for_latency=true, backlog=1024)
261
273
  if host == "localhost"
262
- localhost_addresses.each do |addr|
274
+ loopback_addresses.each do |addr|
263
275
  add_tcp_listener addr, port, optimize_for_latency, backlog
264
276
  end
265
277
  return
@@ -298,7 +310,7 @@ module Puma
298
310
  MiniSSL.check
299
311
 
300
312
  if host == "localhost"
301
- localhost_addresses.each do |addr|
313
+ loopback_addresses.each do |addr|
302
314
  add_ssl_listener addr, port, ctx, optimize_for_latency, backlog
303
315
  end
304
316
  return
@@ -312,6 +324,7 @@ module Puma
312
324
  s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
313
325
  s.listen backlog
314
326
 
327
+
315
328
  ssl = MiniSSL::Server.new s, ctx
316
329
  env = @proto_env.dup
317
330
  env[HTTPS_KEY] = HTTPS
@@ -343,7 +356,7 @@ module Puma
343
356
 
344
357
  # Tell the server to listen on +path+ as a UNIX domain socket.
345
358
  #
346
- def add_unix_listener(path, umask=nil, mode=nil, backlog=nil)
359
+ def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
347
360
  @unix_paths << path
348
361
 
349
362
  # Let anyone connect by default
@@ -364,7 +377,7 @@ module Puma
364
377
  end
365
378
 
366
379
  s = UNIXServer.new(path)
367
- s.listen backlog if backlog
380
+ s.listen backlog
368
381
  @ios << s
369
382
  ensure
370
383
  File.umask old_mask
data/lib/puma/cli.rb CHANGED
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
  require 'uri'
3
5
 
6
+ require 'puma'
4
7
  require 'puma/configuration'
5
8
  require 'puma/launcher'
6
9
  require 'puma/const'
@@ -83,6 +86,14 @@ module Puma
83
86
  raise UnsupportedOption
84
87
  end
85
88
 
89
+ def configure_control_url(command_line_arg)
90
+ if command_line_arg
91
+ @control_url = command_line_arg
92
+ elsif Puma.jruby?
93
+ unsupported "No default url available on JRuby"
94
+ end
95
+ end
96
+
86
97
  # Build the OptionParser object to handle the available options.
87
98
  #
88
99
 
@@ -97,13 +108,13 @@ module Puma
97
108
  file_config.load arg
98
109
  end
99
110
 
100
- o.on "--control URL", "The bind url to use for the control server",
101
- "Use 'auto' to use temp unix server" do |arg|
102
- if arg
103
- @control_url = arg
104
- elsif Puma.jruby?
105
- unsupported "No default url available on JRuby"
106
- end
111
+ o.on "--control-url URL", "The bind url to use for the control server. Use 'auto' to use temp unix server" do |arg|
112
+ configure_control_url(arg)
113
+ end
114
+
115
+ # alias --control-url for backwards-compatibility
116
+ o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
+ configure_control_url(arg)
107
118
  end
108
119
 
109
120
  o.on "--control-token TOKEN",
@@ -181,6 +192,10 @@ module Puma
181
192
  user_config.tcp_mode!
182
193
  end
183
194
 
195
+ o.on "--early-hints", "Enable early hints support" do
196
+ user_config.early_hints
197
+ end
198
+
184
199
  o.on "-V", "--version", "Print the version information" do
185
200
  puts "puma version #{Puma::Const::VERSION}"
186
201
  exit 0
data/lib/puma/client.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class IO
2
4
  # We need to use this for a jruby work around on both 1.8 and 1.9.
3
5
  # So this either creates the constant (on 1.8), or harmlessly
@@ -21,6 +23,18 @@ module Puma
21
23
 
22
24
  class ConnectionError < RuntimeError; end
23
25
 
26
+ # An instance of this class represents a unique request from a client.
27
+ # For example a web request from a browser or from CURL. This
28
+ #
29
+ # An instance of `Puma::Client` can be used as if it were an IO object
30
+ # by the reactor, that's because the latter is expected to call `#to_io`
31
+ # on any non-IO objects it polls. For example nio4r internally calls
32
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
33
+ # registered.
34
+ #
35
+ # Instances of this class are responsible for knowing if
36
+ # the header and body are fully buffered via the `try_to_finish` method.
37
+ # They can be used to "time out" a response via the `timeout_at` reader.
24
38
  class Client
25
39
  include Puma::Const
26
40
  extend Puma::Delegation
@@ -41,6 +55,7 @@ module Puma
41
55
  @ready = false
42
56
 
43
57
  @body = nil
58
+ @body_read_start = nil
44
59
  @buffer = nil
45
60
  @tempfile = nil
46
61
 
@@ -51,6 +66,8 @@ module Puma
51
66
 
52
67
  @peerip = nil
53
68
  @remote_addr_header = nil
69
+
70
+ @body_remain = 0
54
71
  end
55
72
 
56
73
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -89,6 +106,8 @@ module Puma
89
106
  @tempfile = nil
90
107
  @parsed_bytes = 0
91
108
  @ready = false
109
+ @body_remain = 0
110
+ @peerip = nil
92
111
 
93
112
  if @buffer
94
113
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -101,9 +120,16 @@ module Puma
101
120
  end
102
121
 
103
122
  return false
104
- elsif fast_check &&
105
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
106
- return try_to_finish
123
+ else
124
+ begin
125
+ if fast_check &&
126
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
127
+ return try_to_finish
128
+ end
129
+ rescue IOError
130
+ # swallow it
131
+ end
132
+
107
133
  end
108
134
  end
109
135
 
@@ -111,6 +137,7 @@ module Puma
111
137
  begin
112
138
  @io.close
113
139
  rescue IOError
140
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
114
141
  end
115
142
  end
116
143
 
@@ -133,8 +160,11 @@ module Puma
133
160
  def decode_chunk(chunk)
134
161
  if @partial_part_left > 0
135
162
  if @partial_part_left <= chunk.size
136
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
163
+ if @partial_part_left > 2
164
+ @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
165
+ end
137
166
  chunk = chunk[@partial_part_left..-1]
167
+ @partial_part_left = 0
138
168
  else
139
169
  @body << chunk
140
170
  @partial_part_left -= chunk.size
@@ -156,9 +186,9 @@ module Puma
156
186
  if len == 0
157
187
  @body.rewind
158
188
  rest = io.read
189
+ rest = rest[2..-1] if rest.start_with?("\r\n")
159
190
  @buffer = rest.empty? ? nil : rest
160
- @requests_served += 1
161
- @ready = true
191
+ set_ready
162
192
  return true
163
193
  end
164
194
 
@@ -196,7 +226,7 @@ module Puma
196
226
  while true
197
227
  begin
198
228
  chunk = @io.read_nonblock(4096)
199
- rescue Errno::EAGAIN
229
+ rescue IO::WaitReadable
200
230
  return false
201
231
  rescue SystemCallError, IOError
202
232
  raise ConnectionError, "Connection error detected during read"
@@ -206,8 +236,7 @@ module Puma
206
236
  unless chunk
207
237
  @body.close
208
238
  @buffer = nil
209
- @requests_served += 1
210
- @ready = true
239
+ set_ready
211
240
  raise EOFError
212
241
  end
213
242
 
@@ -216,6 +245,8 @@ module Puma
216
245
  end
217
246
 
218
247
  def setup_body
248
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
249
+
219
250
  if @env[HTTP_EXPECT] == CONTINUE
220
251
  # TODO allow a hook here to check the headers before
221
252
  # going forward
@@ -240,8 +271,7 @@ module Puma
240
271
  unless cl
241
272
  @buffer = body.empty? ? nil : body
242
273
  @body = EmptyBody
243
- @requests_served += 1
244
- @ready = true
274
+ set_ready
245
275
  return true
246
276
  end
247
277
 
@@ -250,8 +280,7 @@ module Puma
250
280
  if remain <= 0
251
281
  @body = StringIO.new(body)
252
282
  @buffer = nil
253
- @requests_served += 1
254
- @ready = true
283
+ set_ready
255
284
  return true
256
285
  end
257
286
 
@@ -279,10 +308,17 @@ module Puma
279
308
  data = @io.read_nonblock(CHUNK_SIZE)
280
309
  rescue Errno::EAGAIN
281
310
  return false
282
- rescue SystemCallError, IOError
311
+ rescue SystemCallError, IOError, EOFError
283
312
  raise ConnectionError, "Connection error detected during read"
284
313
  end
285
314
 
315
+ # No data means a closed socket
316
+ unless data
317
+ @buffer = nil
318
+ set_ready
319
+ raise EOFError
320
+ end
321
+
286
322
  if @buffer
287
323
  @buffer << data
288
324
  else
@@ -312,6 +348,13 @@ module Puma
312
348
  raise e
313
349
  end
314
350
 
351
+ # No data means a closed socket
352
+ unless data
353
+ @buffer = nil
354
+ set_ready
355
+ raise EOFError
356
+ end
357
+
315
358
  if @buffer
316
359
  @buffer << data
317
360
  else
@@ -385,8 +428,7 @@ module Puma
385
428
  unless chunk
386
429
  @body.close
387
430
  @buffer = nil
388
- @requests_served += 1
389
- @ready = true
431
+ set_ready
390
432
  raise EOFError
391
433
  end
392
434
 
@@ -395,8 +437,7 @@ module Puma
395
437
  if remain <= 0
396
438
  @body.rewind
397
439
  @buffer = nil
398
- @requests_served += 1
399
- @ready = true
440
+ set_ready
400
441
  return true
401
442
  end
402
443
 
@@ -405,6 +446,14 @@ module Puma
405
446
  false
406
447
  end
407
448
 
449
+ def set_ready
450
+ if @body_read_start
451
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
452
+ end
453
+ @requests_served += 1
454
+ @ready = true
455
+ end
456
+
408
457
  def write_400
409
458
  begin
410
459
  @io << ERROR_400_RESPONSE