gorgon 0.2.0 → 0.3.0

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