gorgon 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -1
- data/Gemfile.lock +9 -9
- data/README.md +33 -9
- data/bin/gorgon +9 -0
- data/gorgon.json.sample +29 -0
- data/gorgon_listener.json.sample +8 -0
- data/lib/gorgon/colors.rb +1 -0
- data/lib/gorgon/crash_reporter.rb +19 -0
- data/lib/gorgon/gem_command_handler.rb +46 -0
- data/lib/gorgon/gem_service.rb +77 -0
- data/lib/gorgon/job_state.rb +4 -4
- data/lib/gorgon/listener.rb +26 -21
- data/lib/gorgon/originator_protocol.rb +9 -8
- data/lib/gorgon/ping_service.rb +2 -2
- data/lib/gorgon/pipe_forker.rb +22 -0
- data/lib/gorgon/source_tree_syncer.rb +2 -1
- data/lib/gorgon/version.rb +1 -1
- data/lib/gorgon/worker.rb +64 -30
- data/lib/gorgon/worker_manager.rb +42 -12
- data/spec/crash_reporter_spec.rb +46 -0
- data/spec/gem_command_handler_spec.rb +84 -0
- data/spec/gem_service_spec.rb +74 -0
- data/spec/job_state_spec.rb +0 -7
- data/spec/listener_spec.rb +37 -19
- data/spec/originator_protocol_spec.rb +12 -9
- data/spec/ping_service_spec.rb +3 -3
- data/spec/pipe_forker_spec.rb +53 -0
- data/spec/worker_manager_spec.rb +34 -5
- data/spec/worker_spec.rb +63 -1
- metadata +12 -4
- data/lib/gorgon/pipe_manager.rb +0 -55
- data/lib/gorgon/worker_watcher.rb +0 -22
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
gorgon (0.
|
4
|
+
gorgon (0.3.0)
|
5
5
|
amqp (~> 0.9.7)
|
6
6
|
awesome_print
|
7
7
|
bunny (~> 0.8.0)
|
@@ -30,14 +30,14 @@ GEM
|
|
30
30
|
eventmachine (1.0.0)
|
31
31
|
open4 (1.3.0)
|
32
32
|
rake (0.9.2.2)
|
33
|
-
rspec (2.
|
34
|
-
rspec-core (~> 2.
|
35
|
-
rspec-expectations (~> 2.
|
36
|
-
rspec-mocks (~> 2.
|
37
|
-
rspec-core (2.
|
38
|
-
rspec-expectations (2.
|
39
|
-
diff-lcs (~> 1.1.
|
40
|
-
rspec-mocks (2.
|
33
|
+
rspec (2.11.0)
|
34
|
+
rspec-core (~> 2.11.0)
|
35
|
+
rspec-expectations (~> 2.11.0)
|
36
|
+
rspec-mocks (~> 2.11.0)
|
37
|
+
rspec-core (2.11.1)
|
38
|
+
rspec-expectations (2.11.3)
|
39
|
+
diff-lcs (~> 1.1.3)
|
40
|
+
rspec-mocks (2.11.3)
|
41
41
|
ruby-progressbar (1.0.1)
|
42
42
|
test-unit (2.5.2)
|
43
43
|
uuidtools (2.1.3)
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@ Gorgon
|
|
4
4
|
About
|
5
5
|
---------------------
|
6
6
|
|
7
|
-
Gorgon provides a method for distributing the workload of running
|
7
|
+
Gorgon provides a method for distributing the workload of running ruby test suites. It relies on amqp for message passing, and rsync for the synchronization of source code.
|
8
8
|
|
9
9
|
Usage
|
10
10
|
---------------------
|
@@ -19,18 +19,24 @@ Configuration
|
|
19
19
|
### gorgon.json
|
20
20
|
This file contains project-specific settings for gorgon, such as:
|
21
21
|
|
22
|
-
* A glob for generating the list of test files
|
23
22
|
* The connection information for AMQP
|
24
|
-
* Information about how clients can rsync the working directory
|
23
|
+
* Information about how clients can rsync the working directory (optional)
|
24
|
+
* Files that can be excluded by rsync
|
25
25
|
* Files containing Ruby code to be used as callbacks
|
26
|
+
* A glob for generating the list of test files
|
27
|
+
* The file used for Originator's logs
|
28
|
+
|
29
|
+
See [gorgon.json example](https://github.com/Fitzsimmons/Gorgon/blob/master/gorgon.json.sample) for more details.
|
26
30
|
|
27
31
|
### gorgon_listener.json
|
28
32
|
This file contains the listener-specific settings, such as:
|
29
33
|
|
30
|
-
* How many worker slots are provided by this listener
|
31
34
|
* The connection information for AMQP
|
35
|
+
* How many worker slots are provided by this listener
|
32
36
|
* The file used for logs
|
33
37
|
|
38
|
+
See [gorgon_listener.json example](https://github.com/Fitzsimmons/Gorgon/blob/master/gorgon_listener.json.sample) for more details.
|
39
|
+
|
34
40
|
Architecture
|
35
41
|
---------------------
|
36
42
|
|
@@ -39,15 +45,33 @@ By running `bundle exec gorgon start`, the originating computer will publish a *
|
|
39
45
|
* The rsync information with which to fetch the source tree
|
40
46
|
* The name of a AMQP queue that contains the list of files that require testing
|
41
47
|
* The name of a AMQP exchange to send replies to
|
42
|
-
* Application-specific setup/teardown, either per-job or per-worker
|
48
|
+
* Application-specific setup/teardown, either per-job or per-worker (callbacks)
|
43
49
|
|
44
50
|
The job listener subscribes to the job publish event, and maintains its own queue of jobs. When a job has available *worker slots*, it will prepare the workspace:
|
45
51
|
|
46
52
|
* Create a unique temporary workspace directory for the job
|
47
53
|
* Rsync the source tree to the temporary workspace
|
48
|
-
* Run
|
49
|
-
* Invoke
|
54
|
+
* Run after_sync callback
|
55
|
+
* Invoke a WorkerManager
|
56
|
+
|
57
|
+
After WorkerManager starts, it will:
|
58
|
+
* Run before\_creating\_workers callback
|
59
|
+
* Fork *n* workers, where *n* is the number of available *worker slots*.
|
60
|
+
* Subscribe to a queue where originator can send a cancel_job message
|
61
|
+
|
62
|
+
Each Worker will:
|
63
|
+
* Run before_start callback
|
64
|
+
* Pop a file from file queue and run it until file queue is empty, or WorkerManager sends an INT signal. For each file, it post a 'start_message' and a 'finish_message' with the results to the *reply queue*
|
65
|
+
* Run after_complete callback
|
50
66
|
|
51
|
-
To invoke
|
67
|
+
To invoke the worker manager, the listener passes the name of the *file queue*, *reply queue*, and *listener queue* to the worker manager initialization, and then it will block until worker manager finishes.
|
68
|
+
|
69
|
+
Contributors
|
70
|
+
---------------------
|
71
|
+
* Justin Fitzsimmons
|
72
|
+
* Arturo Pie
|
73
|
+
* Sean Kirby
|
74
|
+
* Clemens Park
|
75
|
+
* Victor Savkin
|
52
76
|
|
53
|
-
|
77
|
+
Gorgon is funded by [Nulogy Corp](http://www.nulogy.com/).
|
data/bin/gorgon
CHANGED
@@ -3,6 +3,7 @@ require 'gorgon/originator'
|
|
3
3
|
require 'gorgon/listener'
|
4
4
|
require 'gorgon/worker_manager'
|
5
5
|
require 'gorgon/ping_service'
|
6
|
+
require 'gorgon/gem_service'
|
6
7
|
require 'gorgon/version'
|
7
8
|
|
8
9
|
WELCOME_MSG = "Welcome to Gorgon #{Gorgon::VERSION}"
|
@@ -31,11 +32,16 @@ def ping_listeners
|
|
31
32
|
PingService.new.ping_listeners
|
32
33
|
end
|
33
34
|
|
35
|
+
def run_gem command
|
36
|
+
GemService.new.run command
|
37
|
+
end
|
38
|
+
|
34
39
|
def usage
|
35
40
|
#print instructions on how to use gorgon
|
36
41
|
puts "\tstart - remotely runs all tests specified in gorgon.json"
|
37
42
|
puts "\tlisten - starts a listener process using the settings in gorgon_listener.json"
|
38
43
|
puts "\tping - pings listeners and shows hosts and gorgon's version they are running"
|
44
|
+
puts "\tgem command [options...] - execute the gem command on every listener and shutdown listener. e.g. 'gorgon gem install --version 1.0.0'"
|
39
45
|
end
|
40
46
|
|
41
47
|
puts WELCOME_MSG
|
@@ -51,6 +57,9 @@ when "manage_workers"
|
|
51
57
|
manage_workers
|
52
58
|
when "ping"
|
53
59
|
ping_listeners
|
60
|
+
when "gem"
|
61
|
+
ARGV.shift
|
62
|
+
run_gem ARGV.join(' ')
|
54
63
|
when "help"
|
55
64
|
usage
|
56
65
|
else
|
data/gorgon.json.sample
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
{
|
2
|
+
"connection": {
|
3
|
+
"host": "localhost"
|
4
|
+
},
|
5
|
+
|
6
|
+
"job": {
|
7
|
+
"sync_exclude": [
|
8
|
+
"tmp",
|
9
|
+
"log",
|
10
|
+
"doc",
|
11
|
+
".git",
|
12
|
+
".rvmrc"
|
13
|
+
],
|
14
|
+
"callbacks": {
|
15
|
+
"before_start": "test/gorgon_callbacks/before_start_callback.rb",
|
16
|
+
"after_complete": "test/gorgon_callbacks/after_complete.rb",
|
17
|
+
"before_creating_workers": "test/gorgon_callbacks/before_creating_workers.rb",
|
18
|
+
"after_sync": "test/gorgon_callbacks/after_sync.rb"
|
19
|
+
}
|
20
|
+
},
|
21
|
+
|
22
|
+
"files": [
|
23
|
+
"test/unit/**/*_test.rb",
|
24
|
+
"test/functional/**/*_test.rb",
|
25
|
+
"test/integration/**/*_test.rb"
|
26
|
+
],
|
27
|
+
|
28
|
+
"originator_log_file": "log/gorgon-orginator.log"
|
29
|
+
}
|
data/lib/gorgon/colors.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
module CrashReporter
|
2
|
+
OUTPUT_LINES_TO_REPORT = 70
|
3
|
+
|
4
|
+
def report_crash reply_exchange, info
|
5
|
+
stdout = `tail -n #{OUTPUT_LINES_TO_REPORT} #{info[:out_file]}`
|
6
|
+
stderr = `tail -n #{OUTPUT_LINES_TO_REPORT} #{info[:err_file]}` + \
|
7
|
+
info[:footer_text]
|
8
|
+
|
9
|
+
send_crash_message reply_exchange, stdout, stderr
|
10
|
+
|
11
|
+
"#{stdout}\n#{stderr}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def send_crash_message reply_exchange, output, error
|
15
|
+
reply = {:type => :crash, :hostname => Socket.gethostname,
|
16
|
+
:stdout => output, :stderr => error}
|
17
|
+
reply_exchange.publish(Yajl::Encoder.encode(reply))
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "socket"
|
2
|
+
require "yajl"
|
3
|
+
require "bunny"
|
4
|
+
require 'open4'
|
5
|
+
|
6
|
+
class GemCommandHandler
|
7
|
+
def initialize bunny
|
8
|
+
@bunny = bunny
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle payload, configuration
|
12
|
+
reply_exchange_name = payload[:reply_exchange_name]
|
13
|
+
publish_to reply_exchange_name, :type => :running_command
|
14
|
+
|
15
|
+
gem = configuration[:bin_gem_path] || "gem"
|
16
|
+
|
17
|
+
cmd = "#{gem} #{payload[:body][:gem_command]} gorgon"
|
18
|
+
pid, stdin, stdout, stderr = Open4::popen4 cmd
|
19
|
+
stdin.close
|
20
|
+
|
21
|
+
ignore, status = Process.waitpid2 pid
|
22
|
+
exitstatus = status.exitstatus
|
23
|
+
|
24
|
+
output, errors = [stdout, stderr].map { |p| begin p.read ensure p.close end }
|
25
|
+
|
26
|
+
if exitstatus == 0
|
27
|
+
reply = {:type => :command_completed, :command => cmd, :stdout => output,
|
28
|
+
:stderr => errors}
|
29
|
+
publish_to reply_exchange_name, reply
|
30
|
+
@bunny.stop
|
31
|
+
exit # TODO: for now exit until we implement a command to exit listeners
|
32
|
+
else
|
33
|
+
reply = {:type => :command_failed, :command => cmd, :stdout => output, :stderr => errors}
|
34
|
+
publish_to reply_exchange_name, reply
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# TODO: factors this out to a class
|
41
|
+
def publish_to reply_exchange_name, message
|
42
|
+
reply_exchange = @bunny.exchange(reply_exchange_name, :auto_delete => true
|
43
|
+
)
|
44
|
+
reply_exchange.publish(Yajl::Encoder.encode(message.merge(:hostname => Socket.gethostname)))
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'gorgon/originator_protocol'
|
2
|
+
require 'gorgon/originator_logger'
|
3
|
+
require 'gorgon/configuration'
|
4
|
+
|
5
|
+
class GemService
|
6
|
+
include Configuration
|
7
|
+
|
8
|
+
TIMEOUT = 3
|
9
|
+
def initialize
|
10
|
+
@configuration = load_configuration_from_file("gorgon.json")
|
11
|
+
@logger = OriginatorLogger.new @configuration[:originator_log_file]
|
12
|
+
@protocol = OriginatorProtocol.new @logger
|
13
|
+
@hosts_running = []
|
14
|
+
@started_running = 0
|
15
|
+
@finished_running = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def run command
|
19
|
+
EM.run do
|
20
|
+
@logger.log "Connecting..."
|
21
|
+
@protocol.connect @configuration[:connection], :on_closed => proc {EM.stop}
|
22
|
+
|
23
|
+
@logger.log "Sending gem command #{command}..."
|
24
|
+
@protocol.send_message_to_listeners :gem_command, :gem_command => command
|
25
|
+
|
26
|
+
@protocol.receive_payloads do |payload|
|
27
|
+
@logger.log "Received #{payload}"
|
28
|
+
|
29
|
+
handle_reply(Yajl::Parser.new(:symbolize_keys => true).parse(payload))
|
30
|
+
end
|
31
|
+
|
32
|
+
EM.add_periodic_timer(TIMEOUT) { disconnect_if_none_running }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def disconnect_if_none_running
|
39
|
+
disconnect if @hosts_running.empty?
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle_reply payload
|
43
|
+
hostname = payload[:hostname].colorize(Colors::HOST)
|
44
|
+
command = payload[:command].colorize(Colors::COMMAND) if payload[:command]
|
45
|
+
|
46
|
+
case payload[:type]
|
47
|
+
when "running_command"
|
48
|
+
puts "#{hostname} is running command #{payload[:command]}..."
|
49
|
+
@hosts_running << payload[:hostname]
|
50
|
+
@started_running += 1
|
51
|
+
when "command_completed"
|
52
|
+
puts "Command '#{command}' completed in #{hostname}"
|
53
|
+
command_finished payload
|
54
|
+
when "command_failed"
|
55
|
+
puts "Command '#{command}' failed in #{hostname}."
|
56
|
+
command_finished payload
|
57
|
+
else
|
58
|
+
puts "Unknown message received: #{payload}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def command_finished payload
|
63
|
+
puts "Output:\n#{payload[:stdout]}#{payload[:stderr]}"
|
64
|
+
@hosts_running.delete payload[:hostname]
|
65
|
+
@finished_running += 1
|
66
|
+
end
|
67
|
+
|
68
|
+
def disconnect
|
69
|
+
@protocol.disconnect
|
70
|
+
print_summary
|
71
|
+
end
|
72
|
+
|
73
|
+
def print_summary
|
74
|
+
puts "#{@started_running} host(s) started running the command. #{@finished_running} host(s) reported they finished"
|
75
|
+
puts "Use 'gorgon ping' to check if all listeners are running the correct gorgon version."
|
76
|
+
end
|
77
|
+
end
|
data/lib/gorgon/job_state.rb
CHANGED
@@ -30,7 +30,7 @@ class JobState
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def file_started payload
|
33
|
-
|
33
|
+
raise_if_completed
|
34
34
|
|
35
35
|
if @state == :starting
|
36
36
|
@state = :running
|
@@ -43,7 +43,7 @@ class JobState
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def file_finished payload
|
46
|
-
|
46
|
+
raise_if_completed
|
47
47
|
|
48
48
|
@remaining_files_count -= 1
|
49
49
|
@state = :complete if @remaining_files_count == 0
|
@@ -115,9 +115,9 @@ class JobState
|
|
115
115
|
@failed_tests << payload
|
116
116
|
end
|
117
117
|
|
118
|
-
def
|
118
|
+
def raise_if_completed
|
119
119
|
raise "JobState#file_finished called when job was already complete" if is_job_complete?
|
120
|
-
|
120
|
+
puts "NOTICE: JobState#file_finished called after job was cancelled" if is_job_cancelled?
|
121
121
|
end
|
122
122
|
|
123
123
|
def failed_test? payload
|
data/lib/gorgon/listener.rb
CHANGED
@@ -4,6 +4,9 @@ require 'gorgon/source_tree_syncer'
|
|
4
4
|
require "gorgon/g_logger"
|
5
5
|
require "gorgon/callback_handler"
|
6
6
|
require "gorgon/version"
|
7
|
+
require "gorgon/worker_manager"
|
8
|
+
require "gorgon/crash_reporter"
|
9
|
+
require "gorgon/gem_command_handler"
|
7
10
|
|
8
11
|
require "yajl"
|
9
12
|
require "bunny"
|
@@ -11,11 +14,11 @@ require "awesome_print"
|
|
11
14
|
require "open4"
|
12
15
|
require "tmpdir"
|
13
16
|
require "socket"
|
14
|
-
require "bundler"
|
15
17
|
|
16
18
|
class Listener
|
17
19
|
include Configuration
|
18
20
|
include GLogger
|
21
|
+
include CrashReporter
|
19
22
|
|
20
23
|
def initialize
|
21
24
|
@listener_config_filename = Dir.pwd + "/gorgon_listener.json"
|
@@ -63,7 +66,9 @@ class Listener
|
|
63
66
|
when "job_definition"
|
64
67
|
run_job(payload)
|
65
68
|
when "ping"
|
66
|
-
|
69
|
+
respond_to_ping payload[:reply_exchange_name]
|
70
|
+
when "gem_command"
|
71
|
+
GemCommandHandler.new(@bunny).handle payload, configuration
|
67
72
|
end
|
68
73
|
end
|
69
74
|
|
@@ -79,9 +84,7 @@ class Listener
|
|
79
84
|
return
|
80
85
|
end
|
81
86
|
|
82
|
-
|
83
|
-
fork_worker_manager
|
84
|
-
end
|
87
|
+
fork_worker_manager
|
85
88
|
|
86
89
|
clean_up
|
87
90
|
end
|
@@ -120,7 +123,7 @@ class Listener
|
|
120
123
|
if @syncer.success?
|
121
124
|
log "Command '#{@syncer.sys_command}' completed successfully."
|
122
125
|
else
|
123
|
-
send_crash_message @syncer.output, @syncer.errors
|
126
|
+
send_crash_message @reply_exchange, @syncer.output, @syncer.errors
|
124
127
|
log_error "Command '#{@syncer.sys_command}' failed!"
|
125
128
|
log_error "Stdout:\n#{@syncer.output}"
|
126
129
|
log_error "Stderr:\n#{@syncer.errors}"
|
@@ -131,10 +134,12 @@ class Listener
|
|
131
134
|
@syncer.remove_temp_dir
|
132
135
|
end
|
133
136
|
|
137
|
+
ERROR_FOOTER_TEXT = "\n***** See #{WorkerManager::STDERR_FILE} and #{WorkerManager::STDOUT_FILE} at '#{Socket.gethostname}' for more details *****\n"
|
134
138
|
def fork_worker_manager
|
135
139
|
log "Forking Worker Manager..."
|
136
140
|
ENV["GORGON_CONFIG_PATH"] = @listener_config_filename
|
137
|
-
|
141
|
+
|
142
|
+
pid, stdin = Open4::popen4 "gorgon manage_workers"
|
138
143
|
stdin.write(@job_definition.to_json)
|
139
144
|
stdin.close
|
140
145
|
|
@@ -142,21 +147,27 @@ class Listener
|
|
142
147
|
log "Worker Manager #{pid} finished"
|
143
148
|
|
144
149
|
if status.exitstatus != 0
|
145
|
-
|
146
|
-
|
147
|
-
log_error "ERROR MSG: #{error_msg}"
|
150
|
+
exitstatus = status.exitstatus
|
151
|
+
log_error "Worker Manager #{pid} crashed with exit status #{exitstatus}!"
|
148
152
|
|
149
|
-
|
153
|
+
msg = report_crash @reply_exchange, :out_file => WorkerManager::STDOUT_FILE,
|
154
|
+
:err_file => WorkerManager::STDERR_FILE, :footer_text => ERROR_FOOTER_TEXT
|
155
|
+
|
156
|
+
log_error "Process output:\n#{msg}"
|
150
157
|
end
|
151
158
|
end
|
152
159
|
|
153
|
-
def
|
160
|
+
def respond_to_ping reply_exchange_name
|
154
161
|
reply = {:type => "ping_response", :hostname => Socket.gethostname,
|
155
|
-
:version => Gorgon::VERSION}
|
162
|
+
:version => Gorgon::VERSION, :worker_slots => configuration[:worker_slots]}
|
163
|
+
publish_to reply_exchange_name, reply
|
164
|
+
end
|
165
|
+
|
166
|
+
def publish_to reply_exchange_name, message
|
156
167
|
reply_exchange = @bunny.exchange(reply_exchange_name, :auto_delete => true)
|
157
168
|
|
158
|
-
log "Sending #{
|
159
|
-
reply_exchange.publish(Yajl::Encoder.encode(
|
169
|
+
log "Sending #{message}"
|
170
|
+
reply_exchange.publish(Yajl::Encoder.encode(message))
|
160
171
|
end
|
161
172
|
|
162
173
|
def connection_information
|
@@ -166,10 +177,4 @@ class Listener
|
|
166
177
|
def configuration
|
167
178
|
@configuration ||= load_configuration_from_file("gorgon_listener.json")
|
168
179
|
end
|
169
|
-
|
170
|
-
def send_crash_message output, error
|
171
|
-
reply = {:type => :crash, :hostname => Socket.gethostname,
|
172
|
-
:stdout => output, :stderr => error}
|
173
|
-
@reply_exchange.publish(Yajl::Encoder.encode(reply))
|
174
|
-
end
|
175
180
|
end
|