gorgon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +52 -0
- data/README.md +53 -0
- data/Rakefile +1 -0
- data/bin/gorgon +41 -0
- data/gorgon.gemspec +33 -0
- data/lib/gorgon.rb +6 -0
- data/lib/gorgon/amqp_service.rb +39 -0
- data/lib/gorgon/callback_handler.rb +21 -0
- data/lib/gorgon/configuration.rb +9 -0
- data/lib/gorgon/failures_printer.rb +37 -0
- data/lib/gorgon/g_logger.rb +22 -0
- data/lib/gorgon/host_state.rb +31 -0
- data/lib/gorgon/job.rb +26 -0
- data/lib/gorgon/job_definition.rb +24 -0
- data/lib/gorgon/job_state.rb +119 -0
- data/lib/gorgon/listener.rb +147 -0
- data/lib/gorgon/originator.rb +120 -0
- data/lib/gorgon/originator_logger.rb +36 -0
- data/lib/gorgon/originator_protocol.rb +65 -0
- data/lib/gorgon/pipe_manager.rb +55 -0
- data/lib/gorgon/progress_bar_view.rb +121 -0
- data/lib/gorgon/source_tree_syncer.rb +37 -0
- data/lib/gorgon/testunit_runner.rb +50 -0
- data/lib/gorgon/version.rb +3 -0
- data/lib/gorgon/worker.rb +103 -0
- data/lib/gorgon/worker_manager.rb +148 -0
- data/lib/gorgon/worker_watcher.rb +22 -0
- data/spec/callback_handler_spec.rb +77 -0
- data/spec/failures_printer_spec.rb +66 -0
- data/spec/host_state_spec.rb +65 -0
- data/spec/job_definition_spec.rb +20 -0
- data/spec/job_state_spec.rb +231 -0
- data/spec/listener_spec.rb +194 -0
- data/spec/originator_logger_spec.rb +40 -0
- data/spec/originator_protocol_spec.rb +134 -0
- data/spec/originator_spec.rb +134 -0
- data/spec/progress_bar_view_spec.rb +98 -0
- data/spec/source_tree_syncer_spec.rb +65 -0
- data/spec/worker_manager_spec.rb +23 -0
- data/spec/worker_spec.rb +114 -0
- metadata +270 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
class SourceTreeSyncer
|
2
|
+
attr_accessor :exclude
|
3
|
+
attr_reader :sys_command
|
4
|
+
|
5
|
+
SYS_COMMAND = 'rsync'
|
6
|
+
OPTS = '-az'
|
7
|
+
EXCLUDE_OPT = "--exclude"
|
8
|
+
|
9
|
+
def initialize source_tree_path
|
10
|
+
@source_tree_path = source_tree_path
|
11
|
+
@exclude = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def sync
|
15
|
+
@tempdir = Dir.mktmpdir("gorgon")
|
16
|
+
Dir.chdir(@tempdir)
|
17
|
+
|
18
|
+
exclude_opt = build_exclude_opt
|
19
|
+
@sys_command = "#{SYS_COMMAND} #{OPTS} #{exclude_opt} -r --rsh=ssh #{@source_tree_path}/* ."
|
20
|
+
system(@sys_command)
|
21
|
+
|
22
|
+
return $?.exitstatus == 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove_temp_dir
|
26
|
+
FileUtils::remove_entry_secure(@tempdir)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_exclude_opt
|
32
|
+
return "" if @exclude.nil? or @exclude.empty?
|
33
|
+
|
34
|
+
@exclude.unshift("")
|
35
|
+
@exclude.join(" #{EXCLUDE_OPT} ")
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'test/unit/testresult'
|
3
|
+
|
4
|
+
Test::Unit.run = true # This stops testunit from running the file as soon as it is included. Yep. That's correct. True.
|
5
|
+
|
6
|
+
module GorgonTestCases
|
7
|
+
def self.cases
|
8
|
+
@gorgon_cases ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.clear_cases!
|
12
|
+
@gorgon_cases = []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
if defined? ActiveSupport::TestCase
|
18
|
+
class ActiveSupport::TestCase
|
19
|
+
def self.inherited(klass)
|
20
|
+
GorgonTestCases.cases << klass
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Test::Unit::TestCase
|
26
|
+
def self.inherited(klass)
|
27
|
+
GorgonTestCases.cases << klass
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class TestRunner
|
32
|
+
def self.run_file(filename)
|
33
|
+
GorgonTestCases.clear_cases!
|
34
|
+
load filename
|
35
|
+
|
36
|
+
result = Test::Unit::TestResult.new
|
37
|
+
output = []
|
38
|
+
result.add_listener(Test::Unit::TestResult::FAULT) do |value|
|
39
|
+
output << value
|
40
|
+
end
|
41
|
+
|
42
|
+
GorgonTestCases.cases.each do |klass|
|
43
|
+
# Not all descendants of TestCase are actually runnable, but they do all implement #suite
|
44
|
+
# Calling suite.run will give us only runnable tests
|
45
|
+
klass.suite.run(result) {|s,n|;}
|
46
|
+
end
|
47
|
+
|
48
|
+
output
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "gorgon/configuration"
|
2
|
+
require "gorgon/amqp_service"
|
3
|
+
require 'gorgon/callback_handler'
|
4
|
+
require "gorgon/g_logger"
|
5
|
+
|
6
|
+
require "uuidtools"
|
7
|
+
require "awesome_print"
|
8
|
+
require "socket"
|
9
|
+
|
10
|
+
module WorkUnit
|
11
|
+
def self.run_file filename
|
12
|
+
require "gorgon/testunit_runner"
|
13
|
+
start_t = Time.now
|
14
|
+
results = TestRunner.run_file(filename)
|
15
|
+
length = Time.now - start_t
|
16
|
+
|
17
|
+
if results.empty?
|
18
|
+
{:failures => [], :type => :pass, :time => length}
|
19
|
+
else
|
20
|
+
{:failures => results, :type => :fail, :time => length}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Worker
|
26
|
+
include GLogger
|
27
|
+
|
28
|
+
def self.build(config)
|
29
|
+
Signal.trap("INT") { interrupted }
|
30
|
+
|
31
|
+
payload = Yajl::Parser.new(:symbolize_keys => true).parse($stdin.read)
|
32
|
+
job_definition = JobDefinition.new(payload)
|
33
|
+
|
34
|
+
connection_config = config[:connection]
|
35
|
+
amqp = AmqpService.new connection_config
|
36
|
+
|
37
|
+
callback_handler = CallbackHandler.new(job_definition.callbacks)
|
38
|
+
|
39
|
+
worker_id = UUIDTools::UUID.timestamp_create.to_s
|
40
|
+
ENV["GORGON_WORKER_ID"] = worker_id
|
41
|
+
|
42
|
+
params = {
|
43
|
+
:amqp => amqp,
|
44
|
+
:file_queue_name => job_definition.file_queue_name,
|
45
|
+
:reply_exchange_name => job_definition.reply_exchange_name,
|
46
|
+
:worker_id => worker_id,
|
47
|
+
:test_runner => WorkUnit,
|
48
|
+
:callback_handler => callback_handler,
|
49
|
+
:log_file => config[:log_file]
|
50
|
+
}
|
51
|
+
|
52
|
+
new(params)
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(params)
|
56
|
+
initialize_logger params[:log_file]
|
57
|
+
|
58
|
+
@amqp = params[:amqp]
|
59
|
+
@file_queue_name = params[:file_queue_name]
|
60
|
+
@reply_exchange_name = params[:reply_exchange_name]
|
61
|
+
@worker_id = params[:worker_id]
|
62
|
+
@test_runner = params[:test_runner]
|
63
|
+
@callback_handler = params[:callback_handler]
|
64
|
+
end
|
65
|
+
|
66
|
+
def work
|
67
|
+
log "Running before_start callback"
|
68
|
+
@callback_handler.before_start
|
69
|
+
|
70
|
+
@amqp.start_worker @file_queue_name, @reply_exchange_name do |queue, exchange|
|
71
|
+
while filename = queue.pop
|
72
|
+
exchange.publish make_start_message(filename)
|
73
|
+
test_results = run_file(filename)
|
74
|
+
exchange.publish make_finish_message(filename, test_results)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
ensure # this 'ensure' that we run after_complete even after an 'INT' signal
|
78
|
+
clean_up
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def clean_up
|
84
|
+
log "Running after_complete callback"
|
85
|
+
@callback_handler.after_complete
|
86
|
+
end
|
87
|
+
|
88
|
+
def run_file(filename)
|
89
|
+
@test_runner.run_file(filename)
|
90
|
+
end
|
91
|
+
|
92
|
+
def make_start_message(filename)
|
93
|
+
{:action => :start, :hostname => Socket.gethostname, :worker_id => @worker_id, :filename => filename}
|
94
|
+
end
|
95
|
+
|
96
|
+
def make_finish_message(filename, results)
|
97
|
+
{:action => :finish, :hostname => Socket.gethostname, :worker_id => @worker_id, :filename => filename}.merge(results)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.interrupted
|
101
|
+
exit # to avoid raising "INT" exception
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require "gorgon/worker"
|
2
|
+
require "gorgon/g_logger"
|
3
|
+
require 'gorgon/callback_handler'
|
4
|
+
require 'gorgon/pipe_manager'
|
5
|
+
require 'gorgon/job_definition'
|
6
|
+
|
7
|
+
require 'eventmachine'
|
8
|
+
|
9
|
+
class WorkerManager
|
10
|
+
include PipeManager
|
11
|
+
include GLogger
|
12
|
+
|
13
|
+
def self.build listener_config_file
|
14
|
+
@listener_config_file = listener_config_file
|
15
|
+
config = Configuration.load_configuration_from_file(listener_config_file)
|
16
|
+
|
17
|
+
new config
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize config
|
21
|
+
initialize_logger config[:log_file]
|
22
|
+
@worker_pids = []
|
23
|
+
|
24
|
+
@config = config
|
25
|
+
|
26
|
+
payload = Yajl::Parser.new(:symbolize_keys => true).parse($stdin.read)
|
27
|
+
@job_definition = JobDefinition.new(payload)
|
28
|
+
|
29
|
+
@callback_handler = CallbackHandler.new(@job_definition.callbacks)
|
30
|
+
@available_worker_slots = config[:worker_slots]
|
31
|
+
|
32
|
+
connect
|
33
|
+
end
|
34
|
+
|
35
|
+
def manage
|
36
|
+
fork_workers @available_worker_slots
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def connect
|
42
|
+
@bunny = Bunny.new(@config[:connection])
|
43
|
+
@bunny.start
|
44
|
+
@reply_exchange = @bunny.exchange(@job_definition.reply_exchange_name)
|
45
|
+
|
46
|
+
@originator_queue = @bunny.queue("", :exclusive => true, :auto_delete => true)
|
47
|
+
exchange = @bunny.exchange("gorgon.worker_managers", :type => :fanout)
|
48
|
+
@originator_queue.bind(exchange)
|
49
|
+
end
|
50
|
+
|
51
|
+
def fork_workers n_workers
|
52
|
+
log "Running before_creating_workers callback"
|
53
|
+
@callback_handler.before_creating_workers
|
54
|
+
|
55
|
+
log "Forking #{n_workers} worker(s)"
|
56
|
+
EventMachine.run do
|
57
|
+
n_workers.times do
|
58
|
+
fork_a_worker
|
59
|
+
end
|
60
|
+
|
61
|
+
subscribe_to_originator_queue
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def fork_a_worker
|
66
|
+
@available_worker_slots -= 1
|
67
|
+
ENV["GORGON_CONFIG_PATH"] = @listener_config_filename
|
68
|
+
|
69
|
+
pid, stdin, stdout, stderr = pipe_fork_worker
|
70
|
+
@worker_pids << pid
|
71
|
+
stdin.write(@job_definition.to_json)
|
72
|
+
stdin.close
|
73
|
+
|
74
|
+
watcher = proc do
|
75
|
+
ignore, status = Process.waitpid2 pid
|
76
|
+
@worker_pids.delete(pid)
|
77
|
+
log "Worker #{pid} finished"
|
78
|
+
status
|
79
|
+
end
|
80
|
+
|
81
|
+
worker_complete = proc do |status|
|
82
|
+
if status.exitstatus != 0
|
83
|
+
log_error "Worker #{pid} crashed with exit status #{status.exitstatus}!"
|
84
|
+
error_msg = stderr.read
|
85
|
+
log_error "ERROR MSG: #{error_msg}"
|
86
|
+
|
87
|
+
# originator may have cancel job and exit, so only try to send message
|
88
|
+
begin
|
89
|
+
reply = {:type => :crash,
|
90
|
+
:hostname => Socket.gethostname,
|
91
|
+
:stdout => stdout.read,
|
92
|
+
:stderr => error_msg}
|
93
|
+
@reply_ecxhange.publish(Yajl::Encoder.encode(reply))
|
94
|
+
# TODO: find a way to stop the whole system when a worker crashes or do something more clever
|
95
|
+
rescue Exception => e
|
96
|
+
log_error "Exception raised when trying to report crash to originator:"
|
97
|
+
log_error e.message
|
98
|
+
log_error e.backtrace.join("\n")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
on_worker_complete
|
102
|
+
end
|
103
|
+
EventMachine.defer(watcher, worker_complete)
|
104
|
+
end
|
105
|
+
|
106
|
+
def on_worker_complete
|
107
|
+
@available_worker_slots += 1
|
108
|
+
on_current_job_complete if current_job_complete?
|
109
|
+
end
|
110
|
+
|
111
|
+
def current_job_complete?
|
112
|
+
@available_worker_slots == @config[:worker_slots]
|
113
|
+
end
|
114
|
+
|
115
|
+
def on_current_job_complete
|
116
|
+
log "Job '#{@job_definition.inspect}' completed"
|
117
|
+
|
118
|
+
EventMachine.stop_event_loop
|
119
|
+
@bunny.stop
|
120
|
+
end
|
121
|
+
|
122
|
+
def subscribe_to_originator_queue
|
123
|
+
|
124
|
+
originator_watcher = proc do
|
125
|
+
while true
|
126
|
+
if (payload = @originator_queue.pop[:payload]) != :queue_empty
|
127
|
+
break
|
128
|
+
end
|
129
|
+
sleep 0.5
|
130
|
+
end
|
131
|
+
Yajl::Parser.new(:symbolize_keys => true).parse(payload)
|
132
|
+
end
|
133
|
+
|
134
|
+
handle_message = proc do |payload|
|
135
|
+
if payload[:action] == "cancel_job"
|
136
|
+
log "Cancel job received!!!!!!"
|
137
|
+
|
138
|
+
log "Sending 'INT' signal to #{@worker_pids}"
|
139
|
+
Process.kill("INT", *@worker_pids)
|
140
|
+
log "Signal sent"
|
141
|
+
else
|
142
|
+
EventMachine.defer(originator_watcher, handle_message)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
EventMachine.defer(originator_watcher, handle_message)
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
class WorkerWatcher < EventMachine::ProcessWatch
|
5
|
+
def initialize(options = {})
|
6
|
+
@pid = options[:pid]
|
7
|
+
@stdout = options[:stdout]
|
8
|
+
@stderr = options[:stderr]
|
9
|
+
@reply_exchange = options[:reply_exchange]
|
10
|
+
end
|
11
|
+
|
12
|
+
def process_exited
|
13
|
+
ignored, status = Process::waitpid2 @pid
|
14
|
+
if status.exitstatus != 0
|
15
|
+
reply = {:type => :crash,
|
16
|
+
:hostname => Socket.gethostname,
|
17
|
+
:stdout => @stdout.read,
|
18
|
+
:stderr => @stderr.read}
|
19
|
+
@reply_exchange.publish(Yajl::Encoder.encode(reply))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'gorgon/callback_handler'
|
2
|
+
|
3
|
+
describe CallbackHandler do
|
4
|
+
|
5
|
+
let(:config) {
|
6
|
+
{
|
7
|
+
:before_start => "some/file.rb",
|
8
|
+
:after_complete => "some/other/file.rb",
|
9
|
+
:before_creating_workers => "callbacks/before_creating_workers_file.rb",
|
10
|
+
:after_sync => "callbacks/after_sync_file.rb"
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
it "calls before hook" do
|
15
|
+
handler = CallbackHandler.new(config)
|
16
|
+
|
17
|
+
handler.should_receive(:load).with("some/file.rb")
|
18
|
+
|
19
|
+
handler.before_start
|
20
|
+
end
|
21
|
+
|
22
|
+
it "does not attempt to load the before start script when before_start is not defined" do
|
23
|
+
handler = CallbackHandler.new({})
|
24
|
+
|
25
|
+
handler.should_not_receive(:load)
|
26
|
+
|
27
|
+
handler.before_start
|
28
|
+
end
|
29
|
+
|
30
|
+
it "calls after hook" do
|
31
|
+
handler = CallbackHandler.new(config)
|
32
|
+
|
33
|
+
handler.should_receive(:load).with("some/other/file.rb")
|
34
|
+
|
35
|
+
handler.after_complete
|
36
|
+
end
|
37
|
+
|
38
|
+
it "does not attempt to load the after complete script when before_start is not defined" do
|
39
|
+
handler = CallbackHandler.new({})
|
40
|
+
|
41
|
+
handler.should_not_receive(:load)
|
42
|
+
|
43
|
+
handler.after_complete
|
44
|
+
end
|
45
|
+
|
46
|
+
it "calls before fork hook" do
|
47
|
+
handler = CallbackHandler.new(config)
|
48
|
+
|
49
|
+
handler.should_receive(:load).with("callbacks/before_creating_workers_file.rb")
|
50
|
+
|
51
|
+
handler.before_creating_workers
|
52
|
+
end
|
53
|
+
|
54
|
+
it "does not attempt to load the before creating workers script when before_creating_workers is not defined" do
|
55
|
+
handler = CallbackHandler.new({})
|
56
|
+
|
57
|
+
handler.should_not_receive(:load)
|
58
|
+
|
59
|
+
handler.before_creating_workers
|
60
|
+
end
|
61
|
+
|
62
|
+
it "calls after sync hook" do
|
63
|
+
handler = CallbackHandler.new(config)
|
64
|
+
|
65
|
+
handler.should_receive(:load).with("callbacks/after_sync_file.rb")
|
66
|
+
|
67
|
+
handler.after_sync
|
68
|
+
end
|
69
|
+
|
70
|
+
it "does not attempt to load the after-sync script when after_sync is not defined" do
|
71
|
+
handler = CallbackHandler.new({})
|
72
|
+
|
73
|
+
handler.should_not_receive(:load)
|
74
|
+
|
75
|
+
handler.after_sync
|
76
|
+
end
|
77
|
+
end
|