puma 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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)
@@ -552,6 +529,26 @@ module Puma
552
529
  @suicide_pipe.close
553
530
  read.close
554
531
  @wakeup.close
532
+ @launcher.close_binder_unix_paths
533
+ end
534
+ end
535
+
536
+ private
537
+
538
+ # loops thru @workers, removing workers that exited, and calling
539
+ # `#term` if needed
540
+ def wait_workers
541
+ @workers.reject! do |w|
542
+ begin
543
+ if Process.wait(w.pid, Process::WNOHANG)
544
+ true
545
+ else
546
+ w.term if w.term?
547
+ nil
548
+ end
549
+ rescue Errno::ECHILD
550
+ true # child is already terminated
551
+ end
555
552
  end
556
553
  end
557
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.0".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
 
@@ -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
@@ -217,16 +201,28 @@ module Puma
217
201
  end
218
202
 
219
203
  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
204
+ @binder.close_listeners
205
+ end
206
+
207
+ def close_binder_unix_paths
208
+ @binder.close_unix_paths
226
209
  end
227
210
 
228
211
  private
229
212
 
213
+ # If configured, write the pid of the current process out
214
+ # to a file.
215
+ def write_pid
216
+ path = @options[:pidfile]
217
+ return unless path
218
+
219
+ File.open(path, 'w') { |f| f.puts Process.pid }
220
+ cur = Process.pid
221
+ at_exit do
222
+ delete_pidfile if cur == Process.pid
223
+ end
224
+ end
225
+
230
226
  def reload_worker_directory
231
227
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
232
228
  end
@@ -246,48 +242,71 @@ module Puma
246
242
  Dir.chdir(@restart_dir)
247
243
  Kernel.exec(*argv)
248
244
  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
245
  argv = restart_args
256
246
  Dir.chdir(@restart_dir)
257
- argv += [redirects]
247
+ argv += [@binder.redirects_for_restart]
258
248
  Kernel.exec(*argv)
259
249
  end
260
250
  end
261
251
 
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) }
252
+ def dependencies_and_files_to_require_after_prune
253
+ puma = spec_for_gem("puma")
254
+
255
+ deps = puma.runtime_dependencies.map do |d|
256
+ "#{d.name}:#{spec_for_gem(d.name).version}"
257
+ end
258
+
259
+ [deps, require_paths_for_gem(puma) + extra_runtime_deps_directories]
260
+ end
261
+
262
+ def extra_runtime_deps_directories
263
+ Array(@options[:extra_runtime_dependencies]).map do |d_name|
264
+ if (spec = spec_for_gem(d_name))
265
+ require_paths_for_gem(spec)
266
+ else
267
+ log "* Could not load extra dependency: #{d_name}"
268
+ nil
269
+ end
270
+ end.flatten.compact
271
+ end
272
+
273
+ def puma_wild_location
274
+ puma = spec_for_gem("puma")
275
+ dirs = require_paths_for_gem(puma)
266
276
  puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
277
+ File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
278
+ end
267
279
 
268
- unless puma_lib_dir
280
+ def prune_bundler
281
+ return unless defined?(Bundler)
282
+ require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
283
+ unless puma_wild_location
269
284
  log "! Unable to prune Bundler environment, continuing"
270
285
  return
271
286
  end
272
287
 
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
288
+ deps, dirs = dependencies_and_files_to_require_after_prune
277
289
 
278
290
  log '* Pruning Bundler environment'
279
291
  home = ENV['GEM_HOME']
280
292
  Bundler.with_clean_env do
281
293
  ENV['GEM_HOME'] = home
282
294
  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
295
+ args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':'), deps.join(',')] + @original_argv
285
296
  # Ruby 2.0+ defaults to true which breaks socket activation
286
297
  args += [{:close_others => false}]
287
298
  Kernel.exec(*args)
288
299
  end
289
300
  end
290
301
 
302
+ def spec_for_gem(gem_name)
303
+ Bundler.rubygems.loaded_specs(gem_name)
304
+ end
305
+
306
+ def require_paths_for_gem(gem_spec)
307
+ gem_spec.full_require_paths
308
+ end
309
+
291
310
  def log(str)
292
311
  @events.log str
293
312
  end
@@ -307,6 +326,21 @@ module Puma
307
326
  log "- Goodbye!"
308
327
  end
309
328
 
329
+ def log_thread_status
330
+ Thread.list.each do |thread|
331
+ log "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
332
+ logstr = "Thread: TID-#{thread.object_id.to_s(36)}"
333
+ logstr += " #{thread.name}" if thread.respond_to?(:name)
334
+ log logstr
335
+
336
+ if thread.backtrace
337
+ log thread.backtrace.join("\n")
338
+ else
339
+ log "<no backtrace available>"
340
+ end
341
+ end
342
+ end
343
+
310
344
  def set_process_title
311
345
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
312
346
  end
@@ -406,12 +440,6 @@ module Puma
406
440
 
407
441
  begin
408
442
  Signal.trap "SIGINT" do
409
- if Puma.jruby?
410
- @status = :exit
411
- graceful_stop
412
- exit
413
- end
414
-
415
443
  stop
416
444
  end
417
445
  rescue Exception
@@ -429,6 +457,22 @@ module Puma
429
457
  rescue Exception
430
458
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
431
459
  end
460
+
461
+ begin
462
+ Signal.trap "SIGINFO" do
463
+ log_thread_status
464
+ end
465
+ rescue Exception
466
+ # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
467
+ # to see this constantly on Linux.
468
+ end
469
+ end
470
+
471
+ def require_rubygems_min_version!(min_version, feature)
472
+ return if min_version <= Gem::Version.new(Gem::VERSION)
473
+
474
+ raise "#{feature} is not supported on your version of RubyGems. " \
475
+ "You must have RubyGems #{min_version}+ to use this feature."
432
476
  end
433
477
  end
434
478
  end