puma 4.3.1 → 5.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +94 -3
  3. data/LICENSE +23 -20
  4. data/README.md +26 -13
  5. data/docs/architecture.md +3 -3
  6. data/docs/deployment.md +9 -3
  7. data/docs/fork_worker.md +31 -0
  8. data/docs/jungle/README.md +13 -0
  9. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  10. data/{tools → docs}/jungle/rc.d/puma +0 -0
  11. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  12. data/{tools → docs}/jungle/upstart/README.md +0 -0
  13. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  14. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  15. data/docs/signals.md +7 -6
  16. data/docs/systemd.md +1 -63
  17. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  18. data/ext/puma_http11/extconf.rb +4 -3
  19. data/ext/puma_http11/http11_parser.c +3 -1
  20. data/ext/puma_http11/http11_parser.rl +3 -1
  21. data/ext/puma_http11/mini_ssl.c +15 -2
  22. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  23. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  24. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  25. data/ext/puma_http11/puma_http11.c +7 -38
  26. data/lib/puma.rb +17 -0
  27. data/lib/puma/app/status.rb +18 -3
  28. data/lib/puma/binder.rb +88 -68
  29. data/lib/puma/cli.rb +7 -15
  30. data/lib/puma/client.rb +67 -14
  31. data/lib/puma/cluster.rb +191 -74
  32. data/lib/puma/commonlogger.rb +2 -2
  33. data/lib/puma/configuration.rb +31 -42
  34. data/lib/puma/const.rb +4 -3
  35. data/lib/puma/control_cli.rb +29 -17
  36. data/lib/puma/detect.rb +17 -0
  37. data/lib/puma/dsl.rb +144 -70
  38. data/lib/puma/error_logger.rb +97 -0
  39. data/lib/puma/events.rb +35 -31
  40. data/lib/puma/io_buffer.rb +9 -2
  41. data/lib/puma/jruby_restart.rb +0 -58
  42. data/lib/puma/launcher.rb +49 -31
  43. data/lib/puma/minissl.rb +60 -18
  44. data/lib/puma/minissl/context_builder.rb +0 -3
  45. data/lib/puma/null_io.rb +1 -1
  46. data/lib/puma/plugin.rb +1 -10
  47. data/lib/puma/rack/builder.rb +0 -4
  48. data/lib/puma/reactor.rb +9 -4
  49. data/lib/puma/runner.rb +8 -36
  50. data/lib/puma/server.rb +149 -186
  51. data/lib/puma/single.rb +7 -64
  52. data/lib/puma/state_file.rb +6 -3
  53. data/lib/puma/thread_pool.rb +94 -49
  54. data/lib/rack/handler/puma.rb +1 -3
  55. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  56. metadata +21 -23
  57. data/docs/tcp_mode.md +0 -96
  58. data/ext/puma_http11/io_buffer.c +0 -155
  59. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  60. data/lib/puma/tcp_logger.rb +0 -41
  61. data/tools/jungle/README.md +0 -19
  62. data/tools/jungle/init.d/README.md +0 -61
  63. data/tools/jungle/init.d/puma +0 -421
  64. data/tools/jungle/init.d/run-puma +0 -18
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puma/const'
4
+
5
+ module Puma
6
+ # The implementation of a detailed error logging.
7
+ # @version 5.0.0
8
+ #
9
+ class ErrorLogger
10
+ include Const
11
+
12
+ attr_reader :ioerr
13
+
14
+ REQUEST_FORMAT = %{"%s %s%s" - (%s)}
15
+
16
+ def initialize(ioerr)
17
+ @ioerr = ioerr
18
+ @ioerr.sync = true
19
+
20
+ @debug = ENV.key? 'PUMA_DEBUG'
21
+ end
22
+
23
+ def self.stdio
24
+ new $stderr
25
+ end
26
+
27
+ # Print occured error details.
28
+ # +options+ hash with additional options:
29
+ # - +error+ is an exception object
30
+ # - +req+ the http request
31
+ # - +text+ (default nil) custom string to print in title
32
+ # and before all remaining info.
33
+ #
34
+ def info(options={})
35
+ ioerr.puts title(options)
36
+ end
37
+
38
+ # Print occured error details only if
39
+ # environment variable PUMA_DEBUG is defined.
40
+ # +options+ hash with additional options:
41
+ # - +error+ is an exception object
42
+ # - +req+ the http request
43
+ # - +text+ (default nil) custom string to print in title
44
+ # and before all remaining info.
45
+ #
46
+ def debug(options={})
47
+ return unless @debug
48
+
49
+ error = options[:error]
50
+ req = options[:req]
51
+
52
+ string_block = []
53
+ string_block << title(options)
54
+ string_block << request_dump(req) if req
55
+ string_block << error_backtrace(options) if error
56
+
57
+ ioerr.puts string_block.join("\n")
58
+ end
59
+
60
+ def title(options={})
61
+ text = options[:text]
62
+ req = options[:req]
63
+ error = options[:error]
64
+
65
+ string_block = ["#{Time.now}"]
66
+ string_block << " #{text}" if text
67
+ string_block << " (#{request_title(req)})" if request_parsed?(req)
68
+ string_block << ": #{error.inspect}" if error
69
+ string_block.join('')
70
+ end
71
+
72
+ def request_dump(req)
73
+ "Headers: #{request_headers(req)}\n" \
74
+ "Body: #{req.body}"
75
+ end
76
+
77
+ def request_title(req)
78
+ env = req.env
79
+
80
+ REQUEST_FORMAT % [
81
+ env[REQUEST_METHOD],
82
+ env[REQUEST_PATH] || env[PATH_INFO],
83
+ env[QUERY_STRING] || "",
84
+ env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR] || "-"
85
+ ]
86
+ end
87
+
88
+ def request_headers(req)
89
+ headers = req.env.select { |key, _| key.start_with?('HTTP_') }
90
+ headers.map { |key, value| [key[5..-1], value] }.to_h.inspect
91
+ end
92
+
93
+ def request_parsed?(req)
94
+ req && req.env[REQUEST_METHOD]
95
+ end
96
+ end
97
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/const'
4
3
  require "puma/null_io"
4
+ require 'puma/error_logger'
5
5
  require 'stringio'
6
6
 
7
7
  module Puma
@@ -23,8 +23,6 @@ module Puma
23
23
  end
24
24
  end
25
25
 
26
- include Const
27
-
28
26
  # Create an Events object that prints to +stdout+ and +stderr+.
29
27
  #
30
28
  def initialize(stdout, stderr)
@@ -36,6 +34,7 @@ module Puma
36
34
  @stderr.sync = true
37
35
 
38
36
  @debug = ENV.key? 'PUMA_DEBUG'
37
+ @error_logger = ErrorLogger.new(@stderr)
39
38
 
40
39
  @hooks = Hash.new { |h,k| h[k] = [] }
41
40
  end
@@ -66,7 +65,8 @@ module Puma
66
65
  # Write +str+ to +@stdout+
67
66
  #
68
67
  def log(str)
69
- @stdout.puts format(str)
68
+ @stdout.puts format(str) if @stdout.respond_to? :puts
69
+ rescue Errno::EPIPE
70
70
  end
71
71
 
72
72
  def write(str)
@@ -80,7 +80,7 @@ module Puma
80
80
  # Write +str+ to +@stderr+
81
81
  #
82
82
  def error(str)
83
- @stderr.puts format("ERROR: #{str}")
83
+ @error_logger.info(text: format("ERROR: #{str}"))
84
84
  exit 1
85
85
  end
86
86
 
@@ -88,43 +88,47 @@ module Puma
88
88
  formatter.call(str)
89
89
  end
90
90
 
91
+ # An HTTP connection error has occurred.
92
+ # +error+ a connection exception, +req+ the request,
93
+ # and +text+ additional info
94
+ # @version 5.0.0
95
+ #
96
+ def connection_error(error, req, text="HTTP connection error")
97
+ @error_logger.info(error: error, req: req, text: text)
98
+ end
99
+
91
100
  # An HTTP parse error has occurred.
92
- # +server+ is the Server object, +env+ the request, and +error+ a
93
- # parsing exception.
101
+ # +error+ a parsing exception,
102
+ # and +req+ the request.
94
103
  #
95
- def parse_error(server, env, error)
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"
104
+ def parse_error(error, req)
105
+ @error_logger.info(error: error, req: req, text: 'HTTP parse error, malformed request')
100
106
  end
101
107
 
102
108
  # An SSL error has occurred.
103
- # +server+ is the Server object, +peeraddr+ peer address, +peercert+
104
- # any peer certificate (if present), and +error+ an exception object.
109
+ # +error+ an exception object, +peeraddr+ peer address,
110
+ # and +peercert+ any peer certificate (if present).
105
111
  #
106
- def ssl_error(server, peeraddr, peercert, error)
112
+ def ssl_error(error, peeraddr, peercert)
107
113
  subject = peercert ? peercert.subject : nil
108
- @stderr.puts "#{Time.now}: SSL error, peer: #{peeraddr}, peer cert: #{subject}, #{error.inspect}"
114
+ @error_logger.info(error: error, text: "SSL error, peer: #{peeraddr}, peer cert: #{subject}")
109
115
  end
110
116
 
111
117
  # An unknown error has occurred.
112
- # +server+ is the Server object, +error+ an exception object,
113
- # +kind+ some additional info, and +env+ the request.
118
+ # +error+ an exception object, +req+ the request,
119
+ # and +text+ additional info
114
120
  #
115
- def unknown_error(server, error, kind="Unknown", env=nil)
116
- if error.respond_to? :render
117
- error.render "#{Time.now}: #{kind} error", @stderr
118
- else
119
- if env
120
- string_block = [ "#{Time.now}: #{kind} error handling request { #{env['REQUEST_METHOD']} #{env['PATH_INFO']} }" ]
121
- string_block << error.inspect
122
- else
123
- string_block = [ "#{Time.now}: #{kind} error: #{error.inspect}" ]
124
- end
125
- string_block << error.backtrace
126
- @stderr.puts string_block.join("\n")
127
- end
121
+ def unknown_error(error, req=nil, text="Unknown error")
122
+ @error_logger.info(error: error, req: req, text: text)
123
+ end
124
+
125
+ # Log occurred error debug dump.
126
+ # +error+ an exception object, +req+ the request,
127
+ # and +text+ additional info
128
+ # @version 5.0.0
129
+ #
130
+ def debug_error(error, req=nil, text="")
131
+ @error_logger.debug(error: error, req: req, text: text)
128
132
  end
129
133
 
130
134
  def on_booted(&block)
@@ -1,4 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/detect'
4
- require 'puma/puma_http11'
3
+ module Puma
4
+ class IOBuffer < String
5
+ def append(*args)
6
+ args.each { |a| concat(a) }
7
+ end
8
+
9
+ alias reset clear
10
+ end
11
+ end
@@ -22,63 +22,5 @@ module Puma
22
22
  execlp(cmd, *argv)
23
23
  raise SystemCallError.new(FFI.errno)
24
24
  end
25
-
26
- PermKey = 'PUMA_DAEMON_PERM'
27
- RestartKey = 'PUMA_DAEMON_RESTART'
28
-
29
- # Called to tell things "Your now always in daemon mode,
30
- # don't try to reenter it."
31
- #
32
- def self.perm_daemonize
33
- ENV[PermKey] = "1"
34
- end
35
-
36
- def self.daemon?
37
- ENV.key?(PermKey) || ENV.key?(RestartKey)
38
- end
39
-
40
- def self.daemon_init
41
- return true if ENV.key?(PermKey)
42
-
43
- return false unless ENV.key? RestartKey
44
-
45
- master = ENV[RestartKey]
46
-
47
- # In case the master disappears early
48
- begin
49
- Process.kill "SIGUSR2", master.to_i
50
- rescue SystemCallError => e
51
- end
52
-
53
- ENV[RestartKey] = ""
54
-
55
- setsid
56
-
57
- null = File.open "/dev/null", "w+"
58
- STDIN.reopen null
59
- STDOUT.reopen null
60
- STDERR.reopen null
61
-
62
- true
63
- end
64
-
65
- def self.daemon_start(dir, argv)
66
- ENV[RestartKey] = Process.pid.to_s
67
-
68
- if k = ENV['PUMA_JRUBY_DAEMON_OPTS']
69
- ENV['JRUBY_OPTS'] = k
70
- end
71
-
72
- cmd = argv.first
73
- argv = ([:string] * argv.size).zip(argv).flatten
74
- argv << :string
75
- argv << nil
76
-
77
- chdir(dir)
78
- ret = fork
79
- return ret if ret != 0
80
- execlp(cmd, *argv)
81
- raise SystemCallError.new(FFI.errno)
82
- end
83
25
  end
84
26
  end
@@ -47,8 +47,9 @@ module Puma
47
47
  @original_argv = @argv.dup
48
48
  @config = conf
49
49
 
50
- @binder = Binder.new(@events)
51
- @binder.import_from_env
50
+ @binder = Binder.new(@events, conf)
51
+ @binder.create_inherited_fds(ENV).each { |k| ENV.delete k }
52
+ @binder.create_activated_fds(ENV).each { |k| ENV.delete k }
52
53
 
53
54
  @environment = conf.environment
54
55
 
@@ -69,10 +70,6 @@ module Puma
69
70
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
70
71
  end
71
72
 
72
- if @options[:daemon] && Puma.windows?
73
- unsupported 'daemon mode not supported on Windows'
74
- end
75
-
76
73
  Dir.chdir(@restart_dir)
77
74
 
78
75
  prune_bundler if prune_bundler?
@@ -105,6 +102,7 @@ module Puma
105
102
  write_pid
106
103
 
107
104
  path = @options[:state]
105
+ permission = @options[:state_permission]
108
106
  return unless path
109
107
 
110
108
  require 'puma/state_file'
@@ -113,8 +111,9 @@ module Puma
113
111
  sf.pid = Process.pid
114
112
  sf.control_url = @options[:control_url]
115
113
  sf.control_auth_token = @options[:control_auth_token]
114
+ sf.running_from = File.expand_path('.')
116
115
 
117
- sf.save path
116
+ sf.save path, permission
118
117
  end
119
118
 
120
119
  # Delete the configured pidfile
@@ -174,22 +173,24 @@ module Puma
174
173
  case @status
175
174
  when :halt
176
175
  log "* Stopping immediately!"
176
+ @runner.stop_control
177
177
  when :run, :stop
178
178
  graceful_stop
179
179
  when :restart
180
180
  log "* Restarting..."
181
181
  ENV.replace(previous_env)
182
- @runner.before_restart
182
+ @runner.stop_control
183
183
  restart!
184
184
  when :exit
185
185
  # nothing
186
186
  end
187
- @binder.close_unix_paths
187
+ close_binder_listeners unless @status == :restart
188
188
  end
189
189
 
190
- # Return which tcp port the launcher is using, if it's using TCP
191
- def connected_port
192
- @binder.connected_port
190
+ # Return all tcp ports the launcher may be using, TCP or SSL
191
+ # @version 5.0.0
192
+ def connected_ports
193
+ @binder.connected_ports
193
194
  end
194
195
 
195
196
  def restart_args
@@ -202,9 +203,22 @@ module Puma
202
203
  end
203
204
 
204
205
  def close_binder_listeners
206
+ @runner.close_control_listeners
205
207
  @binder.close_listeners
206
208
  end
207
209
 
210
+ # @version 5.0.0
211
+ def thread_status
212
+ Thread.list.each do |thread|
213
+ name = "Thread: TID-#{thread.object_id.to_s(36)}"
214
+ name += " #{thread['label']}" if thread['label']
215
+ name += " #{thread.name}" if thread.respond_to?(:name) && thread.name
216
+ backtrace = thread.backtrace || ["<no backtrace available>"]
217
+
218
+ yield name, backtrace
219
+ end
220
+ end
221
+
208
222
  private
209
223
 
210
224
  # If configured, write the pid of the current process out
@@ -225,7 +239,7 @@ module Puma
225
239
  end
226
240
 
227
241
  def restart!
228
- @config.run_hooks :on_restart, self
242
+ @config.run_hooks :on_restart, self, @events
229
243
 
230
244
  if Puma.jruby?
231
245
  close_binder_listeners
@@ -241,6 +255,7 @@ module Puma
241
255
  else
242
256
  argv = restart_args
243
257
  Dir.chdir(@restart_dir)
258
+ ENV.update(@binder.redirects_for_restart_env)
244
259
  argv += [@binder.redirects_for_restart]
245
260
  Kernel.exec(*argv)
246
261
  end
@@ -275,6 +290,7 @@ module Puma
275
290
  end
276
291
 
277
292
  def prune_bundler
293
+ return if ENV['PUMA_BUNDLER_PRUNED']
278
294
  return unless defined?(Bundler)
279
295
  require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
280
296
  unless puma_wild_location
@@ -286,8 +302,10 @@ module Puma
286
302
 
287
303
  log '* Pruning Bundler environment'
288
304
  home = ENV['GEM_HOME']
289
- Bundler.with_clean_env do
305
+ bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
306
+ with_unbundled_env do
290
307
  ENV['GEM_HOME'] = home
308
+ ENV['BUNDLE_GEMFILE'] = bundle_gemfile
291
309
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
292
310
  args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':'), deps.join(',')] + @original_argv
293
311
  # Ruby 2.0+ defaults to true which breaks socket activation
@@ -323,21 +341,6 @@ module Puma
323
341
  log "- Goodbye!"
324
342
  end
325
343
 
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
-
341
344
  def set_process_title
342
345
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
343
346
  end
@@ -456,8 +459,13 @@ module Puma
456
459
  end
457
460
 
458
461
  begin
459
- Signal.trap "SIGINFO" do
460
- log_thread_status
462
+ unless Puma.jruby? # INFO in use by JVM already
463
+ Signal.trap "SIGINFO" do
464
+ thread_status do |name, backtrace|
465
+ @events.log name
466
+ @events.log backtrace.map { |bt| " #{bt}" }
467
+ end
468
+ end
461
469
  end
462
470
  rescue Exception
463
471
  # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
@@ -471,5 +479,15 @@ module Puma
471
479
  raise "#{feature} is not supported on your version of RubyGems. " \
472
480
  "You must have RubyGems #{min_version}+ to use this feature."
473
481
  end
482
+
483
+ # @version 5.0.0
484
+ def with_unbundled_env
485
+ bundler_ver = Gem::Version.new(Bundler::VERSION)
486
+ if bundler_ver < Gem::Version.new('2.1.0')
487
+ Bundler.with_clean_env { yield }
488
+ else
489
+ Bundler.with_unbundled_env { yield }
490
+ end
491
+ end
474
492
  end
475
493
  end