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
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .bundle
2
+ *.swp
3
+ .rvmrc
4
+ tags
5
+ TAGS
6
+ pkg
7
+ .idea
8
+ .rbenv-version
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in gorgon.gemspec
4
+ gemspec
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,6 @@
1
+ require "gorgon/version"
2
+ require "gorgon/originator"
3
+ require "gorgon/listener"
4
+
5
+ module Gorgon
6
+ end
@@ -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,9 @@
1
+ require "yajl"
2
+
3
+ module Configuration
4
+ extend self
5
+ def load_configuration_from_file(filename)
6
+ file = File.new(filename, "r")
7
+ Yajl::Parser.new(:symbolize_keys => true).parse(file)
8
+ end
9
+ 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