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 CHANGED
@@ -5,4 +5,6 @@ tags
5
5
  TAGS
6
6
  pkg
7
7
  .idea
8
- .rbenv-version
8
+ .rbenv-version
9
+ *.log
10
+ *.json
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gorgon (0.2.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.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)
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 a ruby test suites. It relies on amqp for message passing, and rsync for the synchronization of source code.
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 [scheduled for post-alpha]
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 per-job application-specific setup [scheduled for post-alpha]
49
- * Invoke *n* workers, where *n* is the number of available *worker slots*.
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 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*.
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
- 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.
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
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "connection": {
3
+ "host": "localhost"
4
+ },
5
+
6
+ "worker_slots": 3,
7
+ "log_file": "/tmp/gorgon-remote.log"
8
+ }
@@ -1,4 +1,5 @@
1
1
  module Colors
2
2
  FILENAME = :light_cyan
3
3
  HOST = :light_blue
4
+ COMMAND = :yellow
4
5
  end
@@ -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
@@ -30,7 +30,7 @@ class JobState
30
30
  end
31
31
 
32
32
  def file_started payload
33
- raise_if_completed_or_cancelled
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
- raise_if_completed_or_cancelled
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 raise_if_completed_or_cancelled
118
+ def raise_if_completed
119
119
  raise "JobState#file_finished called when job was already complete" if is_job_complete?
120
- raise "JobState#file_finished called after job was cancelled" if is_job_cancelled?
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
@@ -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
- respong_to_ping payload[:reply_exchange_name]
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
- Bundler.with_clean_env do
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
- pid, stdin, stdout, stderr = Open4::popen4 "bundle exec gorgon manage_workers"
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
- log_error "Worker Manager #{pid} crashed with exit status #{status.exitstatus}!"
146
- error_msg = stderr.read
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
- send_crash_message stdout.read, error_msg
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 respong_to_ping reply_exchange_name
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 #{reply}"
159
- reply_exchange.publish(Yajl::Encoder.encode(reply))
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