gorgon 0.2.0 → 0.3.0
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 +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
|