gorgon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
gorgon (0.0.1)
|
5
|
+
amqp (~> 0.9.7)
|
6
|
+
awesome_print
|
7
|
+
bunny (~> 0.8.0)
|
8
|
+
colorize (~> 0.5.8)
|
9
|
+
open4 (~> 1.3.0)
|
10
|
+
ruby-progressbar (~> 1.0.1)
|
11
|
+
test-unit
|
12
|
+
uuidtools (~> 2.1.3)
|
13
|
+
yajl-ruby (~> 1.1.0)
|
14
|
+
|
15
|
+
GEM
|
16
|
+
remote: http://rubygems.org/
|
17
|
+
specs:
|
18
|
+
amq-client (0.9.4)
|
19
|
+
amq-protocol (>= 0.9.4)
|
20
|
+
eventmachine
|
21
|
+
amq-protocol (0.9.4)
|
22
|
+
amqp (0.9.7)
|
23
|
+
amq-client (~> 0.9.4)
|
24
|
+
amq-protocol (>= 0.9.4)
|
25
|
+
eventmachine
|
26
|
+
awesome_print (1.0.2)
|
27
|
+
bunny (0.8.0)
|
28
|
+
colorize (0.5.8)
|
29
|
+
diff-lcs (1.1.3)
|
30
|
+
eventmachine (1.0.0)
|
31
|
+
open4 (1.3.0)
|
32
|
+
rake (0.9.2.2)
|
33
|
+
rspec (2.8.0)
|
34
|
+
rspec-core (~> 2.8.0)
|
35
|
+
rspec-expectations (~> 2.8.0)
|
36
|
+
rspec-mocks (~> 2.8.0)
|
37
|
+
rspec-core (2.8.0)
|
38
|
+
rspec-expectations (2.8.0)
|
39
|
+
diff-lcs (~> 1.1.2)
|
40
|
+
rspec-mocks (2.8.0)
|
41
|
+
ruby-progressbar (1.0.1)
|
42
|
+
test-unit (2.5.2)
|
43
|
+
uuidtools (2.1.3)
|
44
|
+
yajl-ruby (1.1.0)
|
45
|
+
|
46
|
+
PLATFORMS
|
47
|
+
ruby
|
48
|
+
|
49
|
+
DEPENDENCIES
|
50
|
+
gorgon!
|
51
|
+
rake
|
52
|
+
rspec
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
Gorgon
|
2
|
+
=====================
|
3
|
+
|
4
|
+
About
|
5
|
+
---------------------
|
6
|
+
|
7
|
+
Gorgon provides a method for distributing the workload of running a ruby test suites. It relies on amqp for message passing, and rsync for the synchronization of source code.
|
8
|
+
|
9
|
+
Usage
|
10
|
+
---------------------
|
11
|
+
|
12
|
+
To queue the current test suite, run `bundle exec gorgon start`, or `bundle exec gorgon`. _gorgon_ will read the application configuration out of _gorgon.json_, connect to the AMQP server, and publish the job.
|
13
|
+
|
14
|
+
In order for the job to run, _gorgon job listeners_ must be started that can process the job. To start a gorgon listener, run `bundle exec gorgon listen`. This command will read the listener configuration out of _gorgon\_listener.json_, then start the listener process in the background.
|
15
|
+
|
16
|
+
Configuration
|
17
|
+
---------------------
|
18
|
+
|
19
|
+
### gorgon.json
|
20
|
+
This file contains project-specific settings for gorgon, such as:
|
21
|
+
|
22
|
+
* A glob for generating the list of test files
|
23
|
+
* The connection information for AMQP
|
24
|
+
* Information about how clients can rsync the working directory
|
25
|
+
* Files containing Ruby code to be used as callbacks
|
26
|
+
|
27
|
+
### gorgon_listener.json
|
28
|
+
This file contains the listener-specific settings, such as:
|
29
|
+
|
30
|
+
* How many worker slots are provided by this listener
|
31
|
+
* The connection information for AMQP
|
32
|
+
* The file used for logs
|
33
|
+
|
34
|
+
Architecture
|
35
|
+
---------------------
|
36
|
+
|
37
|
+
By running `bundle exec gorgon start`, the originating computer will publish a *job definition* to the AMQP server. This object contains all of the information required to run the tests:
|
38
|
+
|
39
|
+
* The rsync information with which to fetch the source tree
|
40
|
+
* The name of a AMQP queue that contains the list of files that require testing
|
41
|
+
* The name of a AMQP exchange to send replies to
|
42
|
+
* Application-specific setup/teardown, either per-job or per-worker [scheduled for post-alpha]
|
43
|
+
|
44
|
+
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
|
+
|
46
|
+
* Create a unique temporary workspace directory for the job
|
47
|
+
* Rsync the source tree to the temporary workspace
|
48
|
+
* Run per-job application-specific setup [scheduled for post-alpha]
|
49
|
+
* Invoke *n* workers, where *n* is the number of available *worker slots*.
|
50
|
+
|
51
|
+
To invoke a job worker, the listener passes the name of the *file queue*, *reply queue*, and *listener queue* to the worker initialization. After all workers have been started, the listener will block until an event appears on the *listener queue*.
|
52
|
+
|
53
|
+
The worker process will run any application-specific startup, start a test environment, and load a stub test file that dynamically pulls files out of the *file queue*. It runs the test, posts the results to the *reply queue*, and repeats until the *file queue* is empty. When the *file queue* becomes empty, the worker runs application-specific teardown, then reports its completion to the *listener queue*, and shuts down.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/gorgon
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require 'gorgon/originator'
|
3
|
+
require 'gorgon/listener'
|
4
|
+
require 'gorgon/worker_manager'
|
5
|
+
|
6
|
+
def start
|
7
|
+
o = Originator.new
|
8
|
+
o.originate
|
9
|
+
end
|
10
|
+
|
11
|
+
def listen
|
12
|
+
l = Listener.new
|
13
|
+
l.listen
|
14
|
+
end
|
15
|
+
|
16
|
+
def manage_workers
|
17
|
+
config_path = ENV["GORGON_CONFIG_PATH"]
|
18
|
+
|
19
|
+
manager = WorkerManager.build config_path
|
20
|
+
manager.manage
|
21
|
+
|
22
|
+
# For some reason I have to 'exit' here, otherwise WorkerManager process crashes
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
|
26
|
+
def usage
|
27
|
+
#print instructions on how to use gorgon
|
28
|
+
end
|
29
|
+
|
30
|
+
case ARGV[0]
|
31
|
+
when nil
|
32
|
+
start
|
33
|
+
when "start"
|
34
|
+
start
|
35
|
+
when "listen"
|
36
|
+
listen
|
37
|
+
when "manage_workers"
|
38
|
+
manage_workers
|
39
|
+
else
|
40
|
+
usage
|
41
|
+
end
|
data/gorgon.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "gorgon/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "gorgon"
|
7
|
+
s.version = Gorgon::VERSION
|
8
|
+
s.authors = ["Justin Fitzsimmons", "Sean Kirby", "Victor Savkin", "Clemens Park", "Arturo Pie"]
|
9
|
+
s.email = ["justin@fitzsimmons.ca"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Distributed testing for ruby with centralized management}
|
12
|
+
s.description = %q{Gorgon provides a method for distributing the workload of running a ruby test suites. It relies on amqp for message passing, and rsync for the synchronization of source code.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "gorgon"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
s.add_development_dependency "rake"
|
23
|
+
|
24
|
+
s.add_runtime_dependency "amqp", '~>0.9.7'
|
25
|
+
s.add_runtime_dependency "awesome_print"
|
26
|
+
s.add_runtime_dependency "open4", '~>1.3.0'
|
27
|
+
s.add_runtime_dependency "yajl-ruby", '~>1.1.0'
|
28
|
+
s.add_runtime_dependency "uuidtools", '~>2.1.3'
|
29
|
+
s.add_runtime_dependency "test-unit"
|
30
|
+
s.add_runtime_dependency "bunny", '~>0.8.0'
|
31
|
+
s.add_runtime_dependency "ruby-progressbar", '~>1.0.1'
|
32
|
+
s.add_runtime_dependency "colorize", '~>0.5.8'
|
33
|
+
end
|
data/lib/gorgon.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
require 'yajl'
|
3
|
+
|
4
|
+
class AmqpQueueDecorator
|
5
|
+
def initialize queue
|
6
|
+
@queue = queue
|
7
|
+
end
|
8
|
+
|
9
|
+
def pop
|
10
|
+
m = @queue.pop
|
11
|
+
p = m[:payload]
|
12
|
+
p == :queue_empty ? nil : p
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class AmqpExchangeDecorator
|
17
|
+
def initialize exchange
|
18
|
+
@exchange = exchange
|
19
|
+
end
|
20
|
+
|
21
|
+
def publish msg
|
22
|
+
serialized_msg = Yajl::Encoder.encode(msg)
|
23
|
+
@exchange.publish serialized_msg
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class AmqpService
|
28
|
+
def initialize connection_config
|
29
|
+
@connection_config = connection_config.merge(:spec => "09")
|
30
|
+
end
|
31
|
+
|
32
|
+
def start_worker file_queue_name, reply_exchange_name
|
33
|
+
Bunny.run @connection_config do |b|
|
34
|
+
queue = b.queue file_queue_name
|
35
|
+
exchange = b.exchange reply_exchange_name
|
36
|
+
yield AmqpQueueDecorator.new(queue), AmqpExchangeDecorator.new(exchange)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class CallbackHandler
|
2
|
+
def initialize(config)
|
3
|
+
@config = config || {}
|
4
|
+
end
|
5
|
+
|
6
|
+
def before_start
|
7
|
+
load(@config[:before_start]) if @config[:before_start]
|
8
|
+
end
|
9
|
+
|
10
|
+
def after_complete
|
11
|
+
load(@config[:after_complete]) if @config[:after_complete]
|
12
|
+
end
|
13
|
+
|
14
|
+
def before_creating_workers
|
15
|
+
load(@config[:before_creating_workers]) if @config[:before_creating_workers]
|
16
|
+
end
|
17
|
+
|
18
|
+
def after_sync
|
19
|
+
load(@config[:after_sync]) if @config[:after_sync]
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'yajl'
|
2
|
+
|
3
|
+
class FailuresPrinter
|
4
|
+
OUTPUT_FILE = "/tmp/gorgon-failed-files.json"
|
5
|
+
|
6
|
+
def initialize job_state
|
7
|
+
@job_state = job_state
|
8
|
+
@job_state.add_observer(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def update payload
|
12
|
+
return unless @job_state.is_job_complete? || @job_state.is_job_cancelled?
|
13
|
+
|
14
|
+
File.open(OUTPUT_FILE, 'w+') do |fd|
|
15
|
+
fd.write(Yajl::Encoder.encode(failed_files + unfinished_files))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def failed_files
|
22
|
+
failed_files = []
|
23
|
+
@job_state.each_failed_test do |test|
|
24
|
+
failed_files << "#{test[:filename]}"
|
25
|
+
end
|
26
|
+
failed_files
|
27
|
+
end
|
28
|
+
|
29
|
+
def unfinished_files
|
30
|
+
unfinished_files = []
|
31
|
+
@job_state.each_running_file do |hostname, filename|
|
32
|
+
unfinished_files << "#{filename}"
|
33
|
+
end
|
34
|
+
unfinished_files
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module GLogger
|
4
|
+
def initialize_logger log_file
|
5
|
+
return unless log_file
|
6
|
+
@logger =
|
7
|
+
if log_file == "-"
|
8
|
+
Logger.new($stdout)
|
9
|
+
else
|
10
|
+
Logger.new(log_file)
|
11
|
+
end
|
12
|
+
@logger.datetime_format = "%Y-%m-%d %H:%M:%S "
|
13
|
+
end
|
14
|
+
|
15
|
+
def log text
|
16
|
+
@logger.info(text) if @logger
|
17
|
+
end
|
18
|
+
|
19
|
+
def log_error text
|
20
|
+
@logger.error(text) if @logger
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class HostState
|
2
|
+
def initialize
|
3
|
+
@running_workers = {}
|
4
|
+
end
|
5
|
+
|
6
|
+
def file_started worker_id, filename
|
7
|
+
if @running_workers.has_key? worker_id
|
8
|
+
puts "WARNING: worker #{worker_id} started running a new file, but a 'finish' message has not been received for file #{@running_workers[:filename]}"
|
9
|
+
end
|
10
|
+
|
11
|
+
@running_workers[worker_id] = filename
|
12
|
+
end
|
13
|
+
|
14
|
+
def file_finished worker_id, filename
|
15
|
+
if !@running_workers.has_key? worker_id || @running_workers[:worker_id] != filename
|
16
|
+
puts "WARNING: worker #{worker_id} finished running a file, but a 'start' message for that file was not received. File: #{filename}"
|
17
|
+
end
|
18
|
+
|
19
|
+
@running_workers.delete(worker_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def total_running_workers
|
23
|
+
@running_workers.size
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_running_file
|
27
|
+
@running_workers.each_value do |filename|
|
28
|
+
yield filename
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/gorgon/job.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
class Job
|
2
|
+
def initialize(listener, job_definition)
|
3
|
+
@workers = []
|
4
|
+
@definition = job_definition
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
def add_worker
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_worker_complete
|
14
|
+
@available_worker_slots += 1
|
15
|
+
on_current_job_complete if current_job_complete?
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup_child_process
|
19
|
+
worker = ChildProcess.build("gorgon", "work", @worker_communication.name, @config_filename)
|
20
|
+
|
21
|
+
worker_output = Tempfile.new("gorgon-worker")
|
22
|
+
worker.io.stdout = worker_output
|
23
|
+
worker.io.stderr = worker_output
|
24
|
+
worker
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'yajl'
|
2
|
+
|
3
|
+
class JobDefinition
|
4
|
+
attr_accessor :file_queue_name, :reply_exchange_name, :source_tree_path, :sync_exclude, :callbacks
|
5
|
+
|
6
|
+
def initialize(opts={})
|
7
|
+
@file_queue_name = opts[:file_queue_name]
|
8
|
+
@reply_exchange_name = opts[:reply_exchange_name]
|
9
|
+
@source_tree_path = opts[:source_tree_path]
|
10
|
+
@callbacks = opts[:callbacks]
|
11
|
+
@sync_exclude = opts[:sync_exclude]
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_json
|
15
|
+
Yajl::Encoder.encode(to_hash)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
#This can probably be done with introspection somehow, but this is way easier despite being very verbose
|
21
|
+
def to_hash
|
22
|
+
{:file_queue_name => @file_queue_name, :reply_exchange_name => @reply_exchange_name, :source_tree_path => @source_tree_path, :sync_exclude => @sync_exclude, :callbacks => @callbacks}
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'gorgon/host_state'
|
2
|
+
|
3
|
+
require 'observer'
|
4
|
+
|
5
|
+
class JobState
|
6
|
+
include Observable
|
7
|
+
|
8
|
+
attr_reader :total_files, :remaining_files_count, :state
|
9
|
+
|
10
|
+
def initialize total_files
|
11
|
+
@total_files = total_files
|
12
|
+
@remaining_files_count = total_files
|
13
|
+
@failed_tests = []
|
14
|
+
@hosts = {}
|
15
|
+
|
16
|
+
if @remaining_files_count > 0
|
17
|
+
@state = :starting
|
18
|
+
else
|
19
|
+
@state = :complete
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def failed_files_count
|
24
|
+
@failed_tests.count
|
25
|
+
end
|
26
|
+
|
27
|
+
def finished_files_count
|
28
|
+
total_files - remaining_files_count
|
29
|
+
end
|
30
|
+
|
31
|
+
def file_started payload
|
32
|
+
raise_if_completed_or_cancelled
|
33
|
+
|
34
|
+
if @state == :starting
|
35
|
+
@state = :running
|
36
|
+
end
|
37
|
+
|
38
|
+
file_started_update_host_state payload
|
39
|
+
|
40
|
+
changed
|
41
|
+
notify_observers payload
|
42
|
+
end
|
43
|
+
|
44
|
+
def file_finished payload
|
45
|
+
raise_if_completed_or_cancelled
|
46
|
+
|
47
|
+
@remaining_files_count -= 1
|
48
|
+
@state = :complete if @remaining_files_count == 0
|
49
|
+
|
50
|
+
handle_failed_test payload if failed_test?(payload)
|
51
|
+
|
52
|
+
@hosts[payload[:hostname]].file_finished payload[:worker_id], payload[:filename]
|
53
|
+
|
54
|
+
changed
|
55
|
+
notify_observers payload
|
56
|
+
end
|
57
|
+
|
58
|
+
def cancel
|
59
|
+
@remaining_files_count = 0
|
60
|
+
@state = :cancelled
|
61
|
+
changed
|
62
|
+
notify_observers({})
|
63
|
+
end
|
64
|
+
|
65
|
+
def each_failed_test
|
66
|
+
@failed_tests.each do |test|
|
67
|
+
yield test
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def each_running_file
|
72
|
+
@hosts.each do |hostname, host|
|
73
|
+
host.each_running_file do |filename|
|
74
|
+
yield hostname, filename
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def total_running_hosts
|
80
|
+
@hosts.size
|
81
|
+
end
|
82
|
+
|
83
|
+
def total_running_workers
|
84
|
+
result = 0
|
85
|
+
@hosts.each do |hostname, host|
|
86
|
+
result += host.total_running_workers
|
87
|
+
end
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
def is_job_complete?
|
92
|
+
@state == :complete
|
93
|
+
end
|
94
|
+
|
95
|
+
def is_job_cancelled?
|
96
|
+
@state == :cancelled
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def file_started_update_host_state payload
|
102
|
+
hostname = payload[:hostname]
|
103
|
+
@hosts[hostname] = HostState.new if @hosts[hostname].nil?
|
104
|
+
@hosts[hostname].file_started payload[:worker_id], payload[:filename]
|
105
|
+
end
|
106
|
+
|
107
|
+
def handle_failed_test payload
|
108
|
+
@failed_tests << payload
|
109
|
+
end
|
110
|
+
|
111
|
+
def raise_if_completed_or_cancelled
|
112
|
+
raise "JobState#file_finished called when job was already complete" if is_job_complete?
|
113
|
+
raise "JobState#file_finished called after job was cancelled" if is_job_cancelled?
|
114
|
+
end
|
115
|
+
|
116
|
+
def failed_test? payload
|
117
|
+
payload[:type] == "fail"
|
118
|
+
end
|
119
|
+
end
|