cloud-crowd 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/cloud-crowd.gemspec +2 -2
- data/config/config.example.yml +8 -4
- data/lib/cloud-crowd.rb +19 -1
- data/lib/cloud_crowd/action.rb +5 -2
- data/lib/cloud_crowd/command_line.rb +74 -18
- data/lib/cloud_crowd/models/job.rb +3 -1
- data/lib/cloud_crowd/models/node_record.rb +1 -1
- data/lib/cloud_crowd/models/work_unit.rb +14 -11
- data/lib/cloud_crowd/node.rb +25 -18
- data/lib/cloud_crowd/worker.rb +30 -37
- data/public/css/admin_console.css +12 -12
- data/public/js/admin_console.js +3 -3
- data/test/unit/test_action.rb +23 -2
- data/test/unit/test_job.rb +5 -0
- data/test/unit/test_node.rb +2 -3
- data/views/operations_center.erb +8 -8
- metadata +2 -2
data/cloud-crowd.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'cloud-crowd'
|
3
|
-
s.version = '0.2.
|
4
|
-
s.date = '2009-09-
|
3
|
+
s.version = '0.2.2' # Keep version in sync with cloud-cloud.rb
|
4
|
+
s.date = '2009-09-23'
|
5
5
|
|
6
6
|
s.homepage = "http://wiki.github.com/documentcloud/cloud-crowd"
|
7
7
|
s.summary = "Parallel Processing for the Rest of Us"
|
data/config/config.example.yml
CHANGED
@@ -29,10 +29,14 @@
|
|
29
29
|
:s3_bucket: [your CloudCrowd bucket]
|
30
30
|
:s3_authentication: no
|
31
31
|
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
32
|
+
# The following settings configure local paths. 'local_storage_path' is the
|
33
|
+
# directory in which all files will be saved if you're using the 'filesystem'
|
34
|
+
# storage. 'log_path' and 'pid_path' are the directories in which daemonized
|
35
|
+
# servers and nodes will store their process ids and log files. The default
|
36
|
+
# values are listed.
|
37
|
+
# :local_storage_path: /tmp/cloud_crowd_storage
|
38
|
+
# :log_path: log
|
39
|
+
# :pid_path: tmp/pids
|
36
40
|
|
37
41
|
# Use HTTP Basic Auth for all requests? (Includes all internal worker requests
|
38
42
|
# to the central server). If yes, specify the login and password that all
|
data/lib/cloud-crowd.rb
CHANGED
@@ -43,13 +43,19 @@ module CloudCrowd
|
|
43
43
|
autoload :WorkUnit, 'cloud_crowd/models'
|
44
44
|
|
45
45
|
# Keep this version in sync with the gemspec.
|
46
|
-
VERSION = '0.2.
|
46
|
+
VERSION = '0.2.2'
|
47
47
|
|
48
48
|
# Increment the schema version when there's a backwards incompatible change.
|
49
49
|
SCHEMA_VERSION = 3
|
50
50
|
|
51
51
|
# Root directory of the CloudCrowd gem.
|
52
52
|
ROOT = File.expand_path(File.dirname(__FILE__) + '/..')
|
53
|
+
|
54
|
+
# Default folder to log daemonized servers and nodes into.
|
55
|
+
LOG_PATH = 'log'
|
56
|
+
|
57
|
+
# Default folder to contain the pids of daemonized servers and nodes.
|
58
|
+
PID_PATH = 'tmp/pids'
|
53
59
|
|
54
60
|
# A Job is processing if its WorkUnits are in the queue to be handled by nodes.
|
55
61
|
PROCESSING = 1
|
@@ -107,6 +113,18 @@ module CloudCrowd
|
|
107
113
|
@central_server ||= RestClient::Resource.new(CloudCrowd.config[:central_server], CloudCrowd.client_options)
|
108
114
|
end
|
109
115
|
|
116
|
+
# The path that daemonized servers and nodes will log to.
|
117
|
+
def log_path(log_file=nil)
|
118
|
+
@log_path ||= config[:log_path] || LOG_PATH
|
119
|
+
log_file ? File.join(@log_path, log_file) : @log_path
|
120
|
+
end
|
121
|
+
|
122
|
+
# The path in which daemonized servers and nodes will store their pids.
|
123
|
+
def pid_path(pid_file=nil)
|
124
|
+
@pid_path ||= config[:pid_path] || PID_PATH
|
125
|
+
pid_file ? File.join(@pid_path, pid_file) : @pid_path
|
126
|
+
end
|
127
|
+
|
110
128
|
# The standard RestClient options for the central server talking to nodes,
|
111
129
|
# as well as the other way around. There's a timeout of 5 seconds to open
|
112
130
|
# a connection, and a timeout of 30 to finish reading it.
|
data/lib/cloud_crowd/action.rb
CHANGED
@@ -103,10 +103,13 @@ module CloudCrowd
|
|
103
103
|
@input = JSON.parse(@input)
|
104
104
|
end
|
105
105
|
|
106
|
+
def input_is_url?
|
107
|
+
!URI.parse(@input).scheme.nil? rescue false
|
108
|
+
end
|
109
|
+
|
106
110
|
# If the input is a URL, download the file before beginning processing.
|
107
111
|
def download_input
|
108
|
-
|
109
|
-
return unless input_is_url
|
112
|
+
return unless input_is_url?
|
110
113
|
Dir.chdir(@work_directory) do
|
111
114
|
@input_path = File.join(@work_directory, safe_filename(@input))
|
112
115
|
@file_name = File.basename(@input_path, File.extname(@input_path))
|
@@ -24,6 +24,9 @@ Commands:
|
|
24
24
|
node Start up a worker node (only one node per machine, please)
|
25
25
|
console Launch a CloudCrowd console, connected to the central database
|
26
26
|
load_schema Load the schema into the database specified by database.yml
|
27
|
+
|
28
|
+
server -d [start | stop | restart] Servers and nodes can be launched as
|
29
|
+
node -d [start | stop | restart] daemons, then stopped or restarted.
|
27
30
|
|
28
31
|
Options:
|
29
32
|
EOS
|
@@ -31,11 +34,12 @@ Options:
|
|
31
34
|
# Creating a CloudCrowd::CommandLine runs from the contents of ARGV.
|
32
35
|
def initialize
|
33
36
|
parse_options
|
34
|
-
command
|
37
|
+
command = ARGV.shift
|
38
|
+
subcommand = ARGV.shift
|
35
39
|
case command
|
36
40
|
when 'console' then run_console
|
37
|
-
when 'server' then run_server
|
38
|
-
when 'node' then run_node
|
41
|
+
when 'server' then run_server(subcommand)
|
42
|
+
when 'node' then run_node(subcommand)
|
39
43
|
when 'load_schema' then run_load_schema
|
40
44
|
when 'install' then run_install
|
41
45
|
else usage
|
@@ -53,29 +57,77 @@ Options:
|
|
53
57
|
IRB.start
|
54
58
|
end
|
55
59
|
|
56
|
-
#
|
57
|
-
|
58
|
-
# should use the config.ru rackup file directly. This method will start
|
59
|
-
# a single Thin server, if Thin is installed, otherwise the rackup defaults
|
60
|
-
# (Mongrel, falling back to WEBrick). The equivalent of Rails' script/server.
|
61
|
-
def run_server
|
60
|
+
# `crowd server` can either 'start', 'stop', or 'restart'.
|
61
|
+
def run_server(subcommand)
|
62
62
|
ensure_config
|
63
|
-
|
64
|
-
|
63
|
+
load_code
|
64
|
+
subcommand ||= 'start'
|
65
|
+
case subcommand
|
66
|
+
when 'start' then start_server
|
67
|
+
when 'stop' then stop_server
|
68
|
+
when 'restart' then restart_server
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Convenience command for quickly spinning up the central server. More
|
73
|
+
# sophisticated deployments, load-balancing across multiple app servers,
|
74
|
+
# should use the config.ru rackup file directly. This method will start
|
75
|
+
# a single Thin server.
|
76
|
+
def start_server
|
77
|
+
port = @options[:port] || 9173
|
78
|
+
daemonize = @options[:daemonize] ? '-d' : ''
|
79
|
+
log_path = CloudCrowd.log_path('server.log')
|
80
|
+
pid_path = CloudCrowd.pid_path('server.pid')
|
65
81
|
rackup_path = File.expand_path("#{@options[:config_path]}/config.ru")
|
66
|
-
if
|
67
|
-
|
68
|
-
|
69
|
-
|
82
|
+
FileUtils.mkdir_p(CloudCrowd.log_path) if @options[:daemonize] && !File.exists?(CloudCrowd.log_path)
|
83
|
+
puts "Starting CloudCrowd Central Server on port #{port}..."
|
84
|
+
exec "thin -e #{@options[:environment]} -p #{port} #{daemonize} --tag cloud-crowd-server --log #{log_path} --pid #{pid_path} -R #{rackup_path} start"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Stop the daemonized central server, if it exists.
|
88
|
+
def stop_server
|
89
|
+
Thin::Server.kill(CloudCrowd.pid_path('server.pid'), 0)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Restart the daemonized central server.
|
93
|
+
def restart_server
|
94
|
+
stop_server
|
95
|
+
sleep 1
|
96
|
+
start_server
|
97
|
+
end
|
98
|
+
|
99
|
+
# `crowd node` can either 'start', 'stop', or 'restart'.
|
100
|
+
def run_node(subcommand)
|
101
|
+
ensure_config
|
102
|
+
load_code
|
103
|
+
subcommand ||= 'start'
|
104
|
+
case subcommand
|
105
|
+
when 'start' then start_node
|
106
|
+
when 'stop' then stop_node
|
107
|
+
when 'restart' then restart_node
|
70
108
|
end
|
71
109
|
end
|
72
110
|
|
73
111
|
# Launch a Node. Please only run a single node per machine. The Node process
|
74
112
|
# will be long-lived, although its workers will come and go.
|
75
|
-
def
|
113
|
+
def start_node
|
76
114
|
ENV['RACK_ENV'] = @options['environment']
|
77
115
|
load_code
|
78
|
-
|
116
|
+
port = @options[:port] || Node::DEFAULT_PORT
|
117
|
+
puts "Starting CloudCrowd Node on port #{port}..."
|
118
|
+
Node.new(port, @options[:daemonize])
|
119
|
+
end
|
120
|
+
|
121
|
+
# If the daemonized Node is running, stop it.
|
122
|
+
def stop_node
|
123
|
+
Thin::Server.kill CloudCrowd.pid_path('node.pid')
|
124
|
+
end
|
125
|
+
|
126
|
+
# Restart the daemonized Node, if it exists.
|
127
|
+
def restart_node
|
128
|
+
stop_node
|
129
|
+
sleep 1
|
130
|
+
start_node
|
79
131
|
end
|
80
132
|
|
81
133
|
# Load in the database schema to the database specified in 'database.yml'.
|
@@ -117,7 +169,8 @@ Options:
|
|
117
169
|
def parse_options
|
118
170
|
@options = {
|
119
171
|
:environment => 'production',
|
120
|
-
:config_path => ENV['CLOUD_CROWD_CONFIG'] || '.'
|
172
|
+
:config_path => ENV['CLOUD_CROWD_CONFIG'] || '.',
|
173
|
+
:daemonize => false
|
121
174
|
}
|
122
175
|
@option_parser = OptionParser.new do |opts|
|
123
176
|
opts.on('-c', '--config PATH', 'path to configuration directory') do |conf_path|
|
@@ -129,6 +182,9 @@ Options:
|
|
129
182
|
opts.on('-e', '--environment ENV', 'server environment (sinatra)') do |env|
|
130
183
|
@options[:environment] = env
|
131
184
|
end
|
185
|
+
opts.on('-d', '--daemonize', 'run as a background daemon') do |daemonize|
|
186
|
+
@options[:daemonize] = daemonize
|
187
|
+
end
|
132
188
|
opts.on_tail('-v', '--version', 'show version') do
|
133
189
|
require "#{CC_ROOT}/lib/cloud-crowd"
|
134
190
|
puts "CloudCrowd version #{VERSION}"
|
@@ -114,7 +114,9 @@ module CloudCrowd
|
|
114
114
|
def percent_complete
|
115
115
|
return 99 if merging?
|
116
116
|
return 100 if complete?
|
117
|
-
|
117
|
+
unit_count = work_units.count
|
118
|
+
return 100 if unit_count <= 0
|
119
|
+
(work_units.complete.count / unit_count.to_f * 100).round
|
118
120
|
end
|
119
121
|
|
120
122
|
# How long has this Job taken?
|
@@ -17,24 +17,27 @@ module CloudCrowd
|
|
17
17
|
# Reserved WorkUnits have been marked for distribution by a central server process.
|
18
18
|
named_scope :reserved, {:conditions => {:reservation => $$}, :order => 'updated_at asc'}
|
19
19
|
|
20
|
-
# Attempt to send a list of
|
20
|
+
# Attempt to send a list of WorkUnits to nodes with available capacity.
|
21
21
|
# A single central server process stops the same WorkUnit from being
|
22
22
|
# distributed to multiple nodes by reserving it first. The algorithm used
|
23
23
|
# should be lock-free.
|
24
|
+
#
|
25
|
+
# We loop over the WorkUnits reserved by this process and try to match them
|
26
|
+
# to Nodes that are capable of handling the Action. WorkUnits get removed
|
27
|
+
# from the availability list when they are successfully sent, and Nodes get
|
28
|
+
# removed when they are busy or have the action in question disabled.
|
24
29
|
def self.distribute_to_nodes
|
25
30
|
return unless WorkUnit.reserve_available
|
26
31
|
work_units = WorkUnit.reserved
|
27
32
|
available_nodes = NodeRecord.available
|
28
|
-
|
29
|
-
node
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
if sent
|
35
|
-
work_units.shift
|
36
|
-
available_nodes.push(node) unless node.busy?
|
33
|
+
while node = available_nodes.shift and unit = work_units.shift do
|
34
|
+
if node.actions.include? unit.action
|
35
|
+
if node.send_work_unit(unit)
|
36
|
+
available_nodes.push(node) unless node.busy?
|
37
|
+
next
|
38
|
+
end
|
37
39
|
end
|
40
|
+
work_units.push(unit)
|
38
41
|
end
|
39
42
|
ensure
|
40
43
|
WorkUnit.cancel_reservations
|
@@ -106,7 +109,7 @@ module CloudCrowd
|
|
106
109
|
:output => output,
|
107
110
|
:time => time_taken
|
108
111
|
})
|
109
|
-
|
112
|
+
job && job.check_for_completion
|
110
113
|
end
|
111
114
|
|
112
115
|
# Ever tried. Ever failed. No matter. Try again. Fail again. Fail better.
|
data/lib/cloud_crowd/node.rb
CHANGED
@@ -27,7 +27,7 @@ module CloudCrowd
|
|
27
27
|
# The response sent back when this node is overloaded.
|
28
28
|
OVERLOADED_MESSAGE = 'Node Overloaded'
|
29
29
|
|
30
|
-
attr_reader :
|
30
|
+
attr_reader :enabled_actions, :host, :port, :central
|
31
31
|
|
32
32
|
set :root, ROOT
|
33
33
|
set :authorization_realm, "CloudCrowd"
|
@@ -53,19 +53,20 @@ module CloudCrowd
|
|
53
53
|
# Returns a 503 if this Node is overloaded.
|
54
54
|
post '/work' do
|
55
55
|
throw :halt, [503, OVERLOADED_MESSAGE] if @overloaded
|
56
|
-
|
56
|
+
unit = JSON.parse(params[:work_unit])
|
57
|
+
pid = fork { Worker.new(self, unit).run }
|
57
58
|
Process.detach(pid)
|
58
59
|
json :pid => pid
|
59
60
|
end
|
60
61
|
|
61
62
|
# When creating a node, specify the port it should run on.
|
62
|
-
def initialize(port=
|
63
|
+
def initialize(port=nil, daemon=false)
|
63
64
|
require 'json'
|
64
|
-
@
|
65
|
+
@central = CloudCrowd.central_server
|
65
66
|
@host = Socket.gethostname
|
66
67
|
@enabled_actions = CloudCrowd.actions.keys
|
67
|
-
@asset_store = AssetStore.new
|
68
68
|
@port = port || DEFAULT_PORT
|
69
|
+
@daemon = daemon
|
69
70
|
@overloaded = false
|
70
71
|
@max_load = CloudCrowd.config[:max_load]
|
71
72
|
@min_memory = CloudCrowd.config[:min_free_memory]
|
@@ -75,10 +76,17 @@ module CloudCrowd
|
|
75
76
|
# Starting up a Node registers with the central server and begins to listen
|
76
77
|
# for incoming WorkUnits.
|
77
78
|
def start
|
79
|
+
FileUtils.mkdir_p(CloudCrowd.log_path) if @daemon && !File.exists?(CloudCrowd.log_path)
|
80
|
+
@server = Thin::Server.new('0.0.0.0', @port, self, :signals => false)
|
81
|
+
@server.tag = 'cloud-crowd-node'
|
82
|
+
@server.pid_file = CloudCrowd.pid_path('node.pid')
|
83
|
+
@server.log_file = CloudCrowd.log_path('node.log')
|
84
|
+
@server.daemonize if @daemon
|
78
85
|
trap_signals
|
79
|
-
|
80
|
-
|
86
|
+
asset_store
|
87
|
+
@server_thread = Thread.new { @server.start }
|
81
88
|
check_in(true)
|
89
|
+
monitor_system if @max_load || @min_memory
|
82
90
|
@server_thread.join
|
83
91
|
end
|
84
92
|
|
@@ -86,21 +94,26 @@ module CloudCrowd
|
|
86
94
|
# configuration of this Node. If it can't check-in, there's no point in
|
87
95
|
# starting.
|
88
96
|
def check_in(critical=false)
|
89
|
-
@
|
97
|
+
@central["/node/#{@host}"].put(
|
90
98
|
:port => @port,
|
91
99
|
:busy => @overloaded,
|
92
100
|
:max_workers => CloudCrowd.config[:max_workers],
|
93
101
|
:enabled_actions => @enabled_actions.join(',')
|
94
102
|
)
|
95
103
|
rescue Errno::ECONNREFUSED
|
96
|
-
puts "Failed to connect to the central server (#{@
|
104
|
+
puts "Failed to connect to the central server (#{@central.to_s})."
|
97
105
|
raise SystemExit if critical
|
98
106
|
end
|
99
107
|
|
100
108
|
# Before exiting, the Node checks out with the central server, releasing all
|
101
109
|
# of its WorkUnits for other Nodes to handle
|
102
110
|
def check_out
|
103
|
-
@
|
111
|
+
@central["/node/#{@host}"].delete
|
112
|
+
end
|
113
|
+
|
114
|
+
# Lazy-initialize the asset_store, preferably after the Node has launched.
|
115
|
+
def asset_store
|
116
|
+
@asset_store ||= AssetStore.new
|
104
117
|
end
|
105
118
|
|
106
119
|
# Is the node overloaded? If configured, checks if the load average is
|
@@ -133,13 +146,6 @@ module CloudCrowd
|
|
133
146
|
|
134
147
|
private
|
135
148
|
|
136
|
-
# Launch the Node's Thin server in a separate thread because it blocks.
|
137
|
-
def start_server
|
138
|
-
@server_thread = Thread.new do
|
139
|
-
Thin::Server.start('0.0.0.0', @port, self, :signals => false)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
149
|
# Launch a monitoring thread that periodically checks the node's load
|
144
150
|
# average and the amount of free memory remaining. If we transition out of
|
145
151
|
# the overloaded state, let central know.
|
@@ -156,6 +162,7 @@ module CloudCrowd
|
|
156
162
|
|
157
163
|
# Trap exit signals in order to shut down cleanly.
|
158
164
|
def trap_signals
|
165
|
+
Signal.trap('QUIT') { shut_down }
|
159
166
|
Signal.trap('INT') { shut_down }
|
160
167
|
Signal.trap('KILL') { shut_down }
|
161
168
|
Signal.trap('TERM') { shut_down }
|
@@ -165,7 +172,7 @@ module CloudCrowd
|
|
165
172
|
def shut_down
|
166
173
|
@monitor_thread.kill if @monitor_thread
|
167
174
|
check_out
|
168
|
-
@server_thread.kill
|
175
|
+
@server_thread.kill if @server_thread
|
169
176
|
Process.exit
|
170
177
|
end
|
171
178
|
|
data/lib/cloud_crowd/worker.rb
CHANGED
@@ -30,7 +30,7 @@ module CloudCrowd
|
|
30
30
|
def complete_work_unit(result)
|
31
31
|
keep_trying_to "complete work unit" do
|
32
32
|
data = base_params.merge({:status => 'succeeded', :output => result})
|
33
|
-
@node.
|
33
|
+
@node.central["/work/#{data[:id]}"].put(data)
|
34
34
|
log "finished #{display_work_unit} in #{data[:time]} seconds"
|
35
35
|
end
|
36
36
|
end
|
@@ -39,7 +39,7 @@ module CloudCrowd
|
|
39
39
|
def fail_work_unit(exception)
|
40
40
|
keep_trying_to "mark work unit as failed" do
|
41
41
|
data = base_params.merge({:status => 'failed', :output => {'output' => exception.message}.to_json})
|
42
|
-
@node.
|
42
|
+
@node.central["/work/#{data[:id]}"].put(data)
|
43
43
|
log "failed #{display_work_unit} in #{data[:time]} seconds\n#{exception.message}\n#{exception.backtrace}"
|
44
44
|
end
|
45
45
|
end
|
@@ -70,35 +70,37 @@ module CloudCrowd
|
|
70
70
|
# failures. We capture the thread so that we can kill it from the outside,
|
71
71
|
# when exiting.
|
72
72
|
def run_work_unit
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
else raise Error::StatusUnspecified, "work units must specify their status"
|
84
|
-
end
|
73
|
+
begin
|
74
|
+
result = nil
|
75
|
+
action_class = CloudCrowd.actions[@unit['action']]
|
76
|
+
action = action_class.new(@status, @unit['input'], enhanced_unit_options, @node.asset_store)
|
77
|
+
Dir.chdir(action.work_directory) do
|
78
|
+
result = case @status
|
79
|
+
when PROCESSING then action.process
|
80
|
+
when SPLITTING then action.split
|
81
|
+
when MERGING then action.merge
|
82
|
+
else raise Error::StatusUnspecified, "work units must specify their status"
|
85
83
|
end
|
86
|
-
complete_work_unit({'output' => result}.to_json)
|
87
|
-
rescue Exception => e
|
88
|
-
fail_work_unit(e)
|
89
|
-
ensure
|
90
|
-
action.cleanup_work_directory if action
|
91
84
|
end
|
85
|
+
complete_work_unit({'output' => result}.to_json)
|
86
|
+
rescue Exception => e
|
87
|
+
fail_work_unit(e)
|
88
|
+
ensure
|
89
|
+
action.cleanup_work_directory if action
|
92
90
|
end
|
93
|
-
@worker_thread.join
|
94
91
|
end
|
95
92
|
|
93
|
+
# Run this worker inside of a fork. Attempts to exit cleanly.
|
96
94
|
# Wraps run_work_unit to benchmark the execution time, if requested.
|
97
95
|
def run
|
98
96
|
trap_signals
|
99
97
|
log "starting #{display_work_unit}"
|
100
|
-
|
101
|
-
|
98
|
+
if @unit['options']['benchmark']
|
99
|
+
log("ran #{display_work_unit} in " + Benchmark.measure { run_work_unit }.to_s)
|
100
|
+
else
|
101
|
+
run_work_unit
|
102
|
+
end
|
103
|
+
Process.exit!
|
102
104
|
end
|
103
105
|
|
104
106
|
# There are some potentially important attributes of the WorkUnit that we'd
|
@@ -133,22 +135,13 @@ module CloudCrowd
|
|
133
135
|
puts "Worker ##{@pid}: #{message}" unless ENV['RACK_ENV'] == 'test'
|
134
136
|
end
|
135
137
|
|
136
|
-
# When signaled to exit, make sure that the Worker shuts down
|
138
|
+
# When signaled to exit, make sure that the Worker shuts down without firing
|
139
|
+
# the Node's at_exit callbacks.
|
137
140
|
def trap_signals
|
138
|
-
Signal.trap('
|
139
|
-
Signal.trap('
|
140
|
-
Signal.trap('
|
141
|
-
|
142
|
-
|
143
|
-
# Force the Worker to quit, even if it's in the middle of processing.
|
144
|
-
# If it had a checked-out WorkUnit, the Node should have released it on
|
145
|
-
# the central server already.
|
146
|
-
def shut_down
|
147
|
-
if @worker_thread
|
148
|
-
@worker_thread.kill
|
149
|
-
@worker_thread.kill! if @worker_thread.alive?
|
150
|
-
end
|
151
|
-
Process.exit
|
141
|
+
Signal.trap('QUIT') { Process.exit! }
|
142
|
+
Signal.trap('INT') { Process.exit! }
|
143
|
+
Signal.trap('KILL') { Process.exit! }
|
144
|
+
Signal.trap('TERM') { Process.exit! }
|
152
145
|
end
|
153
146
|
|
154
147
|
end
|
@@ -14,13 +14,13 @@ body {
|
|
14
14
|
height: 110px;
|
15
15
|
position: absolute;
|
16
16
|
top: 0; left: 0; right: 0;
|
17
|
-
background: url(
|
17
|
+
background: url(../images/header_back.png);
|
18
18
|
}
|
19
19
|
#logo {
|
20
20
|
position: absolute;
|
21
21
|
left: 37px; top: 9px;
|
22
22
|
width: 236px; height: 91px;
|
23
|
-
background: url(
|
23
|
+
background: url(../images/logo.png);
|
24
24
|
}
|
25
25
|
|
26
26
|
#disconnected {
|
@@ -37,7 +37,7 @@ body {
|
|
37
37
|
#disconnected .server_error {
|
38
38
|
float: left;
|
39
39
|
width: 16px; height: 16px;
|
40
|
-
background: url(
|
40
|
+
background: url(../images/server_error.png);
|
41
41
|
opacity: 0.7;
|
42
42
|
margin-right: 3px;
|
43
43
|
}
|
@@ -64,7 +64,7 @@ body {
|
|
64
64
|
height: 75px;
|
65
65
|
border: 1px solid #5c5c5c;
|
66
66
|
-moz-border-radius: 10px; -webkit-border-radius: 10px; border-radius: 10px;
|
67
|
-
background: transparent url(
|
67
|
+
background: transparent url(../images/queue_fill.png) repeat-x 0px -1px;
|
68
68
|
}
|
69
69
|
#queue.no_jobs #queue_fill {
|
70
70
|
opacity: 0.3;
|
@@ -126,11 +126,11 @@ body {
|
|
126
126
|
}
|
127
127
|
#sidebar_top {
|
128
128
|
top: 0px;
|
129
|
-
background: url(
|
129
|
+
background: url(../images/sidebar_top.png);
|
130
130
|
}
|
131
131
|
#sidebar_bottom {
|
132
132
|
bottom: 0px;
|
133
|
-
background: url(
|
133
|
+
background: url(../images/sidebar_bottom.png);
|
134
134
|
}
|
135
135
|
#sidebar_header {
|
136
136
|
position: absolute;
|
@@ -164,10 +164,10 @@ body {
|
|
164
164
|
#nodes .node {
|
165
165
|
font-size: 11px;
|
166
166
|
line-height: 22px;
|
167
|
-
background-image: url(
|
167
|
+
background-image: url(../images/server.png);
|
168
168
|
}
|
169
169
|
#nodes .node.busy {
|
170
|
-
background-image: url(
|
170
|
+
background-image: url(../images/server_busy.png);
|
171
171
|
}
|
172
172
|
#nodes .node.busy span.busy {
|
173
173
|
font-size: 9px;
|
@@ -178,7 +178,7 @@ body {
|
|
178
178
|
font-size: 10px;
|
179
179
|
line-height: 18px;
|
180
180
|
cursor: pointer;
|
181
|
-
background-image: url(
|
181
|
+
background-image: url(../images/bullet_green.png);
|
182
182
|
}
|
183
183
|
#nodes .worker:hover {
|
184
184
|
border: 1px solid #aaa;
|
@@ -190,8 +190,7 @@ body {
|
|
190
190
|
position: absolute;
|
191
191
|
width: 231px; height: 79px;
|
192
192
|
margin: -9px 0 0 -20px;
|
193
|
-
background: url(
|
194
|
-
overflow: hidden;
|
193
|
+
background: url(../images/worker_info.png);
|
195
194
|
cursor: pointer;
|
196
195
|
}
|
197
196
|
#worker_info_inner {
|
@@ -199,9 +198,10 @@ body {
|
|
199
198
|
line-height: 15px;
|
200
199
|
color: #333;
|
201
200
|
text-shadow: 0px 1px 1px #eee;
|
201
|
+
overflow: hidden;
|
202
202
|
}
|
203
203
|
#worker_info.loading #worker_info_inner {
|
204
|
-
background: url(
|
204
|
+
background: url(../images/worker_info_loading.gif) no-repeat right bottom;
|
205
205
|
width: 45px; height: 9px;
|
206
206
|
}
|
207
207
|
#worker_info.awake #worker_details,
|
data/public/js/admin_console.js
CHANGED
@@ -18,7 +18,7 @@ window.Console = {
|
|
18
18
|
DISPLAY_STATUS_MAP : ['unknown', 'processing', 'succeeded', 'failed', 'splitting', 'merging'],
|
19
19
|
|
20
20
|
// Images to preload
|
21
|
-
PRELOAD_IMAGES : ['
|
21
|
+
PRELOAD_IMAGES : ['images/server_error.png'],
|
22
22
|
|
23
23
|
// All options for drawing the system graphs.
|
24
24
|
GRAPH_OPTIONS : {
|
@@ -53,7 +53,7 @@ window.Console = {
|
|
53
53
|
// Request the lastest status of all jobs and workers, re-render or update
|
54
54
|
// the DOM to reflect.
|
55
55
|
getStatus : function() {
|
56
|
-
$.ajax({url : '
|
56
|
+
$.ajax({url : 'status', dataType : 'json', success : function(resp) {
|
57
57
|
Console._jobs = resp.jobs;
|
58
58
|
Console._nodes = resp.nodes;
|
59
59
|
Console._workUnitCount = resp.work_unit_count;
|
@@ -167,7 +167,7 @@ window.Console = {
|
|
167
167
|
var info = Console._workerInfo;
|
168
168
|
var row = $(this);
|
169
169
|
info.addClass('loading');
|
170
|
-
$.get('
|
170
|
+
$.get('worker/' + row.attr('rel'), null, Console.renderWorkerInfo, 'json');
|
171
171
|
info.css({top : row.offset().top, left : 325});
|
172
172
|
info.fadeIn(Console.ANIMATION_SPEED);
|
173
173
|
$(document).bind('click', Console.hideWorkerInfo);
|
data/test/unit/test_action.rb
CHANGED
@@ -9,7 +9,7 @@ end
|
|
9
9
|
|
10
10
|
class ActionTest < Test::Unit::TestCase
|
11
11
|
|
12
|
-
context "A CloudCrowd
|
12
|
+
context "A CloudCrowd::Action" do
|
13
13
|
|
14
14
|
setup do
|
15
15
|
@store = CloudCrowd::AssetStore.new
|
@@ -41,9 +41,30 @@ class ActionTest < Test::Unit::TestCase
|
|
41
41
|
end
|
42
42
|
|
43
43
|
should "be able to count the number of words in this file" do
|
44
|
-
assert @action.process ==
|
44
|
+
assert @action.process == 212
|
45
|
+
end
|
46
|
+
|
47
|
+
should "raise an exception when backticks fail" do
|
48
|
+
def @action.process; `utter failure 2>&1`; end
|
49
|
+
assert_raise(CloudCrowd::Error::CommandFailed) { @action.process }
|
45
50
|
end
|
46
51
|
|
47
52
|
end
|
53
|
+
|
54
|
+
|
55
|
+
context "A CloudCrowd::Action without URL input" do
|
56
|
+
|
57
|
+
setup do
|
58
|
+
@store = CloudCrowd::AssetStore.new
|
59
|
+
@args = [CloudCrowd::PROCESSING, 'inputstring', {'job_id' => 1, 'work_unit_id' => 1}, @store]
|
60
|
+
@action = CloudCrowd.actions['word_count'].new(*@args)
|
61
|
+
end
|
62
|
+
|
63
|
+
should "should not interpret the input data as an url" do
|
64
|
+
assert_equal 'inputstring', @action.input
|
65
|
+
assert_nil @action.input_path
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
48
69
|
|
49
70
|
end
|
data/test/unit/test_job.rb
CHANGED
@@ -31,6 +31,11 @@ class JobTest < Test::Unit::TestCase
|
|
31
31
|
assert @job.percent_complete == 100
|
32
32
|
assert @job.outputs == "[\"hello\"]"
|
33
33
|
end
|
34
|
+
|
35
|
+
should "not throw a FloatDomainError, even when WorkUnits have vanished" do
|
36
|
+
@job.work_units.destroy_all
|
37
|
+
assert @job.percent_complete == 100
|
38
|
+
end
|
34
39
|
|
35
40
|
should "be able to create a job from a JSON request" do
|
36
41
|
job = CloudCrowd::Job.create_from_request(JSON.parse(<<-EOS
|
data/test/unit/test_node.rb
CHANGED
@@ -9,7 +9,7 @@ class NodeUnitTest < Test::Unit::TestCase
|
|
9
9
|
end
|
10
10
|
|
11
11
|
should "instantiate correctly" do
|
12
|
-
assert @node.
|
12
|
+
assert @node.central.to_s == "http://localhost:9173"
|
13
13
|
assert @node.port == 11011
|
14
14
|
assert @node.host == Socket.gethostname
|
15
15
|
assert @node.enabled_actions.length > 2
|
@@ -17,8 +17,7 @@ class NodeUnitTest < Test::Unit::TestCase
|
|
17
17
|
end
|
18
18
|
|
19
19
|
should "trap signals and launch a server at start" do
|
20
|
-
|
21
|
-
Thin::Server.expects(:start)
|
20
|
+
Thin::Server.any_instance.expects(:start)
|
22
21
|
@node.expects(:check_in)
|
23
22
|
@node.start
|
24
23
|
end
|
data/views/operations_center.erb
CHANGED
@@ -3,12 +3,12 @@
|
|
3
3
|
<head>
|
4
4
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
5
5
|
<title>Operations Center | CloudCrowd</title>
|
6
|
-
<link href="
|
7
|
-
<link href="
|
8
|
-
<script src="
|
9
|
-
<!--[if IE]><script src="
|
10
|
-
<script src="
|
11
|
-
<script src="
|
6
|
+
<link href="css/reset.css" media="screen" rel="stylesheet" type="text/css" />
|
7
|
+
<link href="css/admin_console.css" media="screen" rel="stylesheet" type="text/css" />
|
8
|
+
<script src="js/jquery.js" type="text/javascript"></script>
|
9
|
+
<!--[if IE]><script src="js/excanvas.js" type="text/javascript"></script><![endif]-->
|
10
|
+
<script src="js/flot.js" type="text/javascript"></script>
|
11
|
+
<script src="js/admin_console.js" type="text/javascript"></script>
|
12
12
|
</head>
|
13
13
|
|
14
14
|
<body>
|
@@ -68,8 +68,8 @@
|
|
68
68
|
<div id="worker_info_inner" class="small_caps">
|
69
69
|
<div id="worker_details">
|
70
70
|
<div id="worker_status">status: <span class="status"></span></div>
|
71
|
-
<div id="worker_action">action
|
72
|
-
<div id="worker_job_id">job #<span class="job_id"></span> /
|
71
|
+
<div id="worker_action">action: <span class="action"></span></div>
|
72
|
+
<div id="worker_job_id">job #<span class="job_id"></span> / unit #<span class="work_unit_id"></span></div>
|
73
73
|
</div>
|
74
74
|
<div id="worker_sleeping">
|
75
75
|
worker exiting…
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cloud-crowd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Ashkenas
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-09-
|
12
|
+
date: 2009-09-23 00:00:00 -04:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|