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.
@@ -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
@@ -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: 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 -04:00
12
+ date: 2009-09-23 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency