gorgon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +8 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +52 -0
  4. data/README.md +53 -0
  5. data/Rakefile +1 -0
  6. data/bin/gorgon +41 -0
  7. data/gorgon.gemspec +33 -0
  8. data/lib/gorgon.rb +6 -0
  9. data/lib/gorgon/amqp_service.rb +39 -0
  10. data/lib/gorgon/callback_handler.rb +21 -0
  11. data/lib/gorgon/configuration.rb +9 -0
  12. data/lib/gorgon/failures_printer.rb +37 -0
  13. data/lib/gorgon/g_logger.rb +22 -0
  14. data/lib/gorgon/host_state.rb +31 -0
  15. data/lib/gorgon/job.rb +26 -0
  16. data/lib/gorgon/job_definition.rb +24 -0
  17. data/lib/gorgon/job_state.rb +119 -0
  18. data/lib/gorgon/listener.rb +147 -0
  19. data/lib/gorgon/originator.rb +120 -0
  20. data/lib/gorgon/originator_logger.rb +36 -0
  21. data/lib/gorgon/originator_protocol.rb +65 -0
  22. data/lib/gorgon/pipe_manager.rb +55 -0
  23. data/lib/gorgon/progress_bar_view.rb +121 -0
  24. data/lib/gorgon/source_tree_syncer.rb +37 -0
  25. data/lib/gorgon/testunit_runner.rb +50 -0
  26. data/lib/gorgon/version.rb +3 -0
  27. data/lib/gorgon/worker.rb +103 -0
  28. data/lib/gorgon/worker_manager.rb +148 -0
  29. data/lib/gorgon/worker_watcher.rb +22 -0
  30. data/spec/callback_handler_spec.rb +77 -0
  31. data/spec/failures_printer_spec.rb +66 -0
  32. data/spec/host_state_spec.rb +65 -0
  33. data/spec/job_definition_spec.rb +20 -0
  34. data/spec/job_state_spec.rb +231 -0
  35. data/spec/listener_spec.rb +194 -0
  36. data/spec/originator_logger_spec.rb +40 -0
  37. data/spec/originator_protocol_spec.rb +134 -0
  38. data/spec/originator_spec.rb +134 -0
  39. data/spec/progress_bar_view_spec.rb +98 -0
  40. data/spec/source_tree_syncer_spec.rb +65 -0
  41. data/spec/worker_manager_spec.rb +23 -0
  42. data/spec/worker_spec.rb +114 -0
  43. 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,3 @@
1
+ module Gorgon
2
+ VERSION = "0.0.1"
3
+ 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