gorgon 0.2.0 → 0.3.0

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