gorgon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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