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