documentcloud-cloud-crowd 0.2.1 → 0.2.2
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/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: documentcloud-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 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|