ringleader 1.1.2 → 1.1.3

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.
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