puma 5.0.2 → 5.0.3

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.

@@ -7,11 +7,16 @@ module Puma
7
7
  class Status
8
8
  OK_STATUS = '{ "status": "ok" }'.freeze
9
9
 
10
- def initialize(cli, token = nil)
11
- @cli = cli
10
+ # @param launcher [::Puma::Launcher]
11
+ # @param token [String, nil] the token used for authentication
12
+ #
13
+ def initialize(launcher, token = nil)
14
+ @launcher = launcher
12
15
  @auth_token = token
13
16
  end
14
17
 
18
+ # most commands call methods in `::Puma::Launcher` based on command in
19
+ # `env['PATH_INFO']`
15
20
  def call(env)
16
21
  unless authenticate(env)
17
22
  return rack_response(403, 'Invalid auth token', 'text/plain')
@@ -21,57 +26,53 @@ module Puma
21
26
  require 'json'
22
27
  end
23
28
 
24
- case env['PATH_INFO']
25
- when /\/stop$/
26
- @cli.stop
27
- rack_response(200, OK_STATUS)
29
+ # resp_type is processed by following case statement, return
30
+ # is a number (status) or a string used as the body of a 200 response
31
+ resp_type =
32
+ case env['PATH_INFO'][/\/([^\/]+)$/, 1]
33
+ when 'stop'
34
+ @launcher.stop ; 200
28
35
 
29
- when /\/halt$/
30
- @cli.halt
31
- rack_response(200, OK_STATUS)
36
+ when 'halt'
37
+ @launcher.halt ; 200
32
38
 
33
- when /\/restart$/
34
- @cli.restart
35
- rack_response(200, OK_STATUS)
39
+ when 'restart'
40
+ @launcher.restart ; 200
36
41
 
37
- when /\/phased-restart$/
38
- if !@cli.phased_restart
39
- rack_response(404, '{ "error": "phased restart not available" }')
40
- else
41
- rack_response(200, OK_STATUS)
42
- end
43
-
44
- when /\/reload-worker-directory$/
45
- if !@cli.send(:reload_worker_directory)
46
- rack_response(404, '{ "error": "reload_worker_directory not available" }')
47
- else
48
- rack_response(200, OK_STATUS)
49
- end
42
+ when 'phased-restart'
43
+ @launcher.phased_restart ? 200 : 404
50
44
 
51
- when /\/gc$/
52
- GC.start
53
- rack_response(200, OK_STATUS)
45
+ when 'reload-worker-directory'
46
+ @launcher.send(:reload_worker_directory) ? 200 : 404
54
47
 
55
- when /\/gc-stats$/
56
- rack_response(200, GC.stat.to_json)
48
+ when 'gc'
49
+ GC.start ; 200
57
50
 
58
- when /\/stats$/
59
- rack_response(200, @cli.stats.to_json)
51
+ when 'gc-stats'
52
+ GC.stat.to_json
60
53
 
61
- when /\/thread-backtraces$/
62
- backtraces = []
63
- @cli.thread_status do |name, backtrace|
64
- backtraces << { name: name, backtrace: backtrace }
65
- end
54
+ when 'stats'
55
+ @launcher.stats.to_json
66
56
 
67
- rack_response(200, backtraces.to_json)
57
+ when 'thread-backtraces'
58
+ backtraces = []
59
+ @launcher.thread_status do |name, backtrace|
60
+ backtraces << { name: name, backtrace: backtrace }
61
+ end
62
+ backtraces.to_json
68
63
 
69
- when /\/refork$/
70
- Process.kill "SIGURG", $$
71
- rack_response(200, OK_STATUS)
64
+ else
65
+ return rack_response(404, "Unsupported action", 'text/plain')
66
+ end
72
67
 
73
- else
74
- rack_response 404, "Unsupported action", 'text/plain'
68
+ case resp_type
69
+ when String
70
+ rack_response 200, resp_type
71
+ when 200
72
+ rack_response 200, OK_STATUS
73
+ when 404
74
+ str = env['PATH_INFO'][/\/(\S+)/, 1].tr '-', '_'
75
+ rack_response 404, "{ \"error\": \"#{str} not available\" }"
75
76
  end
76
77
  end
77
78
 
@@ -12,7 +12,15 @@ module Puma
12
12
  if HAS_SSL
13
13
  require 'puma/minissl'
14
14
  require 'puma/minissl/context_builder'
15
- require 'puma/accept_nonblock'
15
+
16
+ # Odd bug in 'pure Ruby' nio4r verion 2.5.2, which installs with Ruby 2.3.
17
+ # NIO doesn't create any OpenSSL objects, but it rescues an OpenSSL error.
18
+ # The bug was that it did not require openssl.
19
+ # @todo remove when Ruby 2.3 support is dropped
20
+ #
21
+ if windows? && RbConfig::CONFIG['ruby_version'] == '2.3.0'
22
+ require 'openssl'
23
+ end
16
24
  end
17
25
 
18
26
  class Binder
@@ -85,6 +85,12 @@ module Puma
85
85
 
86
86
  def_delegators :@io, :closed?
87
87
 
88
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
89
+ # used for MiniSSL::Socket
90
+ def io_ok?
91
+ @to_io.is_a?(::BasicSocket) && !closed?
92
+ end
93
+
88
94
  # @!attribute [r] inspect
89
95
  def inspect
90
96
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
@@ -103,7 +109,12 @@ module Puma
103
109
  end
104
110
 
105
111
  def set_timeout(val)
106
- @timeout_at = Time.now + val
112
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
113
+ end
114
+
115
+ # Number of seconds until the timeout elapses.
116
+ def timeout
117
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
107
118
  end
108
119
 
109
120
  def reset(fast_check=true)
@@ -188,79 +199,20 @@ module Puma
188
199
  false
189
200
  end
190
201
 
191
- if IS_JRUBY
192
- def jruby_start_try_to_finish
193
- return read_body unless @read_header
194
-
195
- begin
196
- data = @io.sysread_nonblock(CHUNK_SIZE)
197
- rescue OpenSSL::SSL::SSLError => e
198
- return false if e.kind_of? IO::WaitReadable
199
- raise e
200
- end
201
-
202
- # No data means a closed socket
203
- unless data
204
- @buffer = nil
205
- set_ready
206
- raise EOFError
207
- end
208
-
209
- if @buffer
210
- @buffer << data
211
- else
212
- @buffer = data
213
- end
214
-
215
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
216
-
217
- if @parser.finished?
218
- return setup_body
219
- elsif @parsed_bytes >= MAX_HEADER
220
- raise HttpParserError,
221
- "HEADER is longer than allowed, aborting client early."
222
- end
223
-
224
- false
225
- end
226
-
227
- def eagerly_finish
228
- return true if @ready
229
-
230
- if @io.kind_of? OpenSSL::SSL::SSLSocket
231
- return true if jruby_start_try_to_finish
232
- end
233
-
234
- return false unless IO.select([@to_io], nil, nil, 0)
235
- try_to_finish
236
- end
237
-
238
- else
239
-
240
- def eagerly_finish
241
- return true if @ready
242
- return false unless IO.select([@to_io], nil, nil, 0)
243
- try_to_finish
244
- end
245
-
246
- # For documentation, see https://github.com/puma/puma/issues/1754
247
- send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
248
- end # IS_JRUBY
202
+ def eagerly_finish
203
+ return true if @ready
204
+ return false unless IO.select([@to_io], nil, nil, 0)
205
+ try_to_finish
206
+ end
249
207
 
250
208
  def finish(timeout)
251
- return true if @ready
252
- until try_to_finish
253
- can_read = begin
254
- IO.select([@to_io], nil, nil, timeout)
255
- rescue ThreadPool::ForceShutdown
256
- nil
257
- end
258
- unless can_read
259
- write_error(408) if in_data_phase
260
- raise ConnectionError
261
- end
262
- end
263
- true
209
+ return if @ready
210
+ IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
211
+ end
212
+
213
+ def timeout!
214
+ write_error(408) if in_data_phase
215
+ raise ConnectionError
264
216
  end
265
217
 
266
218
  def write_error(status_code)
@@ -3,6 +3,8 @@
3
3
  require 'puma/runner'
4
4
  require 'puma/util'
5
5
  require 'puma/plugin'
6
+ require 'puma/cluster/worker_handle'
7
+ require 'puma/cluster/worker'
6
8
 
7
9
  require 'time'
8
10
 
@@ -11,10 +13,6 @@ module Puma
11
13
  # to boot and serve a Ruby application when puma "workers" are needed
12
14
  # i.e. when using multi-processes. For example `$ puma -w 5`
13
15
  #
14
- # At the core of this class is running an instance of `Puma::Server` which
15
- # gets created via the `start_server` method from the `Puma::Runner` class
16
- # that this inherits from.
17
- #
18
16
  # An instance of this class will spawn the number of processes passed in
19
17
  # via the `spawn_workers` method call. Each worker will have it's own
20
18
  # instance of a `Puma::Server`.
@@ -61,79 +59,6 @@ module Puma
61
59
  @workers.each { |x| x.hup }
62
60
  end
63
61
 
64
- class Worker
65
- def initialize(idx, pid, phase, options)
66
- @index = idx
67
- @pid = pid
68
- @phase = phase
69
- @stage = :started
70
- @signal = "TERM"
71
- @options = options
72
- @first_term_sent = nil
73
- @started_at = Time.now
74
- @last_checkin = Time.now
75
- @last_status = {}
76
- @term = false
77
- end
78
-
79
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
80
-
81
- # @version 5.0.0
82
- attr_writer :pid, :phase
83
-
84
- def booted?
85
- @stage == :booted
86
- end
87
-
88
- def boot!
89
- @last_checkin = Time.now
90
- @stage = :booted
91
- end
92
-
93
- def term?
94
- @term
95
- end
96
-
97
- def ping!(status)
98
- @last_checkin = Time.now
99
- require 'json'
100
- @last_status = JSON.parse(status, symbolize_names: true)
101
- end
102
-
103
- # @see Puma::Cluster#check_workers
104
- # @version 5.0.0
105
- def ping_timeout
106
- @last_checkin +
107
- (booted? ?
108
- @options[:worker_timeout] :
109
- @options[:worker_boot_timeout]
110
- )
111
- end
112
-
113
- def term
114
- begin
115
- if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
116
- @signal = "KILL"
117
- else
118
- @term ||= true
119
- @first_term_sent ||= Time.now
120
- end
121
- Process.kill @signal, @pid if @pid
122
- rescue Errno::ESRCH
123
- end
124
- end
125
-
126
- def kill
127
- @signal = 'KILL'
128
- term
129
- end
130
-
131
- def hup
132
- Process.kill "HUP", @pid
133
- rescue Errno::ESRCH
134
- end
135
- end
136
-
137
62
  def spawn_workers
138
63
  diff = @options[:workers] - @workers.size
139
64
  return if diff < 1
@@ -154,7 +79,7 @@ module Puma
154
79
  end
155
80
 
156
81
  debug "Spawned worker: #{pid}"
157
- @workers << Worker.new(idx, pid, @phase, @options)
82
+ @workers << WorkerHandle.new(idx, pid, @phase, @options)
158
83
  end
159
84
 
160
85
  if @options[:fork_worker] &&
@@ -249,113 +174,23 @@ module Puma
249
174
  end
250
175
 
251
176
  def worker(index, master)
252
- title = "puma: cluster worker #{index}: #{master}"
253
- title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
254
- $0 = title
255
-
256
- Signal.trap "SIGINT", "IGNORE"
257
- Signal.trap "SIGCHLD", "DEFAULT"
258
-
259
- fork_worker = @options[:fork_worker] && index == 0
260
-
261
177
  @workers = []
262
- if !@options[:fork_worker] || fork_worker
263
- @master_read.close
264
- @suicide_pipe.close
265
- @fork_writer.close
266
- end
267
-
268
- Thread.new do
269
- Puma.set_thread_name "worker check pipe"
270
- IO.select [@check_pipe]
271
- log "! Detected parent died, dying"
272
- exit! 1
273
- end
274
178
 
275
- # If we're not running under a Bundler context, then
276
- # report the info about the context we will be using
277
- if !ENV['BUNDLE_GEMFILE']
278
- if File.exist?("Gemfile")
279
- log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
280
- elsif File.exist?("gems.rb")
281
- log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
282
- end
283
- end
284
-
285
- # Invoke any worker boot hooks so they can get
286
- # things in shape before booting the app.
287
- @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
288
-
289
- server = @server ||= start_server
290
- restart_server = Queue.new << true << false
291
-
292
- if fork_worker
293
- restart_server.clear
294
- worker_pids = []
295
- Signal.trap "SIGCHLD" do
296
- wakeup! if worker_pids.reject! do |p|
297
- Process.wait(p, Process::WNOHANG) rescue true
298
- end
299
- end
300
-
301
- Thread.new do
302
- Puma.set_thread_name "worker fork pipe"
303
- while (idx = @fork_pipe.gets)
304
- idx = idx.to_i
305
- if idx == -1 # stop server
306
- if restart_server.length > 0
307
- restart_server.clear
308
- server.begin_restart(true)
309
- @launcher.config.run_hooks :before_refork, nil, @launcher.events
310
- nakayoshi_gc
311
- end
312
- elsif idx == 0 # restart server
313
- restart_server << true << false
314
- else # fork worker
315
- worker_pids << pid = spawn_worker(idx, master)
316
- @worker_write << "f#{pid}:#{idx}\n" rescue nil
317
- end
318
- end
319
- end
320
- end
321
-
322
- Signal.trap "SIGTERM" do
323
- @worker_write << "e#{Process.pid}\n" rescue nil
324
- server.stop
325
- restart_server << false
326
- end
327
-
328
- begin
329
- @worker_write << "b#{Process.pid}:#{index}\n"
330
- rescue SystemCallError, IOError
331
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
332
- STDERR.puts "Master seems to have exited, exiting."
333
- return
334
- end
335
-
336
- Thread.new(@worker_write) do |io|
337
- Puma.set_thread_name "stat payload"
179
+ @master_read.close
180
+ @suicide_pipe.close
181
+ @fork_writer.close
338
182
 
339
- while true
340
- sleep Const::WORKER_CHECK_INTERVAL
341
- begin
342
- require 'json'
343
- io << "p#{Process.pid}#{server.stats.to_json}\n"
344
- rescue IOError
345
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
346
- break
347
- end
348
- end
183
+ pipes = { check_pipe: @check_pipe, worker_write: @worker_write }
184
+ if @options[:fork_worker]
185
+ pipes[:fork_pipe] = @fork_pipe
186
+ pipes[:wakeup] = @wakeup
349
187
  end
350
188
 
351
- server.run.join while restart_server.pop
352
-
353
- # Invoke any worker shutdown hooks so they can prevent the worker
354
- # exiting until any background operations are completed
355
- @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
356
- ensure
357
- @worker_write << "t#{Process.pid}\n" rescue nil
358
- @worker_write.close
189
+ new_worker = Worker.new index: index,
190
+ master: master,
191
+ launcher: @launcher,
192
+ pipes: pipes
193
+ new_worker.run
359
194
  end
360
195
 
361
196
  def restart
@@ -552,7 +387,7 @@ module Puma
552
387
  @master_read, @worker_write = read, @wakeup
553
388
 
554
389
  @launcher.config.run_hooks :before_fork, nil, @launcher.events
555
- nakayoshi_gc
390
+ Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
556
391
 
557
392
  spawn_workers
558
393
 
@@ -560,9 +395,9 @@ module Puma
560
395
  stop
561
396
  end
562
397
 
563
- @launcher.events.fire_on_booted!
564
-
565
398
  begin
399
+ booted = false
400
+
566
401
  while @status == :run
567
402
  begin
568
403
  if @phased_restart
@@ -603,6 +438,10 @@ module Puma
603
438
  when "p"
604
439
  w.ping!(result.sub(/^\d+/,'').chomp)
605
440
  @launcher.events.fire(:ping!, w)
441
+ if !booted && @workers.none? {|worker| worker.last_status.empty?}
442
+ @launcher.events.fire_on_booted!
443
+ booted = true
444
+ end
606
445
  end
607
446
  else
608
447
  log "! Out-of-sync worker list, no #{pid} worker"
@@ -640,7 +479,9 @@ module Puma
640
479
  rescue Errno::ECHILD
641
480
  begin
642
481
  Process.kill(0, w.pid)
643
- false # child still alive, but has another parent
482
+ # child still alive but has another parent (e.g., using fork_worker)
483
+ w.term if w.term?
484
+ false
644
485
  rescue Errno::ESRCH, Errno::EPERM
645
486
  true # child is already terminated
646
487
  end
@@ -657,17 +498,5 @@ module Puma
657
498
  end
658
499
  end
659
500
  end
660
-
661
- # @version 5.0.0
662
- def nakayoshi_gc
663
- return unless @options[:nakayoshi_fork]
664
- log "! Promoting existing objects to old generation..."
665
- 4.times { GC.start(full_mark: false) }
666
- if GC.respond_to?(:compact)
667
- log "! Compacting..."
668
- GC.compact
669
- end
670
- log "! Friendly fork preparation complete."
671
- end
672
501
  end
673
502
  end