gorgon 0.0.1
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/.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
|