puma 2.2.2 → 2.3.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.

@@ -1,3 +1,22 @@
1
+ === 2.3.0 / 2013-07-05
2
+
3
+ * 1 major bug fix:
4
+
5
+ * Stabilize control server, add support in cluster mode
6
+
7
+ * 5 minor bug fixes:
8
+
9
+ * Add ability to cleanup stale unix sockets
10
+ * Check status data better. Fixes #292
11
+ * Convert raw IO errors to ConnectionError. Fixes #274
12
+ * Fix sending Content-Type and Content-Length for no body status. Fixes #304
13
+ * Pass state path through to `pumactl start`. Fixes #287
14
+
15
+ * 2 internal changes:
16
+
17
+ * Refactored modes into seperate classes that CLI uses
18
+ * Changed CLI to take an Events object instead of stdout/stderr (API change)
19
+
1
20
  === 2.2.2 / 2013-07-02
2
21
 
3
22
  * 1 bug fix:
data/README.md CHANGED
@@ -82,21 +82,41 @@ Puma 2 offers clustered mode, allowing you to use forked processes to handle mul
82
82
  On a ruby implementation that offers native threads, you should tune this number to match the number of cores available.
83
83
  Note that threads are still used in clustered mode, and the `-t` thread flag setting is per worker, so `-w 2 -t 16:16` will be 32 threads.
84
84
 
85
+ This code can be used to setup the process before booting the application, allowing
86
+ you to do some puma-specific things that you don't want to embed in your application.
87
+ For instance, you could fire a log notification that a worker booted or send something to statsd.
88
+ This can be called multiple times to add hooks.
89
+
90
+ If you're running in Clustered Mode you can optionally choose to preload your application before starting up the workers. To do this simply specify the `--preload` flag in invocation:
91
+
92
+ # CLI invocation
93
+ $ puma -t 8:32 -w 3 --preload
94
+
95
+ If you're using a configuration file, use the `preload_app!` method, and be sure to specify your config file's location with the `-C` flag:
96
+
97
+ $ puma -C config/puma.rb
98
+
99
+ # config/puma.rb
100
+ threads 8,32
101
+ workers 3
102
+ preload_app!
103
+
104
+
85
105
  Additionally, you can specify a block in your configuration that will be run on boot of each worker:
86
106
 
87
107
  # config/puma.rb
88
108
  on_worker_boot do
89
109
  # configuration here
90
110
  end
111
+
112
+ If you're preloading your application and using ActiveRecord, it's recommend you setup your connection pool here:
91
113
 
92
- This code can be used to setup the process before booting the application, allowing
93
- you to do some puma-specific things that you don't want to embed in your application.
94
- For instance, you could fire a log notification that a worker booted or send something to statsd.
95
- This can be called multiple times to add hooks.
96
-
97
- Be sure to specify the location of your configuration file:
98
-
99
- $ puma -t 8:32 -w 3 -C config/puma.rb
114
+ # config/puma.rb
115
+ on_worker_boot do
116
+ ActiveSupport.on_load(:active_record) do
117
+ ActiveRecord::Base.establish_connection
118
+ end
119
+ end
100
120
 
101
121
  ### Binding TCP / Sockets
102
122
 
@@ -1,8 +1,7 @@
1
1
  module Puma
2
2
  module App
3
3
  class Status
4
- def initialize(server, cli)
5
- @server = server
4
+ def initialize(cli)
6
5
  @cli = cli
7
6
  @auth_token = nil
8
7
  end
@@ -15,6 +14,15 @@ module Puma
15
14
  env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
16
15
  end
17
16
 
17
+ def rack_response(status, body, content_type='application/json')
18
+ headers = {
19
+ 'Content-Type' => content_type,
20
+ 'Content-Length' => body.bytesize.to_s
21
+ }
22
+
23
+ [status, headers, [body]]
24
+ end
25
+
18
26
  def call(env)
19
27
  unless authenticate(env)
20
28
  return rack_response(403, 'Invalid auth token', 'text/plain')
@@ -22,34 +30,29 @@ module Puma
22
30
 
23
31
  case env['PATH_INFO']
24
32
  when /\/stop$/
25
- @server.stop
33
+ @cli.stop
26
34
  return rack_response(200, OK_STATUS)
27
35
 
28
36
  when /\/halt$/
29
- @server.halt
37
+ @cli.halt
30
38
  return rack_response(200, OK_STATUS)
31
39
 
32
40
  when /\/restart$/
33
- if @cli and @cli.restart_on_stop!
34
- @server.begin_restart
41
+ @cli.restart
42
+ return rack_response(200, OK_STATUS)
35
43
 
36
- return rack_response(200, OK_STATUS)
44
+ when /\/phased-restart$/
45
+ if !@cli.phased_restart
46
+ return rack_response(404, '{ "error": "phased resart not available" }')
37
47
  else
38
- return rack_response(200, '{ "status": "not configured" }')
48
+ return rack_response(200, OK_STATUS)
39
49
  end
40
50
 
41
51
  when /\/stats$/
42
- b = @server.backlog
43
- r = @server.running
44
- return rack_response(200, %Q!{ "backlog": #{b}, "running": #{r} }!)
52
+ return rack_response(200, @cli.stats)
53
+ else
54
+ rack_response 404, "Unsupported action", 'text/plain'
45
55
  end
46
-
47
- rack_response 404, "Unsupported action", 'text/plain'
48
- end
49
-
50
- private
51
- def rack_response(status, body, content_type='application/json')
52
- [status, { 'Content-Type' => content_type, 'Content-Length' => body.bytesize.to_s }, [body]]
53
56
  end
54
57
  end
55
58
  end
@@ -255,6 +255,18 @@ module Puma
255
255
 
256
256
  begin
257
257
  old_mask = File.umask(umask)
258
+
259
+ if File.exists? path
260
+ begin
261
+ old = UNIXSocket.new path
262
+ rescue SystemCallError
263
+ File.unlink path
264
+ else
265
+ old.close
266
+ raise "There is already a server bound to: #{path}"
267
+ end
268
+ end
269
+
258
270
  s = UNIXServer.new(path)
259
271
  @ios << s
260
272
  ensure
@@ -8,6 +8,8 @@ require 'puma/binder'
8
8
  require 'puma/detect'
9
9
  require 'puma/daemon_ext'
10
10
  require 'puma/util'
11
+ require 'puma/single'
12
+ require 'puma/cluster'
11
13
 
12
14
  require 'rack/commonlogger'
13
15
  require 'rack/utils'
@@ -22,122 +24,37 @@ module Puma
22
24
  # +stdout+ and +stderr+ can be set to IO-like objects which
23
25
  # this object will report status on.
24
26
  #
25
- def initialize(argv, stdout=STDOUT, stderr=STDERR)
27
+ def initialize(argv, events=Events.stdio)
26
28
  @debug = false
27
29
  @argv = argv
28
- @stdout = stdout
29
- @stderr = stderr
30
30
 
31
- @phase = 0
32
- @workers = []
31
+ @events = events
33
32
 
34
- @events = Events.new @stdout, @stderr
35
-
36
- @server = nil
37
33
  @status = nil
34
+ @runner = nil
38
35
 
39
- @restart = false
40
- @phased_state = :idle
41
-
42
- @app = nil
36
+ @config = nil
43
37
 
44
38
  ENV['NEWRELIC_DISPATCHER'] ||= "puma"
45
39
 
46
40
  setup_options
47
-
48
41
  generate_restart_data
49
42
 
50
43
  @binder = Binder.new(@events)
51
44
  @binder.import_from_env
52
45
  end
53
46
 
54
- def restart_on_stop!
55
- @restart = true
56
- end
57
-
58
- def generate_restart_data
59
- # Use the same trick as unicorn, namely favor PWD because
60
- # it will contain an unresolved symlink, useful for when
61
- # the pwd is /data/releases/current.
62
- if dir = ENV['PWD']
63
- s_env = File.stat(dir)
64
- s_pwd = File.stat(Dir.pwd)
65
-
66
- if s_env.ino == s_pwd.ino and s_env.dev == s_pwd.dev
67
- @restart_dir = dir
68
- @options[:worker_directory] = dir
69
- end
70
- end
71
-
72
- @restart_dir ||= Dir.pwd
73
-
74
- @original_argv = ARGV.dup
75
-
76
- if defined? Rubinius::OS_ARGV
77
- @restart_argv = Rubinius::OS_ARGV
78
- else
79
- require 'rubygems'
80
-
81
- # if $0 is a file in the current directory, then restart
82
- # it the same, otherwise add -S on there because it was
83
- # picked up in PATH.
84
- #
85
- if File.exists?($0)
86
- arg0 = [Gem.ruby, $0]
87
- else
88
- arg0 = [Gem.ruby, "-S", $0]
89
- end
90
-
91
- # Detect and reinject -Ilib from the command line
92
- lib = File.expand_path "lib"
93
- arg0[1,0] = ["-I", lib] if $:[0] == lib
94
-
95
- @restart_argv = arg0 + ARGV
96
- end
97
- end
98
-
99
- def restart_args
100
- if cmd = @options[:restart_cmd]
101
- cmd.split(' ') + @original_argv
102
- else
103
- @restart_argv
104
- end
105
- end
106
-
107
- def restart!
108
- @options[:on_restart].each do |blk|
109
- blk.call self
110
- end
47
+ # The Binder object containing the sockets bound to.
48
+ attr_reader :binder
111
49
 
112
- if jruby?
113
- @binder.listeners.each_with_index do |(str,io),i|
114
- io.close
50
+ # The Configuration object used.
51
+ attr_reader :config
115
52
 
116
- # We have to unlink a unix socket path that's not being used
117
- uri = URI.parse str
118
- if uri.scheme == "unix"
119
- path = "#{uri.host}#{uri.path}"
120
- File.unlink path
121
- end
122
- end
53
+ # The Hash of options used to configure puma.
54
+ attr_reader :options
123
55
 
124
- require 'puma/jruby_restart'
125
- JRubyRestart.chdir_exec(@restart_dir, restart_args)
126
- else
127
- redirects = {:close_others => true}
128
- @binder.listeners.each_with_index do |(l,io),i|
129
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
130
- redirects[io.to_i] = io.to_i
131
- end
132
-
133
- argv = restart_args
134
-
135
- Dir.chdir @restart_dir
136
-
137
- argv += [redirects] unless RUBY_VERSION < '1.9'
138
- Kernel.exec(*argv)
139
- end
140
- end
56
+ # The Events object used to output information.
57
+ attr_reader :events
141
58
 
142
59
  # Delegate +log+ to +@events+
143
60
  #
@@ -289,6 +206,26 @@ module Puma
289
206
  end
290
207
  end
291
208
 
209
+ def write_state
210
+ write_pid
211
+
212
+ require 'yaml'
213
+
214
+ if path = @options[:state]
215
+ state = { "pid" => Process.pid }
216
+
217
+ cfg = @config.dup
218
+
219
+ [ :logger, :worker_boot, :on_restart ].each { |o| cfg.options.delete o }
220
+
221
+ state["config"] = cfg
222
+
223
+ File.open(path, "w") do |f|
224
+ f.write state.to_yaml
225
+ end
226
+ end
227
+ end
228
+
292
229
  # If configured, write the pid of the current process out
293
230
  # to a file.
294
231
  #
@@ -297,6 +234,8 @@ module Puma
297
234
  File.open(path, "w") do |f|
298
235
  f.puts Process.pid
299
236
  end
237
+
238
+ at_exit { delete_pidfile }
300
239
  end
301
240
  end
302
241
 
@@ -312,33 +251,9 @@ module Puma
312
251
  ENV['RACK_ENV'] = env
313
252
  end
314
253
 
315
- def development?
316
- @options[:environment] == "development"
317
- end
318
-
319
254
  def delete_pidfile
320
255
  if path = @options[:pidfile]
321
- File.unlink path
322
- end
323
- end
324
-
325
- def write_state
326
- write_pid
327
-
328
- require 'yaml'
329
-
330
- if path = @options[:state]
331
- state = { "pid" => Process.pid }
332
-
333
- cfg = @config.dup
334
-
335
- [ :logger, :worker_boot, :on_restart ].each { |o| cfg.options.delete o }
336
-
337
- state["config"] = cfg
338
-
339
- File.open(path, "w") do |f|
340
- f.write state.to_yaml
341
- end
256
+ File.unlink path if File.exists? path
342
257
  end
343
258
  end
344
259
 
@@ -357,535 +272,241 @@ module Puma
357
272
 
358
273
  @config.load
359
274
 
360
- if @options[:workers] > 0
275
+ if clustered?
361
276
  unsupported "worker mode not supported on JRuby and Windows",
362
277
  jruby? || windows?
363
278
  end
279
+
280
+ if @options[:daemon] and windows?
281
+ unsupported "daemon mode not supported on Windows"
282
+ end
364
283
  end
365
284
 
366
- def graceful_stop(server)
367
- log " - Gracefully stopping, waiting for requests to finish"
368
- @status.stop(true) if @status
369
- server.stop(true)
370
- delete_pidfile
371
- log " - Goodbye!"
285
+ def clustered?
286
+ @options[:workers] > 0
372
287
  end
373
288
 
374
- def redirect_io
375
- stdout = @options[:redirect_stdout]
376
- stderr = @options[:redirect_stderr]
377
- append = @options[:redirect_append]
289
+ def graceful_stop
290
+ @control.stop(true) if @control
291
+ @runner.stop_blocked
292
+ log "- Goodbye!"
293
+ end
378
294
 
379
- if stdout
380
- STDOUT.reopen stdout, (append ? "a" : "w")
381
- STDOUT.sync = true
382
- STDOUT.puts "=== puma startup: #{Time.now} ==="
383
- end
295
+ def generate_restart_data
296
+ # Use the same trick as unicorn, namely favor PWD because
297
+ # it will contain an unresolved symlink, useful for when
298
+ # the pwd is /data/releases/current.
299
+ if dir = ENV['PWD']
300
+ s_env = File.stat(dir)
301
+ s_pwd = File.stat(Dir.pwd)
384
302
 
385
- if stderr
386
- STDERR.reopen stderr, (append ? "a" : "w")
387
- STDERR.sync = true
388
- STDERR.puts "=== puma startup: #{Time.now} ==="
303
+ if s_env.ino == s_pwd.ino and s_env.dev == s_pwd.dev
304
+ @restart_dir = dir
305
+ @options[:worker_directory] = dir
306
+ end
389
307
  end
390
- end
391
308
 
392
- # Parse the options, load the rackup, start the server and wait
393
- # for it to finish.
394
- #
395
- def run
396
- begin
397
- parse_options
398
- rescue UnsupportedOption
399
- exit 1
400
- end
309
+ @restart_dir ||= Dir.pwd
401
310
 
402
- if dir = @options[:directory]
403
- Dir.chdir dir
404
- end
311
+ @original_argv = ARGV.dup
405
312
 
406
- clustered = @options[:workers] > 0
313
+ if defined? Rubinius::OS_ARGV
314
+ @restart_argv = Rubinius::OS_ARGV
315
+ else
316
+ require 'rubygems'
407
317
 
408
- if clustered
409
- @events = PidEvents.new STDOUT, STDERR
410
- @options[:logger] = @events
411
- end
318
+ # if $0 is a file in the current directory, then restart
319
+ # it the same, otherwise add -S on there because it was
320
+ # picked up in PATH.
321
+ #
322
+ if File.exists?($0)
323
+ arg0 = [Gem.ruby, $0]
324
+ else
325
+ arg0 = [Gem.ruby, "-S", $0]
326
+ end
412
327
 
413
- set_rack_environment
328
+ # Detect and reinject -Ilib from the command line
329
+ lib = File.expand_path "lib"
330
+ arg0[1,0] = ["-I", lib] if $:[0] == lib
414
331
 
415
- if clustered
416
- run_cluster
417
- else
418
- run_single
332
+ @restart_argv = arg0 + ARGV
419
333
  end
420
334
  end
421
335
 
422
- def daemon?
423
- @options[:daemon]
424
- end
425
-
426
- def jruby_daemon?
427
- daemon? and jruby?
336
+ def restart_args
337
+ if cmd = @options[:restart_cmd]
338
+ cmd.split(' ') + @original_argv
339
+ else
340
+ @restart_argv
341
+ end
428
342
  end
429
343
 
430
- def output_header
431
- min_t = @options[:min_threads]
432
- max_t = @options[:max_threads]
433
-
434
- log "Puma #{Puma::Const::PUMA_VERSION} starting..."
435
- log "* Min threads: #{min_t}, max threads: #{max_t}"
436
- log "* Environment: #{ENV['RACK_ENV']}"
344
+ def jruby_daemon_start
345
+ require 'puma/jruby_restart'
346
+ JRubyRestart.daemon_start(@restart_dir, restart_args)
437
347
  end
438
348
 
439
- def start_control(server, str)
440
- require 'puma/app/status'
441
-
442
- uri = URI.parse str
443
-
444
- app = Puma::App::Status.new server, self
445
-
446
- if token = @options[:control_auth_token]
447
- app.auth_token = token unless token.empty? or token == :none
349
+ def restart!
350
+ @options[:on_restart].each do |block|
351
+ block.call self
448
352
  end
449
353
 
450
- status = Puma::Server.new app, @events
451
- status.min_threads = 0
452
- status.max_threads = 1
354
+ if jruby?
355
+ @binder.listeners.each_with_index do |(str,io),i|
356
+ io.close
453
357
 
454
- case uri.scheme
455
- when "tcp"
456
- log "* Starting status server on #{str}"
457
- status.add_tcp_listener uri.host, uri.port
458
- when "unix"
459
- log "* Starting status server on #{str}"
460
- path = "#{uri.host}#{uri.path}"
358
+ # We have to unlink a unix socket path that's not being used
359
+ uri = URI.parse str
360
+ if uri.scheme == "unix"
361
+ path = "#{uri.host}#{uri.path}"
362
+ File.unlink path
363
+ end
364
+ end
461
365
 
462
- status.add_unix_listener path
366
+ require 'puma/jruby_restart'
367
+ JRubyRestart.chdir_exec(@restart_dir, restart_args)
463
368
  else
464
- error "Invalid status URI: #{str}"
465
- end
369
+ redirects = {:close_others => true}
370
+ @binder.listeners.each_with_index do |(l,io),i|
371
+ ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
372
+ redirects[io.to_i] = io.to_i
373
+ end
466
374
 
467
- status.run
468
- @status = status
469
- end
375
+ argv = restart_args
470
376
 
471
- def load_and_bind
472
- unless @config.app_configured?
473
- error "No application configured, nothing to run"
474
- exit 1
475
- end
377
+ Dir.chdir @restart_dir
476
378
 
477
- # Load the app before we daemonize.
478
- begin
479
- @app = @config.app
480
- rescue Exception => e
481
- log "! Unable to load application"
482
- raise e
379
+ argv += [redirects] unless RUBY_VERSION < '1.9'
380
+ Kernel.exec(*argv)
483
381
  end
484
-
485
- @binder.parse @options[:binds], self
486
382
  end
487
383
 
488
- def run_single
489
- already_daemon = false
490
-
491
- if jruby_daemon?
492
- require 'puma/jruby_restart'
493
-
494
- if JRubyRestart.daemon?
495
- # load and bind before redirecting IO so errors show up on stdout/stderr
496
- load_and_bind
497
- end
498
-
499
- already_daemon = JRubyRestart.daemon_init
384
+ # Parse the options, load the rackup, start the server and wait
385
+ # for it to finish.
386
+ #
387
+ def run
388
+ begin
389
+ parse_options
390
+ rescue UnsupportedOption
391
+ exit 1
500
392
  end
501
393
 
502
- output_header
503
-
504
- if jruby_daemon?
505
- unless already_daemon
506
- require 'puma/jruby_restart'
394
+ if dir = @options[:directory]
395
+ Dir.chdir dir
396
+ end
507
397
 
508
- pid = nil
398
+ set_rack_environment
509
399
 
510
- Signal.trap "SIGUSR2" do
511
- log "* Started new process #{pid} as daemon..."
512
- exit
513
- end
400
+ if clustered?
401
+ @events = PidEvents.new STDOUT, STDERR
402
+ @options[:logger] = @events
514
403
 
515
- pid = JRubyRestart.daemon_start(@restart_dir, restart_args)
516
- sleep
517
- end
404
+ @runner = Cluster.new(self)
518
405
  else
519
- load_and_bind
520
- Process.daemon(true) if daemon?
406
+ @runner = Single.new(self)
521
407
  end
522
408
 
523
- write_state
524
-
525
- server = Puma::Server.new @app, @events
526
- server.binder = @binder
527
- server.min_threads = @options[:min_threads]
528
- server.max_threads = @options[:max_threads]
409
+ setup_signals
529
410
 
530
- unless development?
531
- server.leak_stack_on_error = false
411
+ if cont = @options[:control_url]
412
+ start_control cont
532
413
  end
533
414
 
534
- @server = server
415
+ @status = :run
535
416
 
536
- if str = @options[:control_url]
537
- start_control server, str
417
+ @runner.run
418
+
419
+ case @status
420
+ when :halt
421
+ log "* Stopping immediately!"
422
+ when :run, :stop
423
+ graceful_stop
424
+ when :restart
425
+ log "* Restarting..."
426
+ @control.stop true if @control
427
+ restart!
538
428
  end
429
+ end
539
430
 
431
+ def setup_signals
540
432
  begin
541
433
  Signal.trap "SIGUSR2" do
542
- @restart = true
543
- server.begin_restart
434
+ restart
544
435
  end
545
436
  rescue Exception
546
- log "*** Sorry signal SIGUSR2 not implemented, restart feature disabled!"
437
+ log "*** SIGUSR2 not implemented, signal based restart unavailable!"
547
438
  end
548
439
 
549
440
  begin
550
441
  Signal.trap "SIGTERM" do
551
- log " - Gracefully stopping, waiting for requests to finish"
552
- server.stop false
442
+ stop
553
443
  end
554
444
  rescue Exception
555
- log "*** Sorry signal SIGTERM not implemented, gracefully stopping feature disabled!"
556
- end
557
-
558
- unless @options[:daemon]
559
- log "Use Ctrl-C to stop"
445
+ log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
560
446
  end
561
447
 
562
- redirect_io
563
-
564
448
  if jruby?
565
449
  Signal.trap("INT") do
566
- graceful_stop server
450
+ graceful_stop
567
451
  exit
568
452
  end
569
453
  end
570
-
571
- begin
572
- server.run.join
573
- rescue Interrupt
574
- graceful_stop server
575
- end
576
-
577
- if @restart
578
- log "* Restarting..."
579
- @status.stop true if @status
580
- restart!
581
- end
582
454
  end
583
455
 
584
- def worker(upgrade)
585
- $0 = "puma: cluster worker: #{@master_pid}"
586
- Signal.trap "SIGINT", "IGNORE"
587
-
588
- @master_read.close
589
- @suicide_pipe.close
590
-
591
- Thread.new do
592
- IO.select [@check_pipe]
593
- log "! Detected parent died, dying"
594
- exit! 1
595
- end
596
-
597
- # Be sure to change the directory again before loading
598
- # the app. This way we can pick up new code.
599
- if upgrade
600
- if dir = @options[:worker_directory]
601
- log "+ Changing to #{dir}"
602
- Dir.chdir dir
603
- end
604
- end
605
-
606
- # Invoke any worker boot hooks so they can get
607
- # things in shape before booting the app.
608
- hooks = @options[:worker_boot]
609
- hooks.each { |h| h.call }
610
-
611
- min_t = @options[:min_threads]
612
- max_t = @options[:max_threads]
613
-
614
- # If preload is used, then @app is set to the preloaded
615
- # application. Otherwise load it now via the config.
616
- app = @app || @config.app
617
-
618
- server = Puma::Server.new app, @events
619
- server.min_threads = min_t
620
- server.max_threads = max_t
621
- server.inherit_binder @binder
456
+ def start_control(str)
457
+ require 'puma/app/status'
622
458
 
623
- unless development?
624
- server.leak_stack_on_error = false
625
- end
459
+ uri = URI.parse str
626
460
 
627
- Signal.trap "SIGTERM" do
628
- server.stop
629
- end
461
+ app = Puma::App::Status.new self
630
462
 
631
- begin
632
- @worker_write << "b#{Process.pid}\n"
633
- rescue SystemCallError, IOError
634
- STDERR.puts "Master seems to have exitted, exitting."
635
- return
463
+ if token = @options[:control_auth_token]
464
+ app.auth_token = token unless token.empty? or token == :none
636
465
  end
637
466
 
638
- server.run.join
467
+ control = Puma::Server.new app, @events
468
+ control.min_threads = 0
469
+ control.max_threads = 1
639
470
 
640
- ensure
641
- @worker_write.close
642
- end
643
-
644
- def stop_workers
645
- log "- Gracefully shutting down workers..."
646
- @workers.each { |x| x.term }
471
+ case uri.scheme
472
+ when "tcp"
473
+ log "* Starting control server on #{str}"
474
+ control.add_tcp_listener uri.host, uri.port
475
+ when "unix"
476
+ log "* Starting control server on #{str}"
477
+ path = "#{uri.host}#{uri.path}"
647
478
 
648
- begin
649
- Process.waitall
650
- rescue Interrupt
651
- log "! Cancelled waiting for workers"
479
+ control.add_unix_listener path
652
480
  else
653
- log "- Goodbye!"
654
- end
655
- end
656
-
657
- def start_phased_restart
658
- @phase += 1
659
- log "- Starting phased worker restart, phase: #{@phase}"
660
- end
661
-
662
- class Worker
663
- def initialize(pid, phase)
664
- @pid = pid
665
- @phase = phase
666
- @stage = :started
481
+ error "Invalid control URI: #{str}"
667
482
  end
668
483
 
669
- attr_reader :pid, :phase
670
-
671
- def booted?
672
- @stage == :booted
673
- end
674
-
675
- def boot!
676
- @stage = :booted
677
- end
678
-
679
- def term
680
- begin
681
- Process.kill "TERM", @pid
682
- rescue Errno::ESRCH
683
- end
684
- end
484
+ control.run
485
+ @control = control
685
486
  end
686
487
 
687
- def spawn_workers
688
- diff = @options[:workers] - @workers.size
689
-
690
- upgrade = (@phased_state == :waiting)
691
-
692
- diff.times do
693
- pid = fork { worker(upgrade) }
694
- debug "Spawned worker: #{pid}"
695
- @workers << Worker.new(pid, @phase)
696
- end
697
-
698
- if diff > 0
699
- @phased_state = :idle
700
- end
488
+ def stop
489
+ @status = :stop
490
+ @runner.stop
701
491
  end
702
492
 
703
- def all_workers_booted?
704
- @workers.count { |w| !w.booted? } == 0
493
+ def restart
494
+ @status = :restart
495
+ @runner.restart
705
496
  end
706
497
 
707
- def check_workers
708
- while @workers.any?
709
- pid = Process.waitpid(-1, Process::WNOHANG)
710
- break unless pid
711
-
712
- @workers.delete_if { |w| w.pid == pid }
713
- end
714
-
715
- spawn_workers
716
-
717
- if @phased_state == :idle && all_workers_booted?
718
- # If we're running at proper capacity, check to see if
719
- # we need to phase any workers out (which will restart
720
- # in the right phase).
721
- #
722
- w = @workers.find { |x| x.phase != @phase }
723
-
724
- if w
725
- @phased_state = :waiting
726
- log "- Stopping #{w.pid} for phased upgrade..."
727
- w.term
728
- end
729
- end
498
+ def phased_restart
499
+ return false unless @runner.respond_to? :phased_restart
500
+ @runner.phased_restart
730
501
  end
731
502
 
732
- def wakeup!
733
- begin
734
- @wakeup.write "!" unless @wakeup.closed?
735
- rescue SystemCallError, IOError
736
- end
503
+ def stats
504
+ @runner.stats
737
505
  end
738
506
 
739
- def run_cluster
740
- log "Puma #{Puma::Const::PUMA_VERSION} starting in cluster mode..."
741
- log "* Process workers: #{@options[:workers]}"
742
- log "* Min threads: #{@options[:min_threads]}, max threads: #{@options[:max_threads]}"
743
- log "* Environment: #{ENV['RACK_ENV']}"
744
-
745
- if @options[:preload_app]
746
- log "* Preloading application"
747
- load_and_bind
748
- else
749
- log "* Phased restart available"
750
-
751
- unless @config.app_configured?
752
- error "No application configured, nothing to run"
753
- exit 1
754
- end
755
-
756
- @binder.parse @options[:binds], self
757
- end
758
-
759
- read, @wakeup = Puma::Util.pipe
760
-
761
- Signal.trap "SIGCHLD" do
762
- wakeup!
763
- end
764
-
765
- stop = false
766
-
767
- begin
768
- Signal.trap "SIGUSR2" do
769
- @restart = true
770
- stop = true
771
- wakeup!
772
- end
773
- rescue Exception
774
- end
775
-
776
- master_pid = Process.pid
777
-
778
- begin
779
- Signal.trap "SIGTERM" do
780
- # The worker installs their own SIGTERM when booted.
781
- # Until then, this is run by the worker and the worker
782
- # should just exit if they get it.
783
- if Process.pid != master_pid
784
- log "Early termination of worker"
785
- exit! 0
786
- else
787
- stop = true
788
- wakeup!
789
- end
790
- end
791
- rescue Exception
792
- end
793
-
794
- phased_restart = false
795
-
796
- begin
797
- if @options[:preload_app]
798
- Signal.trap "SIGUSR1" do
799
- log "App preloaded, phased restart unavailable"
800
- end
801
- else
802
- Signal.trap "SIGUSR1" do
803
- phased_restart = true
804
- @wakeup << "!"
805
- wakeup!
806
- end
807
- end
808
- rescue Exception
809
- end
810
-
811
- # Used by the workers to detect if the master process dies.
812
- # If select says that @check_pipe is ready, it's because the
813
- # master has exited and @suicide_pipe has been automatically
814
- # closed.
815
- #
816
- @check_pipe, @suicide_pipe = Puma::Util.pipe
817
-
818
- if @options[:daemon]
819
- Process.daemon(true)
820
- else
821
- log "Use Ctrl-C to stop"
822
- end
823
-
824
- @master_pid = Process.pid
825
-
826
- redirect_io
827
-
828
- write_state
829
-
830
- @master_read, @worker_write = read, @wakeup
831
- spawn_workers
832
-
833
- Signal.trap "SIGINT" do
834
- stop = true
835
- wakeup!
836
- end
837
-
838
- begin
839
- while !stop
840
- begin
841
- res = IO.select([read], nil, nil, 5)
842
-
843
- if res
844
- req = read.read_nonblock(1)
845
-
846
- if req == "b"
847
- pid = read.gets.to_i
848
- w = @workers.find { |x| x.pid == pid }
849
- if w
850
- w.boot!
851
- log "- Worker #{pid} booted, phase: #{w.phase}"
852
- else
853
- log "! Out-of-sync worker list, no #{pid} worker"
854
- end
855
- end
856
- end
857
-
858
- check_workers
859
-
860
- if phased_restart
861
- start_phased_restart
862
- phased_restart = false
863
- end
864
-
865
- rescue Interrupt
866
- stop = true
867
- end
868
- end
869
-
870
- stop_workers
871
- ensure
872
- delete_pidfile
873
- @check_pipe.close
874
- @suicide_pipe.close
875
- read.close
876
- @wakeup.close
877
- end
878
-
879
- if @restart
880
- log "* Restarting..."
881
- restart!
882
- end
883
- end
884
-
885
- def stop
886
- @status.stop(true) if @status
887
- @server.stop(true) if @server
888
- delete_pidfile
507
+ def halt
508
+ @status = :halt
509
+ @runner.halt
889
510
  end
890
511
  end
891
512
  end