gorgon 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,7 +16,7 @@ class OriginatorProtocol
16
16
  end
17
17
 
18
18
  def publish_files files
19
- @file_queue = @channel.queue(UUIDTools::UUID.timestamp_create.to_s)
19
+ @file_queue = @channel.queue("file_queue_" + UUIDTools::UUID.timestamp_create.to_s)
20
20
 
21
21
  files.each do |file|
22
22
  @channel.default_exchange.publish(file, :routing_key => @file_queue.name)
@@ -30,9 +30,9 @@ class OriginatorProtocol
30
30
  @channel.fanout("gorgon.jobs").publish(job_definition.to_json)
31
31
  end
32
32
 
33
- def ping_listeners
34
- # TODO: we probably want to use a different exchange for pinging when we add more services
35
- message = {:type => "ping", :reply_exchange_name => @reply_exchange.name}
33
+ def send_message_to_listeners type, body={}
34
+ # TODO: we probably want to use a different exchange for this type of messages
35
+ message = {:type => type, :reply_exchange_name => @reply_exchange.name, :body => body}
36
36
  @channel.fanout("gorgon.jobs").publish(Yajl::Encoder.encode(message))
37
37
  end
38
38
 
@@ -49,21 +49,22 @@ class OriginatorProtocol
49
49
  end
50
50
 
51
51
  def disconnect
52
- cleanup_queues
52
+ cleanup_queues_and_exchange
53
53
  @connection.disconnect
54
54
  end
55
55
 
56
56
  private
57
57
 
58
58
  def open_queues
59
- @reply_queue = @channel.queue(UUIDTools::UUID.timestamp_create.to_s)
60
- @reply_exchange = @channel.direct(UUIDTools::UUID.timestamp_create.to_s)
59
+ @reply_queue = @channel.queue("reply_queue_" + UUIDTools::UUID.timestamp_create.to_s)
60
+ @reply_exchange = @channel.direct("reply_exchange_" + UUIDTools::UUID.timestamp_create.to_s)
61
61
  @reply_queue.bind(@reply_exchange)
62
62
  end
63
63
 
64
- def cleanup_queues
64
+ def cleanup_queues_and_exchange
65
65
  @reply_queue.delete if @reply_queue
66
66
  @file_queue.delete if @file_queue
67
+ @reply_exchange.delete if @reply_exchange
67
68
  end
68
69
 
69
70
  def cancel_message
@@ -26,7 +26,7 @@ class PingService
26
26
  @protocol.connect @configuration[:connection], :on_closed => proc {EM.stop}
27
27
 
28
28
  @logger.log "Pinging Listeners..."
29
- @protocol.ping_listeners
29
+ @protocol.send_message_to_listeners :ping
30
30
 
31
31
  EM.add_timer(TIMEOUT) { disconnect }
32
32
 
@@ -53,7 +53,7 @@ class PingService
53
53
 
54
54
  @listeners << payload
55
55
  hostname = payload[:hostname].colorize(Colors::HOST)
56
- puts "#{hostname} is running Listener version #{payload[:version]}"
56
+ puts "#{hostname} is running Listener version #{payload[:version]} and uses #{payload[:worker_slots]} workers"
57
57
  end
58
58
 
59
59
  def print_summary
@@ -0,0 +1,22 @@
1
+ module PipeForker
2
+ def pipe_fork
3
+ stdin = Pipe.new(*IO.pipe)
4
+ pid = fork do
5
+ stdin.write.close
6
+ STDIN.reopen(stdin.read)
7
+ stdin.read.close
8
+
9
+ yield
10
+
11
+ exit
12
+ end
13
+
14
+ stdin.read.close
15
+
16
+ return pid, stdin.write
17
+ end
18
+
19
+ private
20
+
21
+ Pipe = Struct.new(:read, :write)
22
+ end
@@ -25,9 +25,10 @@ class SourceTreeSyncer
25
25
  pid, stdin, stdout, stderr = Open4::popen4 @sys_command
26
26
  stdin.close
27
27
 
28
+ ignore, status = Process.waitpid2 pid
29
+
28
30
  @output, @errors = [stdout, stderr].map { |p| begin p.read ensure p.close end }
29
31
 
30
- ignore, status = Process.waitpid2 pid
31
32
  @exitstatus = status.exitstatus
32
33
  end
33
34
 
@@ -1,3 +1,3 @@
1
1
  module Gorgon
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -2,6 +2,8 @@ require "gorgon/configuration"
2
2
  require "gorgon/amqp_service"
3
3
  require 'gorgon/callback_handler'
4
4
  require "gorgon/g_logger"
5
+ require 'gorgon/job_definition'
6
+ require "gorgon/testunit_runner"
5
7
 
6
8
  require "uuidtools"
7
9
  require "awesome_print"
@@ -9,7 +11,6 @@ require "socket"
9
11
 
10
12
  module WorkUnit
11
13
  def self.run_file filename
12
- require "gorgon/testunit_runner"
13
14
  start_t = Time.now
14
15
 
15
16
  begin
@@ -31,29 +32,44 @@ end
31
32
  class Worker
32
33
  include GLogger
33
34
 
34
- def self.build(config)
35
- payload = Yajl::Parser.new(:symbolize_keys => true).parse($stdin.read)
36
- job_definition = JobDefinition.new(payload)
35
+ class << self
36
+ def build(worker_id, config)
37
+ redirect_output_to_files worker_id
37
38
 
38
- connection_config = config[:connection]
39
- amqp = AmqpService.new connection_config
39
+ payload = Yajl::Parser.new(:symbolize_keys => true).parse($stdin.read)
40
+ job_definition = JobDefinition.new(payload)
40
41
 
41
- callback_handler = CallbackHandler.new(job_definition.callbacks)
42
+ connection_config = config[:connection]
43
+ amqp = AmqpService.new connection_config
42
44
 
43
- worker_id = UUIDTools::UUID.timestamp_create.to_s
44
- ENV["GORGON_WORKER_ID"] = worker_id
45
+ callback_handler = CallbackHandler.new(job_definition.callbacks)
45
46
 
46
- params = {
47
- :amqp => amqp,
48
- :file_queue_name => job_definition.file_queue_name,
49
- :reply_exchange_name => job_definition.reply_exchange_name,
50
- :worker_id => worker_id,
51
- :test_runner => WorkUnit,
52
- :callback_handler => callback_handler,
53
- :log_file => config[:log_file]
54
- }
47
+ ENV["GORGON_WORKER_ID"] = worker_id.to_s
55
48
 
56
- new(params)
49
+ params = {
50
+ :amqp => amqp,
51
+ :file_queue_name => job_definition.file_queue_name,
52
+ :reply_exchange_name => job_definition.reply_exchange_name,
53
+ :worker_id => worker_id,
54
+ :test_runner => WorkUnit,
55
+ :callback_handler => callback_handler,
56
+ :log_file => config[:log_file]
57
+ }
58
+
59
+ new(params)
60
+ end
61
+
62
+ def output_file id, stream
63
+ "/tmp/gorgon-worker-#{id}.#{stream.to_s}"
64
+ end
65
+
66
+ def redirect_output_to_files worker_id
67
+ STDOUT.reopen(File.open(output_file(worker_id, :out), 'w'))
68
+ STDOUT.sync = true
69
+
70
+ STDERR.reopen(File.open(output_file(worker_id, :err), 'w'))
71
+ STDERR.sync = true
72
+ end
57
73
  end
58
74
 
59
75
  def initialize(params)
@@ -68,27 +84,35 @@ class Worker
68
84
  end
69
85
 
70
86
  def work
71
- log "Running before_start callback..."
72
- @callback_handler.before_start
73
-
74
- log "Running files ..."
75
- @amqp.start_worker @file_queue_name, @reply_exchange_name do |queue, exchange|
76
- while filename = queue.pop
77
- exchange.publish make_start_message(filename)
78
- log "Running '#{filename}'"
79
- test_results = run_file(filename)
80
- exchange.publish make_finish_message(filename, test_results)
87
+ begin
88
+ log "Running before_start callback..."
89
+ register_trap_ints # do it before calling before_start callback!
90
+ @callback_handler.before_start
91
+ @cleaned = false
92
+
93
+ log "Running files ..."
94
+ @amqp.start_worker @file_queue_name, @reply_exchange_name do |queue, exchange|
95
+ while filename = queue.pop
96
+ exchange.publish make_start_message(filename)
97
+ log "Running '#{filename}'"
98
+ test_results = run_file(filename)
99
+ exchange.publish make_finish_message(filename, test_results)
100
+ end
81
101
  end
102
+ rescue Exception => e
103
+ clean_up
104
+ raise e # So worker manager can catch it
82
105
  end
83
- ensure # this 'ensure' that we run after_complete even after an 'INT' signal
84
106
  clean_up
85
107
  end
86
108
 
87
109
  private
88
110
 
89
111
  def clean_up
112
+ return if @cleaned
90
113
  log "Running after_complete callback"
91
114
  @callback_handler.after_complete
115
+ @cleaned = true
92
116
  end
93
117
 
94
118
  def run_file(filename)
@@ -102,4 +126,14 @@ class Worker
102
126
  def make_finish_message(filename, results)
103
127
  {:action => :finish, :hostname => Socket.gethostname, :worker_id => @worker_id, :filename => filename}.merge(results)
104
128
  end
129
+
130
+ def register_trap_ints
131
+ Signal.trap("INT") { ctrl_c }
132
+ Signal.trap("TERM") { ctrl_c }
133
+ end
134
+
135
+ def ctrl_c
136
+ clean_up
137
+ exit
138
+ end
105
139
  end
@@ -1,22 +1,37 @@
1
1
  require "gorgon/worker"
2
2
  require "gorgon/g_logger"
3
3
  require 'gorgon/callback_handler'
4
- require 'gorgon/pipe_manager'
4
+ require 'gorgon/pipe_forker'
5
5
  require 'gorgon/job_definition'
6
+ require "gorgon/crash_reporter"
6
7
 
7
8
  require 'eventmachine'
8
9
 
9
10
  class WorkerManager
10
- include PipeManager
11
+ include PipeForker
11
12
  include GLogger
13
+ include CrashReporter
14
+
15
+ STDOUT_FILE='/tmp/gorgon-worker-mgr.out'
16
+ STDERR_FILE='/tmp/gorgon-worker-mgr.err'
12
17
 
13
18
  def self.build listener_config_file
14
19
  @listener_config_file = listener_config_file
15
20
  config = Configuration.load_configuration_from_file(listener_config_file)
16
21
 
22
+ redirect_output_to_files
23
+
17
24
  new config
18
25
  end
19
26
 
27
+ def self.redirect_output_to_files
28
+ STDOUT.reopen(File.open(STDOUT_FILE, 'w'))
29
+ STDOUT.sync = true
30
+
31
+ STDERR.reopen(File.open(STDERR_FILE, 'w'))
32
+ STDERR.sync = true
33
+ end
34
+
20
35
  def initialize config
21
36
  initialize_logger config[:log_file]
22
37
  log "Worker Manager #{Gorgon::VERSION} initializing"
@@ -68,7 +83,13 @@ class WorkerManager
68
83
  @available_worker_slots -= 1
69
84
  ENV["GORGON_CONFIG_PATH"] = @listener_config_filename
70
85
 
71
- pid, stdin, stdout, stderr = pipe_fork_worker
86
+ worker_id = get_worker_id
87
+ log "Forking Worker #{worker_id}"
88
+ pid, stdin = pipe_fork do
89
+ worker = Worker.build(worker_id, @config)
90
+ worker.work
91
+ end
92
+
72
93
  @worker_pids << pid
73
94
  stdin.write(@job_definition.to_json)
74
95
  stdin.close
@@ -82,17 +103,18 @@ class WorkerManager
82
103
 
83
104
  worker_complete = proc do |status|
84
105
  if status.exitstatus != 0
85
- log_error "Worker #{pid} crashed with exit status #{status.exitstatus}!"
86
- error_msg = stderr.read
87
- log_error "ERROR MSG: #{error_msg}"
106
+ exitstatus = status.exitstatus
107
+ log_error "Worker #{pid} crashed with exit status #{exitstatus}!"
88
108
 
89
109
  # originator may have cancel job and exit, so only try to send message
90
110
  begin
91
- reply = {:type => :crash,
92
- :hostname => Socket.gethostname,
93
- :stdout => stdout.read,
94
- :stderr => error_msg}
95
- @reply_exchange.publish(Yajl::Encoder.encode(reply))
111
+ out_file = Worker.output_file(worker_id, :out)
112
+ err_file = Worker.output_file(worker_id, :err)
113
+
114
+ msg = report_crash @reply_exchange, :out_file => out_file,
115
+ :err_file => err_file, :footer_text => footer_text(err_file, out_file)
116
+ log_error "Process output:\n#{msg}"
117
+
96
118
  # TODO: find a way to stop the whole system when a worker crashes or do something more clever
97
119
  rescue Exception => e
98
120
  log_error "Exception raised when trying to report crash to originator:"
@@ -105,6 +127,10 @@ class WorkerManager
105
127
  EventMachine.defer(watcher, worker_complete)
106
128
  end
107
129
 
130
+ def get_worker_id
131
+ @worker_id_count = @worker_id_count.nil? ? 1 : @worker_id_count + 1
132
+ end
133
+
108
134
  def on_worker_complete
109
135
  @available_worker_slots += 1
110
136
  on_current_job_complete if current_job_complete?
@@ -125,7 +151,7 @@ class WorkerManager
125
151
  @bunny.stop
126
152
  end
127
153
 
128
- CANCEL_TIMEOUT = 15
154
+ CANCEL_TIMEOUT = 20
129
155
  def subscribe_to_originator_queue
130
156
 
131
157
  originator_watcher = proc do
@@ -154,4 +180,8 @@ class WorkerManager
154
180
 
155
181
  EventMachine.defer(originator_watcher, handle_message)
156
182
  end
183
+
184
+ def footer_text err_file, out_file
185
+ "\n***** See #{err_file} and #{out_file} at '#{Socket.gethostname}' for more details *****\n"
186
+ end
157
187
  end
@@ -0,0 +1,46 @@
1
+ require 'gorgon/crash_reporter'
2
+
3
+ describe "CrashReporter" do
4
+ let(:exchange) { stub("Bunny Exchange", :publish => nil) }
5
+ let(:info) { {
6
+ :out_file => "stdout_file", :err_file => "stderr_file", :footer_text => "Text"
7
+ } }
8
+
9
+ let(:container_class) do
10
+ Class.new do
11
+ extend(CrashReporter)
12
+ end
13
+ end
14
+
15
+ describe "#report_crash" do
16
+ it "tails output files to get last few lines" do
17
+ container_class.should_receive(:'`').once.
18
+ with(/tail.*stdout_file/).and_return ""
19
+ container_class.should_receive(:'`').once.
20
+ with(/tail.*stderr_file/).and_return ""
21
+ container_class.report_crash exchange, info
22
+ end
23
+
24
+ it "calls send_crash_message" do
25
+ container_class.stub!(:'`').and_return "stdout text", "stderr text "
26
+ container_class.should_receive(:send_crash_message).with(exchange, "stdout text", "stderr text Text")
27
+ container_class.report_crash exchange, info
28
+ end
29
+
30
+ it "returns last lines of output from stderr message and footer text" do
31
+ container_class.stub!(:'`').and_return "stdout text", "stderr text "
32
+ container_class.stub!(:send_crash_message)
33
+ result = container_class.report_crash exchange, info
34
+ result.should == "stdout text\nstderr text Text"
35
+ end
36
+ end
37
+
38
+ describe "#send_crash_message" do
39
+ it "sends message with output and errors from syncer using reply_exchange " do
40
+ reply = {:type => :crash, :hostname => Socket.gethostname, :stdout => "some output",
41
+ :stderr => "some errors"}
42
+ exchange.should_receive(:publish).with(Yajl::Encoder.encode(reply))
43
+ container_class.send_crash_message exchange, "some output", "some errors"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,84 @@
1
+ require 'gorgon/gem_command_handler'
2
+
3
+ describe GemCommandHandler do
4
+ let(:exchange) { stub("Bunny Exchange", :publish => nil) }
5
+ let(:bunny) { stub("Bunny", :exchange => exchange, :stop => nil) }
6
+
7
+ let(:payload) {
8
+ {:type => :update, :reply_exchange_name => "name",
9
+ :body => {:gem_command => "cmd"}}
10
+ }
11
+ let(:stdin) { stub("IO object", :close => nil)}
12
+ let(:stdout) { stub("IO object", :read => "output", :close => nil)}
13
+ let(:stderr) { stub("IO object", :read => "errors", :close => nil)}
14
+ let(:status) { stub("Process Status", :exitstatus => 0)}
15
+
16
+ describe "#handle" do
17
+ before do
18
+ @handler = GemCommandHandler.new bunny
19
+ @running_response = {:type => :running_command, :hostname => Socket.gethostname}
20
+ stub_methods
21
+ end
22
+
23
+ it "publishes 'running_command' message" do
24
+ bunny.should_receive(:exchange).with("name", anything).and_return(exchange)
25
+ Yajl::Encoder.should_receive(:encode).with(@running_response).and_return :json_msg
26
+ exchange.should_receive(:publish).with(:json_msg)
27
+ @handler.handle payload, {}
28
+ end
29
+
30
+ it "runs 'gem <command> gorgon' using popen4" do
31
+ Open4.should_receive(:popen4).with("gem cmd gorgon").and_return([1, stdin, stdout, stderr])
32
+ @handler.handle payload, {}
33
+ end
34
+
35
+ it "uses binary gem from configuration if this was specified" do
36
+ Open4.should_receive(:popen4).with("path/to/gem cmd gorgon").and_return([1, stdin, stdout, stderr])
37
+ @handler.handle payload, {:bin_gem_path => "path/to/gem"}
38
+ end
39
+
40
+ it "waits for the command to finish" do
41
+ Open4.should_receive(:popen4).ordered
42
+ Process.should_receive(:waitpid2).with(1).ordered.and_return([nil, status])
43
+ @handler.handle payload, {}
44
+ end
45
+
46
+ it "closes stding and reads stdout and stderr from process" do
47
+ stdin.should_receive(:close)
48
+ stdout.should_receive(:read)
49
+ stderr.should_receive(:read)
50
+ @handler.handle payload, {}
51
+ end
52
+
53
+ it "sends 'command_completed' message when exitstatus is 0" do
54
+ response = {:type => :command_completed, :hostname => Socket.gethostname,
55
+ :command => "gem cmd gorgon", :stdout => "output", :stderr => "errors" }
56
+ Yajl::Encoder.should_receive(:encode).once.ordered.with(@running_response)
57
+ Yajl::Encoder.should_receive(:encode).once.ordered.with(response).and_return :json_msg
58
+ exchange.should_receive(:publish).with(:json_msg)
59
+ @handler.handle payload, {}
60
+ end
61
+
62
+ it "stops bunny and exit if exitstatus is 0" do
63
+ bunny.should_receive(:stop).once.ordered
64
+ @handler.should_receive(:exit).once.ordered
65
+ @handler.handle payload, {}
66
+ end
67
+
68
+ it "sends 'command_failed' message when exitstatus is not 0" do
69
+ status.should_receive(:exitstatus).and_return(99)
70
+ response = {:type => :command_failed, :hostname => Socket.gethostname,
71
+ :command => "gem cmd gorgon", :stdout => "output", :stderr => "errors" }
72
+ Yajl::Encoder.should_receive(:encode).with(response).and_return :json_msg
73
+ exchange.should_receive(:publish).with(:json_msg)
74
+ @handler.handle payload, {}
75
+ end
76
+ end
77
+
78
+ def stub_methods
79
+ Open4.stub!(:popen4).and_return([1, stdin, stdout, stderr])
80
+ Process.stub!(:waitpid2).and_return([nil, status])
81
+ Yajl::Encoder.stub!(:encode).and_return :json_msg
82
+ @handler.stub!(:exit)
83
+ end
84
+ end