puma 3.5.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 (67) hide show
  1. checksums.yaml +5 -5
  2. data/{History.txt → History.md} +318 -75
  3. data/README.md +143 -227
  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 +124 -22
  13. data/ext/puma_http11/extconf.rb +2 -0
  14. data/ext/puma_http11/http11_parser.c +85 -84
  15. data/ext/puma_http11/http11_parser.h +1 -0
  16. data/ext/puma_http11/http11_parser.rl +10 -9
  17. data/ext/puma_http11/io_buffer.c +7 -7
  18. data/ext/puma_http11/mini_ssl.c +67 -6
  19. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +13 -16
  20. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -2
  21. data/ext/puma_http11/puma_http11.c +1 -0
  22. data/lib/puma.rb +13 -5
  23. data/lib/puma/app/status.rb +8 -0
  24. data/lib/puma/binder.rb +22 -17
  25. data/lib/puma/cli.rb +49 -33
  26. data/lib/puma/client.rb +149 -4
  27. data/lib/puma/cluster.rb +54 -12
  28. data/lib/puma/commonlogger.rb +19 -20
  29. data/lib/puma/compat.rb +3 -7
  30. data/lib/puma/configuration.rb +133 -130
  31. data/lib/puma/const.rb +19 -37
  32. data/lib/puma/control_cli.rb +38 -35
  33. data/lib/puma/convenient.rb +3 -3
  34. data/lib/puma/detect.rb +3 -1
  35. data/lib/puma/dsl.rb +80 -58
  36. data/lib/puma/events.rb +6 -8
  37. data/lib/puma/io_buffer.rb +1 -1
  38. data/lib/puma/jruby_restart.rb +0 -1
  39. data/lib/puma/launcher.rb +61 -30
  40. data/lib/puma/minissl.rb +85 -4
  41. data/lib/puma/null_io.rb +6 -13
  42. data/lib/puma/plugin.rb +12 -1
  43. data/lib/puma/plugin/tmp_restart.rb +1 -2
  44. data/lib/puma/rack/builder.rb +3 -0
  45. data/lib/puma/rack/urlmap.rb +9 -8
  46. data/lib/puma/reactor.rb +135 -0
  47. data/lib/puma/runner.rb +27 -1
  48. data/lib/puma/server.rb +134 -32
  49. data/lib/puma/single.rb +17 -3
  50. data/lib/puma/thread_pool.rb +67 -20
  51. data/lib/puma/util.rb +1 -5
  52. data/lib/rack/handler/puma.rb +58 -17
  53. data/tools/jungle/README.md +12 -2
  54. data/tools/jungle/init.d/README.md +9 -2
  55. data/tools/jungle/init.d/puma +32 -62
  56. data/tools/jungle/init.d/run-puma +5 -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 +22 -92
  62. data/Gemfile +0 -13
  63. data/Manifest.txt +0 -77
  64. data/Rakefile +0 -158
  65. data/lib/puma/rack/backports/uri/common_18.rb +0 -59
  66. data/lib/puma/rack/backports/uri/common_192.rb +0 -55
  67. data/puma.gemspec +0 -52
@@ -1,7 +1,6 @@
1
1
  # Standard libraries
2
2
  require 'socket'
3
3
  require 'tempfile'
4
- require 'yaml'
5
4
  require 'time'
6
5
  require 'etc'
7
6
  require 'uri'
@@ -9,7 +8,16 @@ require 'stringio'
9
8
 
10
9
  require 'thread'
11
10
 
12
- # Ruby Puma
13
- require 'puma/const'
14
- require 'puma/server'
15
- require 'puma/launcher'
11
+ module Puma
12
+ autoload :Const, 'puma/const'
13
+ autoload :Server, 'puma/server'
14
+ autoload :Launcher, 'puma/launcher'
15
+
16
+ def self.stats_object=(val)
17
+ @get_stats = val
18
+ end
19
+
20
+ def self.stats
21
+ @get_stats.stats
22
+ end
23
+ end
@@ -55,6 +55,14 @@ module Puma
55
55
  return rack_response(200, OK_STATUS)
56
56
  end
57
57
 
58
+ when /\/gc$/
59
+ GC.start
60
+ return rack_response(200, OK_STATUS)
61
+
62
+ when /\/gc-stats$/
63
+ json = "{" + GC.stat.map { |k, v| "\"#{k}\": #{v}" }.join(",") + "}"
64
+ return rack_response(200, json)
65
+
58
66
  when /\/stats$/
59
67
  return rack_response(200, @cli.stats)
60
68
  else
@@ -1,5 +1,8 @@
1
- require 'puma/const'
2
1
  require 'uri'
2
+ require 'socket'
3
+
4
+ require 'puma/const'
5
+ require 'puma/util'
3
6
 
4
7
  module Puma
5
8
  class Binder
@@ -102,7 +105,7 @@ module Puma
102
105
  io = add_tcp_listener uri.host, uri.port, opt, bak
103
106
  end
104
107
 
105
- @listeners << [str, io]
108
+ @listeners << [str, io] if io
106
109
  when "unix"
107
110
  path = "#{uri.host}#{uri.path}".gsub("%20", " ")
108
111
 
@@ -117,7 +120,7 @@ module Puma
117
120
 
118
121
  umask = nil
119
122
  mode = nil
120
- backlog = nil
123
+ backlog = 1024
121
124
 
122
125
  if uri.query
123
126
  params = Util.parse_query uri.query
@@ -140,11 +143,11 @@ module Puma
140
143
 
141
144
  @listeners << [str, io]
142
145
  when "ssl"
143
- MiniSSL.check
144
-
145
146
  params = Util.parse_query uri.query
146
147
  require 'puma/minissl'
147
148
 
149
+ MiniSSL.check
150
+
148
151
  ctx = MiniSSL::Context.new
149
152
 
150
153
  if defined?(JRUBY_VERSION)
@@ -159,6 +162,7 @@ module Puma
159
162
  end
160
163
 
161
164
  ctx.keystore_pass = params['keystore-pass']
165
+ ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
162
166
  else
163
167
  unless params['key']
164
168
  @events.error "Please specify the SSL key via 'key='"
@@ -179,9 +183,10 @@ module Puma
179
183
  end
180
184
 
181
185
  ctx.ca = params['ca'] if params['ca']
186
+ ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
182
187
  end
183
188
 
184
- if params['verify_mode']
189
+ if params['verify_mode']
185
190
  ctx.verify_mode = case params['verify_mode']
186
191
  when "peer"
187
192
  MiniSSL::VERIFY_PEER
@@ -193,8 +198,6 @@ module Puma
193
198
  @events.error "Please specify a valid verify_mode="
194
199
  MiniSSL::VERIFY_NONE
195
200
  end
196
- else
197
- ctx.verify_mode = MiniSSL::VERIFY_PEER
198
201
  end
199
202
 
200
203
  if fd = @inherited_fds.delete(str)
@@ -208,7 +211,7 @@ module Puma
208
211
  io = add_ssl_listener uri.host, uri.port, ctx
209
212
  end
210
213
 
211
- @listeners << [str, io]
214
+ @listeners << [str, io] if io
212
215
  else
213
216
  logger.error "Invalid URI: #{str}"
214
217
  end
@@ -244,9 +247,10 @@ module Puma
244
247
  end
245
248
  end
246
249
 
247
- def localhost_addresses
248
- addrs = TCPSocket.gethostbyname "localhost"
249
- addrs[3..-1]
250
+ def loopback_addresses
251
+ Socket.ip_address_list.select do |addrinfo|
252
+ addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
253
+ end.map { |addrinfo| addrinfo.ip_address }.uniq
250
254
  end
251
255
 
252
256
  # Tell the server to listen on host +host+, port +port+.
@@ -258,7 +262,7 @@ module Puma
258
262
  #
259
263
  def add_tcp_listener(host, port, optimize_for_latency=true, backlog=1024)
260
264
  if host == "localhost"
261
- localhost_addresses.each do |addr|
265
+ loopback_addresses.each do |addr|
262
266
  add_tcp_listener addr, port, optimize_for_latency, backlog
263
267
  end
264
268
  return
@@ -297,8 +301,8 @@ module Puma
297
301
  MiniSSL.check
298
302
 
299
303
  if host == "localhost"
300
- localhost_addresses.each do |addr|
301
- add_ssl_listener addr, port, optimize_for_latency, backlog
304
+ loopback_addresses.each do |addr|
305
+ add_ssl_listener addr, port, ctx, optimize_for_latency, backlog
302
306
  end
303
307
  return
304
308
  end
@@ -311,6 +315,7 @@ module Puma
311
315
  s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
312
316
  s.listen backlog
313
317
 
318
+
314
319
  ssl = MiniSSL::Server.new s, ctx
315
320
  env = @proto_env.dup
316
321
  env[HTTPS_KEY] = HTTPS
@@ -342,7 +347,7 @@ module Puma
342
347
 
343
348
  # Tell the server to listen on +path+ as a UNIX domain socket.
344
349
  #
345
- def add_unix_listener(path, umask=nil, mode=nil, backlog=nil)
350
+ def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
346
351
  @unix_paths << path
347
352
 
348
353
  # Let anyone connect by default
@@ -363,7 +368,7 @@ module Puma
363
368
  end
364
369
 
365
370
  s = UNIXServer.new(path)
366
- s.listen backlog if backlog
371
+ s.listen backlog
367
372
  @ios << s
368
373
  ensure
369
374
  File.umask old_mask
@@ -1,7 +1,11 @@
1
1
  require 'optparse'
2
2
  require 'uri'
3
3
 
4
+ require 'puma'
5
+ require 'puma/configuration'
4
6
  require 'puma/launcher'
7
+ require 'puma/const'
8
+ require 'puma/events'
5
9
 
6
10
  module Puma
7
11
  class << self
@@ -44,21 +48,21 @@ module Puma
44
48
  @parser.parse! @argv
45
49
 
46
50
  if file = @argv.shift
47
- @conf.configure do |c|
48
- c.rackup file
51
+ @conf.configure do |user_config, file_config|
52
+ file_config.rackup file
49
53
  end
50
54
  end
51
55
  rescue UnsupportedOption
52
56
  exit 1
53
57
  end
54
58
 
55
- @conf.configure do |c|
59
+ @conf.configure do |user_config, file_config|
56
60
  if @stdout || @stderr
57
- c.stdout_redirect @stdout, @stderr, @append
61
+ user_config.stdout_redirect @stdout, @stderr, @append
58
62
  end
59
63
 
60
64
  if @control_url
61
- c.activate_control_app @control_url, @control_options
65
+ user_config.activate_control_app @control_url, @control_options
62
66
  end
63
67
  end
64
68
 
@@ -80,27 +84,35 @@ module Puma
80
84
  raise UnsupportedOption
81
85
  end
82
86
 
87
+ def configure_control_url(command_line_arg)
88
+ if command_line_arg
89
+ @control_url = command_line_arg
90
+ elsif Puma.jruby?
91
+ unsupported "No default url available on JRuby"
92
+ end
93
+ end
94
+
83
95
  # Build the OptionParser object to handle the available options.
84
96
  #
85
97
 
86
98
  def setup_options
87
- @conf = Configuration.new do |c|
99
+ @conf = Configuration.new do |user_config, file_config|
88
100
  @parser = OptionParser.new do |o|
89
101
  o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
90
- c.bind arg
102
+ user_config.bind arg
91
103
  end
92
104
 
93
105
  o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
94
- c.load arg
106
+ file_config.load arg
95
107
  end
96
108
 
97
- o.on "--control URL", "The bind url to use for the control server",
98
- "Use 'auto' to use temp unix server" do |arg|
99
- if arg
100
- @control_url = arg
101
- elsif Puma.jruby?
102
- unsupported "No default url available on JRuby"
103
- end
109
+ o.on "--control-url URL", "The bind url to use for the control server. Use 'auto' to use temp unix server" do |arg|
110
+ configure_control_url(arg)
111
+ end
112
+
113
+ # alias --control-url for backwards-compatibility
114
+ o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
115
+ configure_control_url(arg)
104
116
  end
105
117
 
106
118
  o.on "--control-token TOKEN",
@@ -109,21 +121,21 @@ module Puma
109
121
  end
110
122
 
111
123
  o.on "-d", "--daemon", "Daemonize the server into the background" do
112
- c.daemonize
113
- c.quiet
124
+ user_config.daemonize
125
+ user_config.quiet
114
126
  end
115
127
 
116
128
  o.on "--debug", "Log lowlevel debugging information" do
117
- c.debug
129
+ user_config.debug
118
130
  end
119
131
 
120
132
  o.on "--dir DIR", "Change to DIR before starting" do |d|
121
- c.directory d
133
+ user_config.directory d
122
134
  end
123
135
 
124
136
  o.on "-e", "--environment ENVIRONMENT",
125
137
  "The environment to run the Rack app on (default development)" do |arg|
126
- c.environment arg
138
+ user_config.environment arg
127
139
  end
128
140
 
129
141
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
@@ -132,50 +144,54 @@ module Puma
132
144
 
133
145
  o.on "-p", "--port PORT", "Define the TCP port to bind to",
134
146
  "Use -b for more advanced options" do |arg|
135
- c.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
147
+ user_config.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
136
148
  end
137
149
 
138
150
  o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
139
- c.pidfile arg
151
+ user_config.pidfile arg
140
152
  end
141
153
 
142
154
  o.on "--preload", "Preload the app. Cluster mode only" do
143
- c.preload_app!
155
+ user_config.preload_app!
144
156
  end
145
157
 
146
158
  o.on "--prune-bundler", "Prune out the bundler env if possible" do
147
- c.prune_bundler
159
+ user_config.prune_bundler
148
160
  end
149
161
 
150
162
  o.on "-q", "--quiet", "Do not log requests internally (default true)" do
151
- c.quiet
163
+ user_config.quiet
152
164
  end
153
165
 
154
166
  o.on "-v", "--log-requests", "Log requests as they occur" do
155
- c.log_requests
167
+ user_config.log_requests
156
168
  end
157
169
 
158
170
  o.on "-R", "--restart-cmd CMD",
159
171
  "The puma command to run during a hot restart",
160
172
  "Default: inferred" do |cmd|
161
- c.restart_command cmd
173
+ user_config.restart_command cmd
162
174
  end
163
175
 
164
176
  o.on "-S", "--state PATH", "Where to store the state details" do |arg|
165
- c.state_path arg
177
+ user_config.state_path arg
166
178
  end
167
179
 
168
180
  o.on '-t', '--threads INT', "min:max threads to use (default 0:16)" do |arg|
169
181
  min, max = arg.split(":")
170
182
  if max
171
- c.threads min, max
183
+ user_config.threads min, max
172
184
  else
173
- c.threads 0, max
185
+ user_config.threads min, min
174
186
  end
175
187
  end
176
188
 
177
189
  o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
178
- c.tcp_mode!
190
+ user_config.tcp_mode!
191
+ end
192
+
193
+ o.on "--early-hints", "Enable early hints support" do
194
+ user_config.early_hints
179
195
  end
180
196
 
181
197
  o.on "-V", "--version", "Print the version information" do
@@ -185,11 +201,11 @@ module Puma
185
201
 
186
202
  o.on "-w", "--workers COUNT",
187
203
  "Activate cluster mode: How many worker processes to create" do |arg|
188
- c.workers arg
204
+ user_config.workers arg
189
205
  end
190
206
 
191
207
  o.on "--tag NAME", "Additional text to display in process listing" do |arg|
192
- c.tag arg
208
+ user_config.tag arg
193
209
  end
194
210
 
195
211
  o.on "--redirect-stdout FILE", "Redirect STDOUT to a specific file" do |arg|
@@ -8,6 +8,7 @@ end
8
8
 
9
9
  require 'puma/detect'
10
10
  require 'puma/delegation'
11
+ require 'tempfile'
11
12
 
12
13
  if Puma::IS_JRUBY
13
14
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -20,6 +21,17 @@ module Puma
20
21
 
21
22
  class ConnectionError < RuntimeError; end
22
23
 
24
+ # An instance of this class represents a unique request from a client.
25
+ # For example a web request from a browser or from CURL. This
26
+ #
27
+ # An instance of `Puma::Client` can be used as if it were an IO object
28
+ # for example it is passed into `IO.select` inside of the `Puma::Reactor`.
29
+ # This is accomplished by the `to_io` method which gets called on any
30
+ # non-IO objects being used with the IO api such as `IO.select.
31
+ #
32
+ # Instances of this class are responsible for knowing if
33
+ # the header and body are fully buffered via the `try_to_finish` method.
34
+ # They can be used to "time out" a response via the `timeout_at` reader.
23
35
  class Client
24
36
  include Puma::Const
25
37
  extend Puma::Delegation
@@ -110,6 +122,7 @@ module Puma
110
122
  begin
111
123
  @io.close
112
124
  rescue IOError
125
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
113
126
  end
114
127
  end
115
128
 
@@ -117,9 +130,123 @@ module Puma
117
130
  # no body share this one object since it has no state.
118
131
  EmptyBody = NullIO.new
119
132
 
133
+ def setup_chunked_body(body)
134
+ @chunked_body = true
135
+ @partial_part_left = 0
136
+ @prev_chunk = ""
137
+
138
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
139
+ @body.binmode
140
+ @tempfile = @body
141
+
142
+ return decode_chunk(body)
143
+ end
144
+
145
+ def decode_chunk(chunk)
146
+ if @partial_part_left > 0
147
+ if @partial_part_left <= chunk.size
148
+ @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
149
+ chunk = chunk[@partial_part_left..-1]
150
+ else
151
+ @body << chunk
152
+ @partial_part_left -= chunk.size
153
+ return false
154
+ end
155
+ end
156
+
157
+ if @prev_chunk.empty?
158
+ io = StringIO.new(chunk)
159
+ else
160
+ io = StringIO.new(@prev_chunk+chunk)
161
+ @prev_chunk = ""
162
+ end
163
+
164
+ while !io.eof?
165
+ line = io.gets
166
+ if line.end_with?("\r\n")
167
+ len = line.strip.to_i(16)
168
+ if len == 0
169
+ @body.rewind
170
+ rest = io.read
171
+ @buffer = rest.empty? ? nil : rest
172
+ @requests_served += 1
173
+ @ready = true
174
+ return true
175
+ end
176
+
177
+ len += 2
178
+
179
+ part = io.read(len)
180
+
181
+ unless part
182
+ @partial_part_left = len
183
+ next
184
+ end
185
+
186
+ got = part.size
187
+
188
+ case
189
+ when got == len
190
+ @body << part[0..-3] # to skip the ending \r\n
191
+ when got <= len - 2
192
+ @body << part
193
+ @partial_part_left = len - part.size
194
+ when got == len - 1 # edge where we get just \r but not \n
195
+ @body << part[0..-2]
196
+ @partial_part_left = len - part.size
197
+ end
198
+ else
199
+ @prev_chunk = line
200
+ return false
201
+ end
202
+ end
203
+
204
+ return false
205
+ end
206
+
207
+ def read_chunked_body
208
+ while true
209
+ begin
210
+ chunk = @io.read_nonblock(4096)
211
+ rescue Errno::EAGAIN
212
+ return false
213
+ rescue SystemCallError, IOError
214
+ raise ConnectionError, "Connection error detected during read"
215
+ end
216
+
217
+ # No chunk means a closed socket
218
+ unless chunk
219
+ @body.close
220
+ @buffer = nil
221
+ @requests_served += 1
222
+ @ready = true
223
+ raise EOFError
224
+ end
225
+
226
+ return true if decode_chunk(chunk)
227
+ end
228
+ end
229
+
120
230
  def setup_body
121
- @in_data_phase = true
231
+ if @env[HTTP_EXPECT] == CONTINUE
232
+ # TODO allow a hook here to check the headers before
233
+ # going forward
234
+ @io << HTTP_11_100
235
+ @io.flush
236
+ end
237
+
238
+ @read_header = false
239
+
122
240
  body = @parser.body
241
+
242
+ te = @env[TRANSFER_ENCODING2]
243
+
244
+ if te && CHUNKED.casecmp(te) == 0
245
+ return setup_chunked_body(body)
246
+ end
247
+
248
+ @chunked_body = false
249
+
123
250
  cl = @env[CONTENT_LENGTH]
124
251
 
125
252
  unless cl
@@ -154,8 +281,6 @@ module Puma
154
281
 
155
282
  @body_remain = remain
156
283
 
157
- @read_header = false
158
-
159
284
  return false
160
285
  end
161
286
 
@@ -170,6 +295,14 @@ module Puma
170
295
  raise ConnectionError, "Connection error detected during read"
171
296
  end
172
297
 
298
+ # No data means a closed socket
299
+ unless data
300
+ @buffer = nil
301
+ @requests_served += 1
302
+ @ready = true
303
+ raise EOFError
304
+ end
305
+
173
306
  if @buffer
174
307
  @buffer << data
175
308
  else
@@ -184,7 +317,7 @@ module Puma
184
317
  raise HttpParserError,
185
318
  "HEADER is longer than allowed, aborting client early."
186
319
  end
187
-
320
+
188
321
  false
189
322
  end
190
323
 
@@ -199,6 +332,14 @@ module Puma
199
332
  raise e
200
333
  end
201
334
 
335
+ # No data means a closed socket
336
+ unless data
337
+ @buffer = nil
338
+ @requests_served += 1
339
+ @ready = true
340
+ raise EOFError
341
+ end
342
+
202
343
  if @buffer
203
344
  @buffer << data
204
345
  else
@@ -246,6 +387,10 @@ module Puma
246
387
  end
247
388
 
248
389
  def read_body
390
+ if @chunked_body
391
+ return read_chunked_body
392
+ end
393
+
249
394
  # Read an odd sized chunk so we can read even sized ones
250
395
  # after this
251
396
  remain = @body_remain