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 CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'cloud-crowd'
3
- s.version = '0.2.1' # Keep version in sync with cloud-cloud.rb
4
- s.date = '2009-09-18'
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"
@@ -29,10 +29,14 @@
29
29
  :s3_bucket: [your CloudCrowd bucket]
30
30
  :s3_authentication: no
31
31
 
32
- # If you're using the 'filesystem' storage, perhaps with an NFS share or
33
- # something similar, all files will be saved inside of the 'local_storage_path'.
34
- # The default value if left unspecified is '/tmp/cloud_crowd_storage'.
35
- :local_storage_path: /tmp/cloud_crowd_storage
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.1'
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.
@@ -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
- input_is_url = !!URI.parse(@input) rescue false
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 = ARGV.shift
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
- # Convenience command for quickly spinning up the central server. More
57
- # sophisticated deployments, load-balancing across multiple app servers,
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
- @options[:port] ||= 9173
64
- require 'rubygems'
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 Gem.available? 'thin'
67
- exec "thin -e #{@options[:environment]} -p #{@options[:port]} -R #{rackup_path} start"
68
- else
69
- exec "rackup -E #{@options[:environment]} -p #{@options[:port]} #{rackup_path}"
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 run_node
113
+ def start_node
76
114
  ENV['RACK_ENV'] = @options['environment']
77
115
  load_code
78
- Node.new(@options[:port])
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
- (work_units.complete.count / work_units.count.to_f * 100).round
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?
@@ -48,7 +48,7 @@ module CloudCrowd
48
48
 
49
49
  # What Actions is this Node able to run?
50
50
  def actions
51
- enabled_actions.split(',')
51
+ @actions ||= enabled_actions.split(',')
52
52
  end
53
53
 
54
54
  # Is this Node too busy for more work? Determined by number of workers, or
@@ -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 work_units to nodes with available capacity.
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
- until work_units.empty? do
29
- node = available_nodes.shift
30
- unit = work_units.first
31
- break unless node && unit
32
- next unless node.actions.include? unit.action
33
- sent = node.send_work_unit(unit)
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
- self.job.check_for_completion
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.
@@ -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 :asset_store, :enabled_actions, :host, :port, :server
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
- pid = fork { Worker.new(self, JSON.parse(params[:work_unit])).run }
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=DEFAULT_PORT)
63
+ def initialize(port=nil, daemon=false)
63
64
  require 'json'
64
- @server = CloudCrowd.central_server
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
- start_server
80
- monitor_system if @max_load || @min_memory
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
- @server["/node/#{@host}"].put(
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 (#{@server.to_s})."
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
- @server["/node/#{@host}"].delete
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
 
@@ -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.server["/work/#{data[:id]}"].put(data)
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.server["/work/#{data[:id]}"].put(data)
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
- @worker_thread = Thread.new do
74
- begin
75
- result = nil
76
- action_class = CloudCrowd.actions[@unit['action']]
77
- action = action_class.new(@status, @unit['input'], enhanced_unit_options, @node.asset_store)
78
- Dir.chdir(action.work_directory) do
79
- result = case @status
80
- when PROCESSING then action.process
81
- when SPLITTING then action.split
82
- when MERGING then action.merge
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
- return run_work_unit unless @unit['options']['benchmark']
101
- log("ran #{display_work_unit} in " + Benchmark.measure { run_work_unit }.to_s)
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 cleanly.
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('INT') { shut_down }
139
- Signal.trap('KILL') { shut_down }
140
- Signal.trap('TERM') { shut_down }
141
- end
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(/images/header_back.png);
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(/images/logo.png);
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(/images/server_error.png);
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(/images/queue_fill.png) repeat-x 0px -1px;
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(/images/sidebar_top.png);
129
+ background: url(../images/sidebar_top.png);
130
130
  }
131
131
  #sidebar_bottom {
132
132
  bottom: 0px;
133
- background: url(/images/sidebar_bottom.png);
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(/images/server.png);
167
+ background-image: url(../images/server.png);
168
168
  }
169
169
  #nodes .node.busy {
170
- background-image: url(/images/server_busy.png);
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(/images/bullet_green.png);
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(/images/worker_info.png);
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(/images/worker_info_loading.gif) no-repeat right bottom;
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,
@@ -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 : ['/images/server_error.png'],
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 : '/status', dataType : 'json', success : function(resp) {
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('/worker/' + row.attr('rel'), null, Console.renderWorkerInfo, 'json');
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);
@@ -9,7 +9,7 @@ end
9
9
 
10
10
  class ActionTest < Test::Unit::TestCase
11
11
 
12
- context "A CloudCrowd Job" do
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 == 149
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
@@ -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
@@ -9,7 +9,7 @@ class NodeUnitTest < Test::Unit::TestCase
9
9
  end
10
10
 
11
11
  should "instantiate correctly" do
12
- assert @node.server.to_s == "http://localhost:9173"
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
- Signal.expects(:trap).times(3)
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
@@ -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="/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>
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: <span class="action"></span></div>
72
- <div id="worker_job_id">job #<span class="job_id"></span> / work unit #<span class="work_unit_id"></span></div>
71
+ <div id="worker_action">action:&nbsp;<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&hellip;
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.1
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-18 00:00:00 -07:00
12
+ date: 2009-09-23 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency