puma 3.9.1 → 4.3.1

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 (82) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +232 -0
  3. data/README.md +162 -224
  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 +38 -0
  10. data/docs/restart.md +41 -0
  11. data/docs/signals.md +56 -3
  12. data/docs/systemd.md +130 -37
  13. data/docs/tcp_mode.md +96 -0
  14. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  15. data/ext/puma_http11/extconf.rb +13 -0
  16. data/ext/puma_http11/http11_parser.c +115 -140
  17. data/ext/puma_http11/http11_parser.java.rl +21 -37
  18. data/ext/puma_http11/http11_parser.rl +9 -9
  19. data/ext/puma_http11/http11_parser_common.rl +3 -3
  20. data/ext/puma_http11/mini_ssl.c +104 -8
  21. data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
  22. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +90 -108
  23. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  24. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +21 -4
  25. data/ext/puma_http11/puma_http11.c +2 -0
  26. data/lib/puma.rb +16 -0
  27. data/lib/puma/accept_nonblock.rb +7 -1
  28. data/lib/puma/app/status.rb +40 -26
  29. data/lib/puma/binder.rb +57 -74
  30. data/lib/puma/cli.rb +26 -7
  31. data/lib/puma/client.rb +243 -190
  32. data/lib/puma/cluster.rb +78 -34
  33. data/lib/puma/commonlogger.rb +2 -0
  34. data/lib/puma/configuration.rb +24 -16
  35. data/lib/puma/const.rb +36 -18
  36. data/lib/puma/control_cli.rb +46 -19
  37. data/lib/puma/detect.rb +2 -0
  38. data/lib/puma/dsl.rb +329 -68
  39. data/lib/puma/events.rb +6 -1
  40. data/lib/puma/io_buffer.rb +3 -6
  41. data/lib/puma/jruby_restart.rb +2 -1
  42. data/lib/puma/launcher.rb +120 -58
  43. data/lib/puma/minissl.rb +69 -27
  44. data/lib/puma/minissl/context_builder.rb +76 -0
  45. data/lib/puma/null_io.rb +2 -0
  46. data/lib/puma/plugin.rb +7 -2
  47. data/lib/puma/plugin/tmp_restart.rb +2 -1
  48. data/lib/puma/rack/builder.rb +4 -1
  49. data/lib/puma/rack/urlmap.rb +2 -0
  50. data/lib/puma/rack_default.rb +2 -0
  51. data/lib/puma/reactor.rb +224 -34
  52. data/lib/puma/runner.rb +25 -4
  53. data/lib/puma/server.rb +148 -62
  54. data/lib/puma/single.rb +16 -5
  55. data/lib/puma/state_file.rb +2 -0
  56. data/lib/puma/tcp_logger.rb +2 -0
  57. data/lib/puma/thread_pool.rb +61 -38
  58. data/lib/puma/util.rb +2 -6
  59. data/lib/rack/handler/puma.rb +10 -4
  60. data/tools/docker/Dockerfile +16 -0
  61. data/tools/jungle/README.md +12 -2
  62. data/tools/jungle/init.d/README.md +2 -0
  63. data/tools/jungle/init.d/puma +8 -8
  64. data/tools/jungle/init.d/run-puma +1 -1
  65. data/tools/jungle/rc.d/README.md +74 -0
  66. data/tools/jungle/rc.d/puma +61 -0
  67. data/tools/jungle/rc.d/puma.conf +10 -0
  68. data/tools/trickletest.rb +1 -2
  69. metadata +29 -56
  70. data/.github/issue_template.md +0 -20
  71. data/Gemfile +0 -14
  72. data/Manifest.txt +0 -78
  73. data/Rakefile +0 -165
  74. data/Release.md +0 -9
  75. data/gemfiles/2.1-Gemfile +0 -12
  76. data/lib/puma/compat.rb +0 -14
  77. data/lib/puma/convenient.rb +0 -23
  78. data/lib/puma/daemon_ext.rb +0 -31
  79. data/lib/puma/delegation.rb +0 -11
  80. data/lib/puma/java_io_buffer.rb +0 -45
  81. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  82. data/puma.gemspec +0 -20
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/const'
2
4
  require "puma/null_io"
3
5
  require 'stringio'
@@ -91,7 +93,10 @@ module Puma
91
93
  # parsing exception.
92
94
  #
93
95
  def parse_error(server, env, error)
94
- @stderr.puts "#{Time.now}: HTTP parse error, malformed request (#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}): #{error.inspect}\n---\n"
96
+ @stderr.puts "#{Time.now}: HTTP parse error, malformed request " \
97
+ "(#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}#{env[REQUEST_PATH]}): " \
98
+ "#{error.inspect}" \
99
+ "\n---\n"
95
100
  end
96
101
 
97
102
  # An SSL error has occurred.
@@ -1,7 +1,4 @@
1
- require 'puma/detect'
1
+ # frozen_string_literal: true
2
2
 
3
- if Puma.jruby?
4
- require 'puma/java_io_buffer'
5
- else
6
- require 'puma/puma_http11'
7
- end
3
+ require 'puma/detect'
4
+ require 'puma/puma_http11'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ffi'
2
4
 
3
5
  module Puma
@@ -80,4 +82,3 @@ module Puma
80
82
  end
81
83
  end
82
84
  end
83
-
@@ -1,11 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/events'
2
4
  require 'puma/detect'
3
-
4
5
  require 'puma/cluster'
5
6
  require 'puma/single'
6
-
7
7
  require 'puma/const'
8
-
9
8
  require 'puma/binder'
10
9
 
11
10
  module Puma
@@ -40,7 +39,7 @@ module Puma
40
39
  # [200, {}, ["hello world"]]
41
40
  # end
42
41
  # end
43
- # Puma::Launcher.new(conf, argv: Puma::Events.stdio).run
42
+ # Puma::Launcher.new(conf, events: Puma::Events.stdio).run
44
43
  def initialize(conf, launcher_args={})
45
44
  @runner = nil
46
45
  @events = launcher_args[:events] || Events::DEFAULT
@@ -61,10 +60,13 @@ module Puma
61
60
  @options = @config.options
62
61
  @config.clamp
63
62
 
63
+ @events.formatter = Events::PidFormatter.new if clustered?
64
+ @events.formatter = options[:log_formatter] if @options[:log_formatter]
65
+
64
66
  generate_restart_data
65
67
 
66
- if clustered? && (Puma.jruby? || Puma.windows?)
67
- unsupported 'worker mode not supported on JRuby or Windows'
68
+ if clustered? && !Process.respond_to?(:fork)
69
+ unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
68
70
  end
69
71
 
70
72
  if @options[:daemon] && Puma.windows?
@@ -79,13 +81,13 @@ module Puma
79
81
  set_rack_environment
80
82
 
81
83
  if clustered?
82
- @events.formatter = Events::PidFormatter.new
83
84
  @options[:logger] = @events
84
85
 
85
86
  @runner = Cluster.new(self, @events)
86
87
  else
87
88
  @runner = Single.new(self, @events)
88
89
  end
90
+ Puma.stats_object = @runner
89
91
 
90
92
  @status = :run
91
93
  end
@@ -121,19 +123,6 @@ module Puma
121
123
  File.unlink(path) if path && File.exist?(path)
122
124
  end
123
125
 
124
- # If configured, write the pid of the current process out
125
- # to a file.
126
- def write_pid
127
- path = @options[:pidfile]
128
- return unless path
129
-
130
- File.open(path, 'w') { |f| f.puts Process.pid }
131
- cur = Process.pid
132
- at_exit do
133
- delete_pidfile if cur == Process.pid
134
- end
135
- end
136
-
137
126
  # Begin async shutdown of the server
138
127
  def halt
139
128
  @status = :halt
@@ -163,7 +152,16 @@ module Puma
163
152
 
164
153
  # Run the server. This blocks until the server is stopped
165
154
  def run
166
- previous_env = (defined?(Bundler) ? Bundler::ORIGINAL_ENV : ENV.to_h)
155
+ previous_env =
156
+ if defined?(Bundler)
157
+ env = Bundler::ORIGINAL_ENV.dup
158
+ # add -rbundler/setup so we load from Gemfile when restarting
159
+ bundle = "-rbundler/setup"
160
+ env["RUBYOPT"] = [env["RUBYOPT"], bundle].join(" ").lstrip unless env["RUBYOPT"].to_s.include?(bundle)
161
+ env
162
+ else
163
+ ENV.to_h
164
+ end
167
165
 
168
166
  @config.clamp
169
167
 
@@ -186,6 +184,7 @@ module Puma
186
184
  when :exit
187
185
  # nothing
188
186
  end
187
+ @binder.close_unix_paths
189
188
  end
190
189
 
191
190
  # Return which tcp port the launcher is using, if it's using TCP
@@ -202,8 +201,25 @@ module Puma
202
201
  end
203
202
  end
204
203
 
204
+ def close_binder_listeners
205
+ @binder.close_listeners
206
+ end
207
+
205
208
  private
206
209
 
210
+ # If configured, write the pid of the current process out
211
+ # to a file.
212
+ def write_pid
213
+ path = @options[:pidfile]
214
+ return unless path
215
+
216
+ File.open(path, 'w') { |f| f.puts Process.pid }
217
+ cur = Process.pid
218
+ at_exit do
219
+ delete_pidfile if cur == Process.pid
220
+ end
221
+ end
222
+
207
223
  def reload_worker_directory
208
224
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
209
225
  end
@@ -223,48 +239,71 @@ module Puma
223
239
  Dir.chdir(@restart_dir)
224
240
  Kernel.exec(*argv)
225
241
  else
226
- redirects = {:close_others => true}
227
- @binder.listeners.each_with_index do |(l, io), i|
228
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
229
- redirects[io.to_i] = io.to_i
230
- end
231
-
232
242
  argv = restart_args
233
243
  Dir.chdir(@restart_dir)
234
- argv += [redirects] if RUBY_VERSION >= '1.9'
244
+ argv += [@binder.redirects_for_restart]
235
245
  Kernel.exec(*argv)
236
246
  end
237
247
  end
238
248
 
239
- def prune_bundler
240
- return unless defined?(Bundler)
241
- puma = Bundler.rubygems.loaded_specs("puma")
242
- dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
249
+ def dependencies_and_files_to_require_after_prune
250
+ puma = spec_for_gem("puma")
251
+
252
+ deps = puma.runtime_dependencies.map do |d|
253
+ "#{d.name}:#{spec_for_gem(d.name).version}"
254
+ end
255
+
256
+ [deps, require_paths_for_gem(puma) + extra_runtime_deps_directories]
257
+ end
258
+
259
+ def extra_runtime_deps_directories
260
+ Array(@options[:extra_runtime_dependencies]).map do |d_name|
261
+ if (spec = spec_for_gem(d_name))
262
+ require_paths_for_gem(spec)
263
+ else
264
+ log "* Could not load extra dependency: #{d_name}"
265
+ nil
266
+ end
267
+ end.flatten.compact
268
+ end
269
+
270
+ def puma_wild_location
271
+ puma = spec_for_gem("puma")
272
+ dirs = require_paths_for_gem(puma)
243
273
  puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
274
+ File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
275
+ end
244
276
 
245
- unless puma_lib_dir
277
+ def prune_bundler
278
+ return unless defined?(Bundler)
279
+ require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
280
+ unless puma_wild_location
246
281
  log "! Unable to prune Bundler environment, continuing"
247
282
  return
248
283
  end
249
284
 
250
- deps = puma.runtime_dependencies.map do |d|
251
- spec = Bundler.rubygems.loaded_specs(d.name)
252
- "#{d.name}:#{spec.version.to_s}"
253
- end
285
+ deps, dirs = dependencies_and_files_to_require_after_prune
254
286
 
255
287
  log '* Pruning Bundler environment'
256
288
  home = ENV['GEM_HOME']
257
289
  Bundler.with_clean_env do
258
290
  ENV['GEM_HOME'] = home
259
291
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
260
- wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
261
- args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
292
+ args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':'), deps.join(',')] + @original_argv
262
293
  # Ruby 2.0+ defaults to true which breaks socket activation
263
- args += [{:close_others => false}] if RUBY_VERSION >= '2.0'
294
+ args += [{:close_others => false}]
264
295
  Kernel.exec(*args)
265
296
  end
266
297
  end
267
298
 
299
+ def spec_for_gem(gem_name)
300
+ Bundler.rubygems.loaded_specs(gem_name)
301
+ end
302
+
303
+ def require_paths_for_gem(gem_spec)
304
+ gem_spec.full_require_paths
305
+ end
306
+
268
307
  def log(str)
269
308
  @events.log str
270
309
  end
@@ -284,13 +323,28 @@ module Puma
284
323
  log "- Goodbye!"
285
324
  end
286
325
 
326
+ def log_thread_status
327
+ Thread.list.each do |thread|
328
+ log "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
329
+ logstr = "Thread: TID-#{thread.object_id.to_s(36)}"
330
+ logstr += " #{thread.name}" if thread.respond_to?(:name)
331
+ log logstr
332
+
333
+ if thread.backtrace
334
+ log thread.backtrace.join("\n")
335
+ else
336
+ log "<no backtrace available>"
337
+ end
338
+ end
339
+ end
340
+
287
341
  def set_process_title
288
342
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
289
343
  end
290
344
 
291
345
  def title
292
- buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
293
- buffer << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
346
+ buffer = "puma #{Puma::Const::VERSION} (#{@options[:binds].join(',')})"
347
+ buffer += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
294
348
  buffer
295
349
  end
296
350
 
@@ -307,16 +361,6 @@ module Puma
307
361
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
308
362
  end
309
363
 
310
- def close_binder_listeners
311
- @binder.listeners.each do |l, io|
312
- io.close
313
- uri = URI.parse(l)
314
- next unless uri.scheme == 'unix'
315
- File.unlink("#{uri.host}#{uri.path}")
316
- end
317
- end
318
-
319
-
320
364
  def generate_restart_data
321
365
  if dir = @options[:directory]
322
366
  @restart_dir = dir
@@ -383,12 +427,22 @@ module Puma
383
427
 
384
428
  begin
385
429
  Signal.trap "SIGTERM" do
386
- stop
430
+ graceful_stop
431
+
432
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
387
433
  end
388
434
  rescue Exception
389
435
  log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
390
436
  end
391
437
 
438
+ begin
439
+ Signal.trap "SIGINT" do
440
+ stop
441
+ end
442
+ rescue Exception
443
+ log "*** SIGINT not implemented, signal based gracefully stopping unavailable!"
444
+ end
445
+
392
446
  begin
393
447
  Signal.trap "SIGHUP" do
394
448
  if @runner.redirected_io?
@@ -401,13 +455,21 @@ module Puma
401
455
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
402
456
  end
403
457
 
404
- if Puma.jruby?
405
- Signal.trap("INT") do
406
- @status = :exit
407
- graceful_stop
408
- exit
458
+ begin
459
+ Signal.trap "SIGINFO" do
460
+ log_thread_status
409
461
  end
462
+ rescue Exception
463
+ # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
464
+ # to see this constantly on Linux.
410
465
  end
411
466
  end
467
+
468
+ def require_rubygems_min_version!(min_version, feature)
469
+ return if min_version <= Gem::Version.new(Gem::VERSION)
470
+
471
+ raise "#{feature} is not supported on your version of RubyGems. " \
472
+ "You must have RubyGems #{min_version}+ to use this feature."
473
+ end
412
474
  end
413
475
  end
@@ -1,3 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'io/wait'
5
+ rescue LoadError
6
+ end
7
+
1
8
  module Puma
2
9
  module MiniSSL
3
10
  class Socket
@@ -11,6 +18,10 @@ module Puma
11
18
  @socket
12
19
  end
13
20
 
21
+ def closed?
22
+ @socket.closed?
23
+ end
24
+
14
25
  def readpartial(size)
15
26
  while true
16
27
  output = @engine.read
@@ -43,7 +54,21 @@ module Puma
43
54
  output = engine_read_all
44
55
  return output if output
45
56
 
46
- data = @socket.read_nonblock(size)
57
+ data = @socket.read_nonblock(size, exception: false)
58
+ if data == :wait_readable || data == :wait_writable
59
+ # It would make more sense to let @socket.read_nonblock raise
60
+ # EAGAIN if necessary but it seems like it'll misbehave on Windows.
61
+ # I don't have a Windows machine to debug this so I can't explain
62
+ # exactly whats happening in that OS. Please let me know if you
63
+ # find out!
64
+ #
65
+ # In the meantime, we can emulate the correct behavior by
66
+ # capturing :wait_readable & :wait_writable and raising EAGAIN
67
+ # ourselves.
68
+ raise IO::EAGAINWaitReadable
69
+ elsif data.nil?
70
+ return nil
71
+ end
47
72
 
48
73
  @engine.inject(data)
49
74
  output = engine_read_all
@@ -57,6 +82,8 @@ module Puma
57
82
  end
58
83
 
59
84
  def write(data)
85
+ return 0 if data.empty?
86
+
60
87
  need = data.bytesize
61
88
 
62
89
  while true
@@ -96,35 +123,29 @@ module Puma
96
123
  @socket.flush
97
124
  end
98
125
 
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)
126
+ def read_and_drop(timeout = 1)
127
+ return :timeout unless IO.select([@socket], nil, nil, timeout)
128
+ return :eof unless read_nonblock(1024)
129
+ :drop
130
+ rescue Errno::EAGAIN
131
+ # do nothing
132
+ :eagain
133
+ end
121
134
 
122
- @engine.inject notify
123
- done = @engine.shutdown
135
+ def should_drop_bytes?
136
+ @engine.init? || !@engine.shutdown
137
+ end
124
138
 
125
- break if done
139
+ def close
140
+ begin
141
+ # Read any drop any partially initialized sockets and any received bytes during shutdown.
142
+ # Don't let this socket hold this loop forever.
143
+ # If it can't send more packets within 1s, then give up.
144
+ while should_drop_bytes?
145
+ return if [:timeout, :eof].include?(read_and_drop(1))
126
146
  end
127
147
  rescue IOError, SystemCallError
148
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
128
149
  # nothing
129
150
  ensure
130
151
  @socket.close
@@ -155,11 +176,18 @@ module Puma
155
176
 
156
177
  class Context
157
178
  attr_accessor :verify_mode
179
+ attr_reader :no_tlsv1, :no_tlsv1_1
180
+
181
+ def initialize
182
+ @no_tlsv1 = false
183
+ @no_tlsv1_1 = false
184
+ end
158
185
 
159
186
  if defined?(JRUBY_VERSION)
160
187
  # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
161
188
  attr_reader :keystore
162
189
  attr_accessor :keystore_pass
190
+ attr_accessor :ssl_cipher_list
163
191
 
164
192
  def keystore=(keystore)
165
193
  raise ArgumentError, "No such keystore file '#{keystore}'" unless File.exist? keystore
@@ -175,6 +203,7 @@ module Puma
175
203
  attr_reader :key
176
204
  attr_reader :cert
177
205
  attr_reader :ca
206
+ attr_accessor :ssl_cipher_filter
178
207
 
179
208
  def key=(key)
180
209
  raise ArgumentError, "No such key file '#{key}'" unless File.exist? key
@@ -196,6 +225,19 @@ module Puma
196
225
  raise "Cert not configured" unless @cert
197
226
  end
198
227
  end
228
+
229
+ # disables TLSv1
230
+ def no_tlsv1=(tlsv1)
231
+ raise ArgumentError, "Invalid value of no_tlsv1" unless ['true', 'false', true, false].include?(tlsv1)
232
+ @no_tlsv1 = tlsv1
233
+ end
234
+
235
+ # disables TLSv1 and TLSv1.1. Overrides `#no_tlsv1=`
236
+ def no_tlsv1_1=(tlsv1_1)
237
+ raise ArgumentError, "Invalid value of no_tlsv1" unless ['true', 'false', true, false].include?(tlsv1_1)
238
+ @no_tlsv1_1 = tlsv1_1
239
+ end
240
+
199
241
  end
200
242
 
201
243
  VERIFY_NONE = 0
@@ -229,7 +271,7 @@ module Puma
229
271
  end
230
272
 
231
273
  def close
232
- @socket.close
274
+ @socket.close unless @socket.closed? # closed? call is for Windows
233
275
  end
234
276
  end
235
277
  end