ringleader 1.1.2 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source :rubygems
1
+ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
data/Rakefile CHANGED
@@ -1,2 +1,14 @@
1
- #!/usr/bin/env rake
2
- require "bundler/gem_tasks"
1
+ # encoding: utf-8
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rspec/core/rake_task'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :spec
9
+
10
+ desc "Run all specs"
11
+ RSpec::Core::RakeTask.new do |t|
12
+ t.pattern = 'spec/**/*_spec.rb'
13
+ t.rspec_opts = ["-c", "-f progress"]
14
+ end
@@ -4,4 +4,5 @@ listen: ncat -k -l 10001
4
4
  # slow_echo: sleep 10 && ruby echo_server.rb
5
5
  # echo: ruby echo_server.rb
6
6
  # sleep: sleep 30
7
- stubborn: ruby stubborn.rb
7
+ # stubborn: ruby stubborn.rb
8
+ orphans: ruby bad_parent.rb
@@ -0,0 +1,15 @@
1
+ trap("INT") { STDERR.puts "#{$$} ignoring INT" }
2
+ trap("HUP") { STDERR.puts "#{$$} ignoring HUP" }
3
+ trap("TERM") { STDERR.puts "#{$$} ignoring TERM" }
4
+
5
+ @extra = 0
6
+ 3.times do |n|
7
+ STDERR.puts "#{$$} forking (#{n})"
8
+ if pid = fork
9
+ STDERR.puts "#{$$} forked child #{pid}"
10
+ break
11
+ else
12
+ @extra += 1
13
+ end
14
+ end
15
+ sleep 10 + @extra * 2 # do clean up, eventually, with parent dying first
@@ -2,9 +2,10 @@
2
2
  test:
3
3
  dir: "./dev_scripts"
4
4
  # command: "ncat -k -l 10001"
5
- command: "bundle exec foreman start 2>&1 | tee fail.log"
5
+ command: "bundle exec foreman start"
6
6
  # command: sleep 10
7
7
  server_port: 10000
8
8
  app_port: 10001
9
- idle_timeout: 10
9
+ idle_timeout: 5
10
10
  startup_timeout: 5
11
+ kill_with: "INT"
@@ -11,6 +11,7 @@ require "trollop"
11
11
  require "rainbow"
12
12
  require "color"
13
13
  require "pathname"
14
+ require 'sys/proctable'
14
15
 
15
16
  module Ringleader
16
17
  end
@@ -11,7 +11,7 @@ module Ringleader
11
11
  def initialize(config)
12
12
  @config = config
13
13
  @process = Process.new(config)
14
- enable! unless config.disabled
14
+ async.enable unless config.disabled
15
15
  start if config.run_on_load
16
16
  end
17
17
 
@@ -51,7 +51,7 @@ module Ringleader
51
51
  return if @server
52
52
  @server = TCPServer.new @config.host, @config.server_port
53
53
  @enabled = true
54
- run!
54
+ async.run
55
55
  rescue Errno::EADDRINUSE
56
56
  error "could not bind to #{@config.host}:#{@config.server_port} for #{@config.name}!"
57
57
  @server = nil
@@ -73,7 +73,7 @@ module Ringleader
73
73
 
74
74
  def run
75
75
  info "listening for connections for #{@config.name} on #{@config.host}:#{@config.server_port}"
76
- loop { handle_connection! @server.accept }
76
+ loop { async.handle_connection @server.accept }
77
77
  rescue IOError
78
78
  @server.close if @server
79
79
  end
@@ -84,7 +84,7 @@ module Ringleader
84
84
 
85
85
  started = @process.start
86
86
  if started
87
- proxy_to_app! socket
87
+ async.proxy_to_app socket
88
88
  reset_activity_timer
89
89
  else
90
90
  error "could not start app"
@@ -2,19 +2,22 @@ module Ringleader
2
2
  class CLI
3
3
  include Celluloid::Logger
4
4
 
5
+ RC_FILE = File.expand_path('~/.ringleaderrc')
6
+
5
7
  def run(argv)
6
- # hide "shutdown" info message until after opts are validated
7
- Celluloid.logger.level = ::Logger::ERROR
8
+ configure_logging
8
9
 
9
10
  opts = nil
10
11
  Trollop.with_standard_exception_handling parser do
11
- opts = parser.parse argv
12
+ opts = merge_rc_opts(parser.parse(argv))
12
13
  end
13
14
 
14
15
  die "must provide a filename" if argv.empty?
15
16
  die "could not find config file #{argv.first}" unless File.exist?(argv.first)
16
17
 
17
- configure_logging(opts.verbose ? "debug" : "info")
18
+ if opts.verbose?
19
+ Celluloid.logger.level = ::Logger::DEBUG
20
+ end
18
21
 
19
22
  apps = Config.new(argv.first, opts.boring).apps
20
23
 
@@ -30,8 +33,10 @@ module Ringleader
30
33
  sleep
31
34
  end
32
35
 
33
- def configure_logging(level)
34
- Celluloid.logger.level = ::Logger.const_get(level.upcase)
36
+ def configure_logging
37
+ # set to INFO at first to hide celluloid's shutdown message until after
38
+ # opts are validated.
39
+ Celluloid.logger.level = ::Logger::INFO
35
40
  format = "%5s %s.%06d | %s\n"
36
41
  date_format = "%H:%M:%S"
37
42
  Celluloid.logger.formatter = lambda do |severity, time, progname, msg|
@@ -117,20 +122,37 @@ something like this:
117
122
  OPTIONS
118
123
  banner
119
124
 
120
- opt "verbose", "log at debug level",
121
- :long => "--verbose", :short => "-v",
122
- :type => :boolean, :default => false
123
- opt "host", "host for web control panel",
124
- :long => "--host", :short => "-H",
125
- :default => "localhost"
126
- opt "port", "port for the web control panel",
127
- :long => "--port", :short => "-p",
128
- :type => :integer, :default => 42000
129
- opt "boring", "use boring colors instead of a fabulous rainbow",
130
- :long => "--boring", :short => "-b",
131
- :type => :boolean, :default => false
125
+ opt :verbose, "log at debug level",
126
+ :short => "-v", :default => false
127
+ opt :host, "host for web control panel",
128
+ :short => "-H", :default => "localhost"
129
+ opt :port, "port for the web control panel",
130
+ :short => "-p", :default => 42000
131
+ opt :boring, "use boring colors instead of a fabulous rainbow",
132
+ :short => "-b", :default => false
133
+
134
+ end
135
+ end
136
+
137
+ def merge_rc_opts(opts)
138
+ [:verbose, :host, :port, :boring].each do |option_name|
139
+ if rc_opts.has_key?(option_name) && !opts["#{option_name}_given".to_sym]
140
+ opts[option_name] = rc_opts[option_name]
141
+ end
142
+ end
143
+ opts
144
+ end
132
145
 
146
+ def rc_opts
147
+ unless @rc_opts
148
+ if File.readable?(RC_FILE)
149
+ info "reading options from ~/.ringleaderrc"
150
+ @rc_opts = parser.parse File.read(RC_FILE).strip.split(/\s+/)
151
+ else
152
+ @rc_opts = {}
153
+ end
133
154
  end
155
+ @rc_opts
134
156
  end
135
157
 
136
158
  end
@@ -21,7 +21,7 @@ module Ringleader
21
21
  @apps = Hash[*configs.flatten]
22
22
  end
23
23
 
24
- # Private: convert a YML hash to an array of name/OpenStruct pairs
24
+ # Internal: convert a YML hash to an array of name/OpenStruct pairs
25
25
  #
26
26
  # Does validation for each app config and raises an error if anything is
27
27
  # wrong. Sets default values for missing options, and assigns colors to each
@@ -61,7 +61,7 @@ module Ringleader
61
61
  end
62
62
  end
63
63
 
64
- # Private: validate that the options have all of the required keys
64
+ # Internal: validate that the options have all of the required keys
65
65
  def validate(name, options)
66
66
  REQUIRED_KEYS.each do |key|
67
67
  unless options.has_key?(key)
@@ -70,7 +70,7 @@ module Ringleader
70
70
  end
71
71
  end
72
72
 
73
- # Private: assign a color to each application configuration.
73
+ # Internal: assign a color to each application configuration.
74
74
  #
75
75
  # configs - the config data to modify
76
76
  # boring - use boring standard terminal colors instead of a rainbow.
@@ -55,21 +55,27 @@ module Ringleader
55
55
  def stop
56
56
  return unless @pid
57
57
 
58
+ children = child_pids @pid
59
+
58
60
  info "stopping #{@pid}"
61
+ debug "child pids: #{children.inspect}"
62
+
59
63
  @master.close unless @master.closed?
64
+
60
65
  debug "kill -#{config.kill_with} #{@pid}"
61
66
  ::Process.kill config.kill_with, -@pid
62
67
 
63
- kill = after 7 do
64
- if @running
65
- warn "process #{@pid} did not shut down cleanly, killing it"
66
- debug "kill -KILL #{@pid}"
67
- ::Process.kill "KILL", -@pid
68
- end
68
+ failsafe = after 7 do
69
+ warn "process #{@pid} did not shut down cleanly, killing it"
70
+ debug "kill -KILL #{@pid}"
71
+ ::Process.kill "KILL", -@pid
72
+ reap_orphans children
69
73
  end
70
74
 
71
75
  wait :running # wait for the exit callback
72
- kill.cancel
76
+ failsafe.cancel
77
+ sleep 2 # give the children a chance to shut down
78
+ reap_orphans children
73
79
 
74
80
  rescue Errno::ESRCH, Errno::EPERM
75
81
  exited
@@ -83,8 +89,7 @@ module Ringleader
83
89
 
84
90
  # Internal: callback for when the process has exited.
85
91
  def exited
86
- debug "pid #{@pid} has exited"
87
- info "exited."
92
+ info "pid #{@pid} exited"
88
93
  @running = false
89
94
  @pid = nil
90
95
  @wait_for_port.terminate if @wait_for_port.alive?
@@ -92,7 +97,7 @@ module Ringleader
92
97
  signal :running, false
93
98
  end
94
99
 
95
- # Private: start the application process and associated infrastructure
100
+ # Internal: start the application process and associated infrastructure
96
101
  #
97
102
  # Intended to be synchronous, as it blocks until the app has started (or
98
103
  # failed to start).
@@ -124,7 +129,7 @@ module Ringleader
124
129
 
125
130
  timer = after config.startup_timeout do
126
131
  warn "application startup took more than #{config.startup_timeout}"
127
- stop!
132
+ async.stop
128
133
  end
129
134
 
130
135
  @running = wait :running
@@ -143,7 +148,7 @@ module Ringleader
143
148
  end
144
149
  end
145
150
 
146
- # Private: check if the app is already running outside ringleader
151
+ # Internal: check if the app is already running outside ringleader
147
152
  def already_running?
148
153
  socket = TCPSocket.new config.host, config.app_port
149
154
  socket.close
@@ -152,7 +157,7 @@ module Ringleader
152
157
  false
153
158
  end
154
159
 
155
- # Private: proxy output streams to the logger.
160
+ # Internal: proxy output streams to the logger.
156
161
  #
157
162
  # Fire and forget, runs in its own thread.
158
163
  def proxy_output(input)
@@ -163,7 +168,7 @@ module Ringleader
163
168
  end
164
169
  end
165
170
 
166
- # Private: execute a command in a clean environment (bundler)
171
+ # Internal: execute a command in a clean environment (bundler)
167
172
  def in_clean_environment(&block)
168
173
  if Object.const_defined?(:Bundler)
169
174
  ::Bundler.with_clean_env(&block)
@@ -172,6 +177,37 @@ module Ringleader
172
177
  end
173
178
  end
174
179
 
180
+ # Internal: kill orphaned processes
181
+ def reap_orphans(child_pids)
182
+ child_pids.each do |pid|
183
+ next unless Sys::ProcTable.ps(pid)
184
+ error "child process #{pid} was orphaned, killing it"
185
+ begin
186
+ ::Process.kill "KILL", pid
187
+ rescue Errno::ESRCH, Errno::EPERM
188
+ debug "could not kill #{pid}"
189
+ end
190
+ end
191
+ end
192
+
193
+ # Internal: returns all child pids of the given parent
194
+ def child_pids(parent_pid)
195
+ proc_table = Sys::ProcTable.ps
196
+ children_of parent_pid, proc_table
197
+ end
198
+
199
+ # Internal: find child pids given a parent pid and a proc table
200
+ def children_of(parent_pid, proc_table)
201
+ [].tap do |pids|
202
+ proc_table.each do |proc_record|
203
+ if proc_record.ppid == parent_pid
204
+ pids << proc_record.pid
205
+ pids.concat children_of proc_record.pid, proc_table
206
+ end
207
+ end
208
+ end
209
+ end
210
+
175
211
  end
176
212
 
177
213
  end
@@ -16,8 +16,8 @@ module Ringleader
16
16
  debug "proxying to #{host}:#{port}"
17
17
  @socket = TCPSocket.new(host, port)
18
18
 
19
- proxy! @socket, @upstream
20
- proxy! @upstream, @socket
19
+ async.proxy @socket, @upstream
20
+ async.proxy @upstream, @socket
21
21
 
22
22
  rescue Errno::ECONNREFUSED
23
23
  error "could not proxy to #{host}:#{port}"
@@ -1,3 +1,3 @@
1
1
  module Ringleader
2
- VERSION = "1.1.2"
2
+ VERSION = "1.1.3"
3
3
  end
@@ -4,12 +4,12 @@ module Ringleader
4
4
 
5
5
  def initialize(pid, app)
6
6
  @pid, @app = pid, app
7
- wait!
7
+ async.wait
8
8
  end
9
9
 
10
10
  def wait
11
11
  ::Process.waitpid @pid
12
- @app.exited!
12
+ @app.async.exited
13
13
  terminate
14
14
  end
15
15
  end
@@ -5,19 +5,19 @@ module Ringleader
5
5
 
6
6
  def initialize(host, port, app)
7
7
  @host, @port, @app = host, port, app
8
- wait!
8
+ async.wait
9
9
  end
10
10
 
11
11
  def wait
12
12
  begin
13
- socket = TCPSocket.new @host, @port
13
+ TCPSocket.new @host, @port
14
14
  rescue Errno::ECONNREFUSED
15
15
  sleep 0.5
16
16
  debug "#{@host}:#{@port} not open yet"
17
17
  retry
18
18
  end
19
19
  debug "#{@host}:#{@port} open"
20
- @app.port_opened!
20
+ @app.async.port_opened
21
21
  terminate
22
22
  end
23
23
  end
@@ -16,12 +16,13 @@ Gem::Specification.new do |gem|
16
16
  gem.version = Ringleader::VERSION
17
17
  gem.required_ruby_version = "> 1.9.3"
18
18
 
19
- gem.add_dependency "celluloid", "~> 0.11.0"
20
- gem.add_dependency "celluloid-io", "~> 0.11.0"
19
+ gem.add_dependency "celluloid", "~> 0.12.4"
20
+ gem.add_dependency "celluloid-io", "~> 0.12.0"
21
21
  gem.add_dependency "reel", "~> 0.1.0"
22
22
  gem.add_dependency "trollop", "~> 1.16.2"
23
23
  gem.add_dependency "rainbow", "~> 1.1.4"
24
24
  gem.add_dependency "color", "~> 1.4.1"
25
+ gem.add_dependency "sys-proctable", "~> 0.9.2"
25
26
 
26
27
  gem.add_development_dependency "rake"
27
28
  gem.add_development_dependency "rspec", "~> 2.11.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ringleader
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.1.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-03 00:00:00.000000000 Z
12
+ date: 2013-03-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: celluloid
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: 0.11.0
21
+ version: 0.12.4
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: 0.11.0
29
+ version: 0.12.4
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: celluloid-io
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -34,7 +34,7 @@ dependencies:
34
34
  requirements:
35
35
  - - ~>
36
36
  - !ruby/object:Gem::Version
37
- version: 0.11.0
37
+ version: 0.12.0
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,7 @@ dependencies:
42
42
  requirements:
43
43
  - - ~>
44
44
  - !ruby/object:Gem::Version
45
- version: 0.11.0
45
+ version: 0.12.0
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: reel
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +107,22 @@ dependencies:
107
107
  - - ~>
108
108
  - !ruby/object:Gem::Version
109
109
  version: 1.4.1
110
+ - !ruby/object:Gem::Dependency
111
+ name: sys-proctable
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 0.9.2
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: 0.9.2
110
126
  - !ruby/object:Gem::Dependency
111
127
  name: rake
112
128
  requirement: !ruby/object:Gem::Requirement
@@ -190,6 +206,7 @@ files:
190
206
  - assets/underscore-min.js
191
207
  - bin/ringleader
192
208
  - dev_scripts/Procfile
209
+ - dev_scripts/bad_parent.rb
193
210
  - dev_scripts/echo_server.rb
194
211
  - dev_scripts/many.yml
195
212
  - dev_scripts/signaling.rb
@@ -244,10 +261,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
244
261
  version: '0'
245
262
  segments:
246
263
  - 0
247
- hash: 1091358799761404049
264
+ hash: -3588672138086956428
248
265
  requirements: []
249
266
  rubyforge_project:
250
- rubygems_version: 1.8.25
267
+ rubygems_version: 1.8.23
251
268
  signing_key:
252
269
  specification_version: 3
253
270
  summary: Proxy TCP connections to an on-demand pool of configured applications