puma 4.1.0 → 4.2.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.

@@ -35,34 +35,10 @@ module Puma
35
35
  @workers.each { |x| x.term }
36
36
 
37
37
  begin
38
- if RUBY_VERSION < '2.6'
39
- @workers.each do |w|
40
- begin
41
- Process.waitpid(w.pid)
42
- rescue Errno::ECHILD
43
- # child is already terminated
44
- end
45
- end
46
- else
47
- # below code is for a bug in Ruby 2.6+, above waitpid call hangs
48
- t_st = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
- pids = @workers.map(&:pid)
50
- loop do
51
- pids.reject! do |w_pid|
52
- begin
53
- if Process.waitpid(w_pid, Process::WNOHANG)
54
- log " worker status: #{$?}"
55
- true
56
- end
57
- rescue Errno::ECHILD
58
- true # child is already terminated
59
- end
60
- end
61
- break if pids.empty?
62
- sleep 0.5
63
- end
64
- t_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
- log format(" worker shutdown time: %6.2f", t_end - t_st)
38
+ loop do
39
+ wait_workers
40
+ break if @workers.empty?
41
+ sleep 0.2
66
42
  end
67
43
  rescue Interrupt
68
44
  log "! Cancelled waiting for workers"
@@ -98,7 +74,7 @@ module Puma
98
74
  @started_at = Time.now
99
75
  @last_checkin = Time.now
100
76
  @last_status = '{}'
101
- @dead = false
77
+ @term = false
102
78
  end
103
79
 
104
80
  attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
@@ -112,12 +88,8 @@ module Puma
112
88
  @stage = :booted
113
89
  end
114
90
 
115
- def dead?
116
- @dead
117
- end
118
-
119
- def dead!
120
- @dead = true
91
+ def term?
92
+ @term
121
93
  end
122
94
 
123
95
  def ping!(status)
@@ -134,9 +106,9 @@ module Puma
134
106
  if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
135
107
  @signal = "KILL"
136
108
  else
109
+ @term ||= true
137
110
  @first_term_sent ||= Time.now
138
111
  end
139
-
140
112
  Process.kill @signal, @pid
141
113
  rescue Errno::ESRCH
142
114
  end
@@ -227,12 +199,7 @@ module Puma
227
199
  # during this loop by giving the kernel time to kill them.
228
200
  sleep 1 if any
229
201
 
230
- pids = []
231
- while pid = Process.waitpid(-1, Process::WNOHANG) do
232
- pids << pid
233
- end
234
- @workers.reject! { |w| w.dead? || pids.include?(w.pid) }
235
-
202
+ wait_workers
236
203
  cull_workers
237
204
  spawn_workers
238
205
 
@@ -249,8 +216,10 @@ module Puma
249
216
  log "- Stopping #{w.pid} for phased upgrade..."
250
217
  end
251
218
 
252
- w.term
253
- log "- #{w.signal} sent to #{w.pid}..."
219
+ unless w.term?
220
+ w.term
221
+ log "- #{w.signal} sent to #{w.pid}..."
222
+ end
254
223
  end
255
224
  end
256
225
  end
@@ -277,6 +246,7 @@ module Puma
277
246
  @suicide_pipe.close
278
247
 
279
248
  Thread.new do
249
+ Puma.set_thread_name "worker check pipe"
280
250
  IO.select [@check_pipe]
281
251
  log "! Detected parent died, dying"
282
252
  exit! 1
@@ -299,6 +269,7 @@ module Puma
299
269
  server = start_server
300
270
 
301
271
  Signal.trap "SIGTERM" do
272
+ @worker_write << "e#{Process.pid}\n" rescue nil
302
273
  server.stop
303
274
  end
304
275
 
@@ -311,6 +282,7 @@ module Puma
311
282
  end
312
283
 
313
284
  Thread.new(@worker_write) do |io|
285
+ Puma.set_thread_name "stat payload"
314
286
  base_payload = "p#{Process.pid}"
315
287
 
316
288
  while true
@@ -376,6 +348,8 @@ module Puma
376
348
  Dir.chdir dir
377
349
  end
378
350
 
351
+ # Inside of a child process, this will return all zeroes, as @workers is only populated in
352
+ # the master process.
379
353
  def stats
380
354
  old_worker_count = @workers.count { |w| w.phase != @phase }
381
355
  booted_worker_count = @workers.count { |w| w.booted? }
@@ -530,8 +504,11 @@ module Puma
530
504
  w.boot!
531
505
  log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
532
506
  force_check = true
507
+ when "e"
508
+ # external term, see worker method, Signal.trap "SIGTERM"
509
+ w.instance_variable_set :@term, true
533
510
  when "t"
534
- w.dead!
511
+ w.term unless w.term?
535
512
  force_check = true
536
513
  when "p"
537
514
  w.ping!(result.sub(/^\d+/,'').chomp)
@@ -554,5 +531,24 @@ module Puma
554
531
  @wakeup.close
555
532
  end
556
533
  end
534
+
535
+ private
536
+
537
+ # loops thru @workers, removing workers that exited, and calling
538
+ # `#term` if needed
539
+ def wait_workers
540
+ @workers.reject! do |w|
541
+ begin
542
+ if Process.wait(w.pid, Process::WNOHANG)
543
+ true
544
+ else
545
+ w.term if w.term?
546
+ nil
547
+ end
548
+ rescue Errno::ECHILD
549
+ true # child is already terminated
550
+ end
551
+ end
552
+ end
557
553
  end
558
554
  end
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "4.1.0".freeze
104
- CODE_NAME = "Fourth and One".freeze
103
+ PUMA_VERSION = VERSION = "4.2.1".freeze
104
+ CODE_NAME = "Distant Airhorns".freeze
105
105
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
106
106
 
107
107
  FAST_TRACK_KA_TIMEOUT = 0.2
@@ -122,27 +122,24 @@ module Puma
122
122
  REQUEST_URI= 'REQUEST_URI'.freeze
123
123
  REQUEST_PATH = 'REQUEST_PATH'.freeze
124
124
  QUERY_STRING = 'QUERY_STRING'.freeze
125
+ CONTENT_LENGTH = "CONTENT_LENGTH".freeze
125
126
 
126
127
  PATH_INFO = 'PATH_INFO'.freeze
127
128
 
128
129
  PUMA_TMP_BASE = "puma".freeze
129
130
 
130
- # Indicate that we couldn't parse the request
131
- ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n".freeze
132
-
133
- # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
134
- ERROR_404_RESPONSE = "HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\nNOT FOUND".freeze
135
-
136
- # The standard empty 408 response for requests that timed out.
137
- ERROR_408_RESPONSE = "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze
138
-
139
- CONTENT_LENGTH = "CONTENT_LENGTH".freeze
140
-
141
- # Indicate that there was an internal error, obviously.
142
- ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze
143
-
144
- # A common header for indicating the server is too busy. Not used yet.
145
- ERROR_503_RESPONSE = "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
131
+ ERROR_RESPONSE = {
132
+ # Indicate that we couldn't parse the request
133
+ 400 => "HTTP/1.1 400 Bad Request\r\n\r\n".freeze,
134
+ # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
135
+ 404 => "HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\nNOT FOUND".freeze,
136
+ # The standard empty 408 response for requests that timed out.
137
+ 408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze,
138
+ # Indicate that there was an internal error, obviously.
139
+ 500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze,
140
+ # A common header for indicating the server is too busy. Not used yet.
141
+ 503 => "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
142
+ }
146
143
 
147
144
  # The basic max request size we'll try to read.
148
145
  CHUNK_SIZE = 16 * 1024
@@ -22,6 +22,7 @@ module Puma
22
22
  @control_auth_token = nil
23
23
  @config_file = nil
24
24
  @command = nil
25
+ @environment = ENV['RACK_ENV'] || "development"
25
26
 
26
27
  @argv = argv.dup
27
28
  @stdout = stdout
@@ -59,6 +60,11 @@ module Puma
59
60
  @config_file = arg
60
61
  end
61
62
 
63
+ o.on "-e", "--environment ENVIRONMENT",
64
+ "The environment to run the Rack app on (default development)" do |arg|
65
+ @environment = arg
66
+ end
67
+
62
68
  o.on_tail("-H", "--help", "Show this message") do
63
69
  @stdout.puts o
64
70
  exit
@@ -76,8 +82,10 @@ module Puma
76
82
  @command = argv.shift
77
83
 
78
84
  unless @config_file == '-'
79
- if @config_file.nil? and File.exist?('config/puma.rb')
80
- @config_file = 'config/puma.rb'
85
+ if @config_file.nil?
86
+ @config_file = %W(config/puma/#{@environment}.rb config/puma.rb).find do |f|
87
+ File.exist?(f)
88
+ end
81
89
  end
82
90
 
83
91
  if @config_file
@@ -258,6 +266,7 @@ module Puma
258
266
  run_args += ["--control-url", @control_url] if @control_url
259
267
  run_args += ["--control-token", @control_auth_token] if @control_auth_token
260
268
  run_args += ["-C", @config_file] if @config_file
269
+ run_args += ["-e", @environment] if @environment
261
270
 
262
271
  events = Puma::Events.new @stdout, @stderr
263
272
 
@@ -396,7 +396,7 @@ module Puma
396
396
  # keystore_pass: password
397
397
  # }
398
398
  def ssl_bind(host, port, opts)
399
- verify = opts.fetch(:verify_mode, 'none')
399
+ verify = opts.fetch(:verify_mode, 'none').to_s
400
400
  no_tlsv1 = opts.fetch(:no_tlsv1, 'false')
401
401
  no_tlsv1_1 = opts.fetch(:no_tlsv1_1, 'false')
402
402
  ca_additions = "&ca=#{opts[:ca]}" if ['peer', 'force_peer'].include?(verify)
@@ -584,6 +584,7 @@ module Puma
584
584
  # dictates.
585
585
  #
586
586
  # @note This is incompatible with +preload_app!+.
587
+ # @note This is only supported for RubyGems 2.2+
587
588
  def prune_bundler(answer=true)
588
589
  @options[:prune_bundler] = answer
589
590
  end
@@ -601,6 +602,21 @@ module Puma
601
602
  @options[:raise_exception_on_sigterm] = answer
602
603
  end
603
604
 
605
+ # When using prune_bundler, if extra runtime dependencies need to be loaded to
606
+ # initialize your app, then this setting can be used.
607
+ #
608
+ # Before bundler is pruned, the gem names supplied will be looked up in the bundler
609
+ # context and then loaded again after bundler is pruned.
610
+ # Only applies if prune_bundler is used.
611
+ #
612
+ # @example
613
+ # extra_runtime_dependencies ['gem_name_1', 'gem_name_2']
614
+ # @example
615
+ # extra_runtime_dependencies ['puma_worker_killer']
616
+ def extra_runtime_dependencies(answer = [])
617
+ @options[:extra_runtime_dependencies] = Array(answer)
618
+ end
619
+
604
620
  # Additional text to display in process listing.
605
621
  #
606
622
  # If you do not specify a tag, Puma will infer it. If you do not want Puma
@@ -29,8 +29,8 @@ module Puma
29
29
  #
30
30
  def initialize(stdout, stderr)
31
31
  @formatter = DefaultFormatter.new
32
- @stdout = stdout.dup
33
- @stderr = stderr.dup
32
+ @stdout = stdout
33
+ @stderr = stderr
34
34
 
35
35
  @stdout.sync = true
36
36
  @stderr.sync = true
@@ -2,12 +2,9 @@
2
2
 
3
3
  require 'puma/events'
4
4
  require 'puma/detect'
5
-
6
5
  require 'puma/cluster'
7
6
  require 'puma/single'
8
-
9
7
  require 'puma/const'
10
-
11
8
  require 'puma/binder'
12
9
 
13
10
  module Puma
@@ -126,19 +123,6 @@ module Puma
126
123
  File.unlink(path) if path && File.exist?(path)
127
124
  end
128
125
 
129
- # If configured, write the pid of the current process out
130
- # to a file.
131
- def write_pid
132
- path = @options[:pidfile]
133
- return unless path
134
-
135
- File.open(path, 'w') { |f| f.puts Process.pid }
136
- cur = Process.pid
137
- at_exit do
138
- delete_pidfile if cur == Process.pid
139
- end
140
- end
141
-
142
126
  # Begin async shutdown of the server
143
127
  def halt
144
128
  @status = :halt
@@ -200,6 +184,7 @@ module Puma
200
184
  when :exit
201
185
  # nothing
202
186
  end
187
+ @binder.close_unix_paths
203
188
  end
204
189
 
205
190
  # Return which tcp port the launcher is using, if it's using TCP
@@ -217,16 +202,24 @@ module Puma
217
202
  end
218
203
 
219
204
  def close_binder_listeners
220
- @binder.listeners.each do |l, io|
221
- io.close
222
- uri = URI.parse(l)
223
- next unless uri.scheme == 'unix'
224
- File.unlink("#{uri.host}#{uri.path}")
225
- end
205
+ @binder.close_listeners
226
206
  end
227
207
 
228
208
  private
229
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
+
230
223
  def reload_worker_directory
231
224
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
232
225
  end
@@ -246,48 +239,71 @@ module Puma
246
239
  Dir.chdir(@restart_dir)
247
240
  Kernel.exec(*argv)
248
241
  else
249
- redirects = {:close_others => true}
250
- @binder.listeners.each_with_index do |(l, io), i|
251
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
252
- redirects[io.to_i] = io.to_i
253
- end
254
-
255
242
  argv = restart_args
256
243
  Dir.chdir(@restart_dir)
257
- argv += [redirects]
244
+ argv += [@binder.redirects_for_restart]
258
245
  Kernel.exec(*argv)
259
246
  end
260
247
  end
261
248
 
262
- def prune_bundler
263
- return unless defined?(Bundler)
264
- puma = Bundler.rubygems.loaded_specs("puma")
265
- 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)
266
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
267
276
 
268
- 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
269
281
  log "! Unable to prune Bundler environment, continuing"
270
282
  return
271
283
  end
272
284
 
273
- deps = puma.runtime_dependencies.map do |d|
274
- spec = Bundler.rubygems.loaded_specs(d.name)
275
- "#{d.name}:#{spec.version.to_s}"
276
- end
285
+ deps, dirs = dependencies_and_files_to_require_after_prune
277
286
 
278
287
  log '* Pruning Bundler environment'
279
288
  home = ENV['GEM_HOME']
280
289
  Bundler.with_clean_env do
281
290
  ENV['GEM_HOME'] = home
282
291
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
283
- wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
284
- 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
285
293
  # Ruby 2.0+ defaults to true which breaks socket activation
286
294
  args += [{:close_others => false}]
287
295
  Kernel.exec(*args)
288
296
  end
289
297
  end
290
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
+
291
307
  def log(str)
292
308
  @events.log str
293
309
  end
@@ -307,6 +323,21 @@ module Puma
307
323
  log "- Goodbye!"
308
324
  end
309
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
+
310
341
  def set_process_title
311
342
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
312
343
  end
@@ -406,12 +437,6 @@ module Puma
406
437
 
407
438
  begin
408
439
  Signal.trap "SIGINT" do
409
- if Puma.jruby?
410
- @status = :exit
411
- graceful_stop
412
- exit
413
- end
414
-
415
440
  stop
416
441
  end
417
442
  rescue Exception
@@ -429,6 +454,22 @@ module Puma
429
454
  rescue Exception
430
455
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
431
456
  end
457
+
458
+ begin
459
+ Signal.trap "SIGINFO" do
460
+ log_thread_status
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.
465
+ end
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."
432
473
  end
433
474
  end
434
475
  end