cloud-crowd 0.3.3 → 0.4.0

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.
@@ -22,7 +22,7 @@ body {
22
22
  width: 236px; height: 91px;
23
23
  background: url(../images/logo.png);
24
24
  }
25
-
25
+
26
26
  #disconnected {
27
27
  position: absolute;
28
28
  top: 122px; right: 15px;
@@ -42,70 +42,27 @@ body {
42
42
  margin-right: 3px;
43
43
  }
44
44
 
45
- #queue {
45
+ #stats {
46
46
  position: absolute;
47
- top: 16px; left: 327px; right: 15px;
47
+ top: 16px; left: 327px; right: 22px;
48
48
  height: 77px;
49
49
  overflow: hidden;
50
+ color: #454545;
50
51
  }
51
- #no_jobs {
52
- text-align: left;
53
- position: absolute;
54
- bottom: 8px; right: 8px;
55
- color: #999;
56
- display: none;
52
+ #stats tr.data td {
53
+ padding: 0 50px 0 0;
54
+ font-size: 50px;
57
55
  }
58
- #queue.no_jobs #no_jobs {
59
- display: block;
56
+ #stats tr.data td.last {
57
+ padding-right: 0;
60
58
  }
61
- #queue_fill {
62
- position: absolute;
63
- left: 0; right: 0; top: 0;
64
- height: 75px;
65
- border: 1px solid #5c5c5c;
66
- -moz-border-radius: 10px; -webkit-border-radius: 10px; border-radius: 10px;
67
- background: transparent url(../images/queue_fill.png) repeat-x 0px -1px;
68
- }
69
- #queue.no_jobs #queue_fill {
70
- opacity: 0.3;
59
+ #stats tr.data td div {
60
+ border-bottom: 1px solid #777;
71
61
  }
72
- #queue .job {
73
- position: relative;
74
- margin-top: 1px;
75
- height: 75px;
76
- background: blue;
77
- float: left;
78
- overflow: hidden;
79
- -moz-border-radius: 10px;
80
- -webkit-border-radius: 10px;
62
+ #stats tr.labels td {
63
+ font-size: 10px;
64
+ text-transform: uppercase;
81
65
  }
82
- #queue .completion {
83
- position: absolute;
84
- bottom: -1px;
85
- height: 30px;
86
- background: black;
87
- border: 1px solid white;
88
- -moz-border-radius: 10px; -webkit-border-radius: 10px;
89
- opacity: 0.5;
90
- overflow: hidden;
91
- }
92
- #queue .completion.zero {
93
- border: 0;
94
- }
95
- #queue .percent_complete {
96
- position: absolute;
97
- bottom: 8px; left: 8px;
98
- color: #c7c7c7;
99
- z-index: 10;
100
- }
101
- #queue .job_id {
102
- color: #333;
103
- font-size: 14px;
104
- position: absolute;
105
- top: 8px; left: 8px;
106
- z-index: 10;
107
- }
108
-
109
66
  #sidebar {
110
67
  position: absolute;
111
68
  top: 120px; left: 10px; bottom: 10px;
@@ -134,11 +91,17 @@ body {
134
91
  }
135
92
  #sidebar_header {
136
93
  position: absolute;
137
- width: 250px;
94
+ width: 283px;
138
95
  top: 5px; left: 8px;
139
96
  color: #404040;
140
97
  text-shadow: 0px 1px 1px #eee;
141
98
  }
99
+ #tail_log {
100
+ text-transform: none;
101
+ text-decoration: underline;
102
+ cursor: pointer;
103
+ float: right;
104
+ }
142
105
  #sidebar_header.no_nodes .no_nodes,
143
106
  #sidebar_header .has_nodes {
144
107
  display: block;
@@ -185,7 +148,7 @@ body {
185
148
  border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px;
186
149
  background-color: #ccc;
187
150
  }
188
-
151
+
189
152
  #worker_info {
190
153
  position: absolute;
191
154
  width: 231px; height: 79px;
@@ -204,15 +167,15 @@ body {
204
167
  background: url(../images/worker_info_loading.gif) no-repeat right bottom;
205
168
  width: 45px; height: 9px;
206
169
  }
207
- #worker_info.awake #worker_details,
170
+ #worker_info.awake #worker_details,
208
171
  #worker_sleeping {
209
172
  display: block;
210
173
  }
211
- #worker_details, #worker_info.loading #worker_details,
174
+ #worker_details, #worker_info.loading #worker_details,
212
175
  #worker_info.loading #worker_sleeping, #worker_info.awake #worker_sleeping {
213
176
  display: none;
214
177
  }
215
-
178
+
216
179
  #graphs {
217
180
  position: absolute;
218
181
  padding: 17px 15px 15px 17px;
@@ -4,22 +4,22 @@
4
4
  // Think about pulling in the DCJS framework, instead of just raw jQuery here.
5
5
  // Leaving it hacked together like this just cries out for templates, dunnit?
6
6
  window.Console = {
7
-
7
+
8
8
  // Maximum number of data points to record and graph.
9
9
  MAX_DATA_POINTS : 100,
10
-
10
+
11
11
  // Milliseconds between polling the central server for updates to Job progress.
12
- POLL_INTERVAL : 3000,
13
-
12
+ POLL_INTERVAL : 6000,
13
+
14
14
  // Default speed for all animations.
15
15
  ANIMATION_SPEED : 300,
16
-
16
+
17
17
  // Keep this in sync with the map in cloud-crowd.rb
18
- DISPLAY_STATUS_MAP : ['unknown', 'processing', 'succeeded', 'failed', 'splitting', 'merging'],
19
-
18
+ DISPLAY_STATUS_MAP : ['unknown', 'processing', 'succeeded', 'failed', 'splitting', 'merging'],
19
+
20
20
  // Images to preload
21
21
  PRELOAD_IMAGES : ['images/server_error.png'],
22
-
22
+
23
23
  // All options for drawing the system graphs.
24
24
  GRAPH_OPTIONS : {
25
25
  xaxis : {mode : 'time', timeformat : '%M:%S'},
@@ -31,37 +31,40 @@ window.Console = {
31
31
  NODES_COLOR : '#1870ab',
32
32
  WORKERS_COLOR : '#45a4e5',
33
33
  WORK_UNITS_COLOR : '#ffba14',
34
-
34
+
35
35
  // Starting the console begins polling the server.
36
36
  initialize : function() {
37
- this._jobsHistory = [];
38
- this._nodesHistory = [];
39
- this._workersHistory = [];
40
- this._workUnitsHistory = [];
41
- this._histories = [this._jobsHistory, this._nodesHistory, this._workersHistory, this._workUnitsHistory];
42
- this._queue = $('#jobs');
43
- this._workerInfo = $('#worker_info');
37
+ this._jobsHistory = [];
38
+ this._nodesHistory = [];
39
+ this._workersHistory = [];
40
+ this._workUnitsHistory = [];
41
+ this._histories = [this._jobsHistory, this._nodesHistory, this._workersHistory, this._workUnitsHistory];
42
+ this._workerInfo = $('#worker_info');
43
+ this._jobCountEl = $('#job_count');
44
+ this._workUnitCountEl = $('#work_unit_count');
45
+ this._nodeCountEl = $('#node_count');
46
+ this._workerCountEl = $('#worker_count');
44
47
  this._disconnected = $('#disconnected');
45
48
  $(window).bind('resize', Console.renderGraphs);
46
49
  $('#nodes .worker').live('click', Console.getWorkerInfo);
50
+ $('#tail_log').bind('click', Console.tailLog);
47
51
  $('#workers_legend').css({background : this.WORKERS_COLOR});
48
52
  $('#nodes_legend').css({background : this.NODES_COLOR});
49
53
  this.getStatus();
50
54
  $.each(this.PRELOAD_IMAGES, function(){ var i = new Image(); i.src = this; });
51
55
  },
52
-
56
+
53
57
  // Request the lastest status of all jobs and workers, re-render or update
54
58
  // the DOM to reflect.
55
59
  getStatus : function() {
56
60
  $.ajax({url : 'status', dataType : 'json', success : function(resp) {
57
- Console._jobs = resp.jobs;
61
+ Console._jobCount = resp.job_count;
58
62
  Console._nodes = resp.nodes;
59
63
  Console._workUnitCount = resp.work_unit_count;
60
64
  Console._workerCount = Console.countWorkers();
61
65
  Console.recordDataPoint();
62
66
  if (Console._disconnected.is(':visible')) Console._disconnected.fadeOut(Console.ANIMATION_SPEED);
63
- $('#queue').toggleClass('no_jobs', Console._jobs.length <= 0);
64
- Console.renderJobs();
67
+ Console.renderStats();
65
68
  Console.renderNodes();
66
69
  Console.renderGraphs();
67
70
  setTimeout(Console.getStatus, Console.POLL_INTERVAL);
@@ -70,52 +73,32 @@ window.Console = {
70
73
  setTimeout(Console.getStatus, Console.POLL_INTERVAL);
71
74
  }});
72
75
  },
73
-
76
+
77
+ // Fetch the last 100 lines of log from the server.
78
+ tailLog : function() {
79
+ $.ajax({url : 'log', success : function(resp) {
80
+ var win = window.open('');
81
+ win.document.open();
82
+ win.document.write('<pre>' + resp + '</pre>');
83
+ win.document.close();
84
+ }});
85
+ },
86
+
74
87
  // Count the total number of workers in the current list of nodes.
75
88
  countWorkers : function() {
76
89
  var sum = 0;
77
90
  for (var i=0; i < this._nodes.length; i++) sum += this._nodes[i].workers.length;
78
91
  return sum;
79
92
  },
80
-
81
- // Render an individual job afresh.
82
- renderJob : function(job) {
83
- this._queue.append('<div class="job" id="job_' + job.id + '" style="width:' + job.width + '%; background: #' + job.color + ';"><div class="completion ' + (job.percent_complete <= 0 ? 'zero' : '') + '" style="width:' + job.percent_complete + '%;"></div><div class="percent_complete">' + job.percent_complete + '%</div><div class="job_id">#' + job.id + '</div></div>');
84
- },
85
-
86
- // Animate the update to an existing job in the queue.
87
- updateJob : function(job, jobEl) {
88
- jobEl.animate({width : job.width + '%'}, this.ANIMATION_SPEED);
89
- var completion = $('.completion', jobEl);
90
- if (job.percent_complete > 0) completion.removeClass('zero');
91
- completion.animate({width : job.percent_complete + '%'}, this.ANIMATION_SPEED);
92
- $('.percent_complete', jobEl).html(job.percent_complete + '%');
93
- },
94
-
95
- // Render all jobs, calculating relative widths and completions.
96
- renderJobs : function() {
97
- var totalUnits = 0;
98
- var totalWidth = this._queue.width();
99
- var jobIds = [];
100
- $.each(this._jobs, function() {
101
- jobIds.push(this.id);
102
- totalUnits += this.work_units;
103
- });
104
- $.each($('.job'), function() {
105
- var el = this;
106
- if (jobIds.indexOf(parseInt(el.id.replace(/\D/g, ''), 10)) < 0) {
107
- $(el).animate({width : '0%'}, Console.ANIMATION_SPEED - 50, 'linear', function() {
108
- $(el).remove();
109
- });
110
- }
111
- });
112
- $.each(this._jobs, function() {
113
- this.width = (this.work_units / totalUnits) * 100;
114
- var jobEl = $('#job_' + this.id);
115
- jobEl[0] ? Console.updateJob(this, jobEl) : Console.renderJob(this);
116
- });
93
+
94
+ // Render the numeric statistic counts.
95
+ renderStats : function() {
96
+ this._jobCountEl.text(this._jobCount);
97
+ this._workUnitCountEl.text(this._workUnitCount);
98
+ this._nodeCountEl.text(this._nodes.length);
99
+ this._workerCountEl.text(this._workerCount);
117
100
  },
118
-
101
+
119
102
  // Re-render all workers from scratch each time.
120
103
  // This method is desperately in need of Javascript templates...
121
104
  renderNodes : function() {
@@ -123,7 +106,7 @@ window.Console = {
123
106
  var nc = this._nodes.length, wc = this._workerCount;
124
107
  $('.has_nodes', header).html(nc + " Node" + (nc != 1 ? 's' : '') + " / " + wc + " Worker" + (wc != 1 ? 's' : ''));
125
108
  header.toggleClass('no_nodes', this._nodes.length <= 0);
126
- $('#nodes').html($.map(this._nodes, function(node) {
109
+ $('#nodes').html($.map(this._nodes, function(node) {
127
110
  var html = "";
128
111
  var extra = node.status == 'busy' ? ' <span class="busy">[busy]</span>' : '';
129
112
  html += '<div class="node ' + node.status + '">' + node.host + extra + '</div>';
@@ -134,19 +117,19 @@ window.Console = {
134
117
  return html;
135
118
  }).join(''));
136
119
  },
137
-
120
+
138
121
  // Record the current state and re-render all graphs.
139
122
  recordDataPoint : function() {
140
123
  var timestamp = (new Date()).getTime();
141
- this._jobsHistory.push([timestamp, this._jobs.length]);
124
+ this._jobsHistory.push([timestamp, this._jobCount]);
142
125
  this._nodesHistory.push([timestamp, this._nodes.length]);
143
126
  this._workersHistory.push([timestamp, this._workerCount]);
144
127
  this._workUnitsHistory.push([timestamp, this._workUnitCount]);
145
- $.each(this._histories, function() {
146
- if (this.length > Console.MAX_DATA_POINTS) this.shift();
128
+ $.each(this._histories, function() {
129
+ if (this.length > Console.MAX_DATA_POINTS) this.shift();
147
130
  });
148
131
  },
149
-
132
+
150
133
  // Convert our recorded data points into a format Flot can understand.
151
134
  renderGraphs : function() {
152
135
  $.plot($('#work_units_graph'), [
@@ -160,7 +143,7 @@ window.Console = {
160
143
  {label : 'Workers', color : Console.WORKERS_COLOR, data : Console._workersHistory}
161
144
  ], Console.GRAPH_OPTIONS);
162
145
  },
163
-
146
+
164
147
  // Request the Worker info from the central server.
165
148
  getWorkerInfo : function(e) {
166
149
  e.stopImmediatePropagation();
@@ -173,7 +156,7 @@ window.Console = {
173
156
  $(document).bind('click', Console.hideWorkerInfo);
174
157
  return false;
175
158
  },
176
-
159
+
177
160
  // When we receieve worker info, update the bubble.
178
161
  renderWorkerInfo : function(resp) {
179
162
  var info = Console._workerInfo;
@@ -185,13 +168,13 @@ window.Console = {
185
168
  $('.job_id', info).html(resp.job_id);
186
169
  $('.work_unit_id', info).html(resp.id);
187
170
  },
188
-
171
+
189
172
  // Hide worker info and unbind the global hide handler.
190
173
  hideWorkerInfo : function() {
191
174
  $(document).unbind('click', Console.hideWorkerInfo);
192
- Console._workerInfo.fadeOut(Console.ANIMATION_SPEED);
175
+ Console._workerInfo.fadeOut(Console.ANIMATION_SPEED);
193
176
  }
194
-
177
+
195
178
  };
196
179
 
197
180
  $(document).ready(function() { Console.initialize(); });
@@ -1,43 +1,41 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class ServerTest < Test::Unit::TestCase
4
-
4
+
5
5
  include Rack::Test::Methods
6
-
6
+
7
7
  def app
8
8
  CloudCrowd::Server
9
9
  end
10
-
10
+
11
11
  context "The CloudCrowd::Server (Sinatra)" do
12
-
12
+
13
13
  setup do
14
14
  Job.destroy_all
15
15
  2.times { Job.make }
16
16
  end
17
-
17
+
18
18
  should "set the identity of the Ruby instance" do
19
19
  app.new
20
20
  assert CloudCrowd.server?
21
21
  end
22
-
22
+
23
23
  should "be able to render the Operations Center (GET /)" do
24
24
  get '/'
25
25
  assert last_response.body.include? '<div id="nodes">'
26
26
  assert last_response.body.include? '<div id="graphs">'
27
27
  end
28
-
28
+
29
29
  should "be able to get the current status for all jobs (GET /status)" do
30
30
  resp = JSON.parse(get('/status').body)
31
- assert resp['jobs'].length == 2
32
- assert resp['jobs'][0]['status'] == 'processing'
33
- assert resp['jobs'][0]['percent_complete'] == 0
31
+ assert resp['job_count'] == 2
34
32
  assert resp['work_unit_count'] == 2
35
33
  end
36
-
34
+
37
35
  should "have a heartbeat" do
38
36
  assert get('/heartbeat').body == 'buh-bump'
39
37
  end
40
-
38
+
41
39
  should "be able to create a job" do
42
40
  WorkUnit.expects(:distribute_to_nodes).returns(true)
43
41
  post('/jobs', :job => '{"action":"graphics_magick","inputs":["http://www.google.com/"]}')
@@ -47,20 +45,20 @@ class ServerTest < Test::Unit::TestCase
47
45
  assert job_info['work_units'] == 1
48
46
  assert Job.last.id == job_info['id']
49
47
  end
50
-
48
+
51
49
  should "be able to check in on the status of a job" do
52
50
  get("/jobs/#{Job.last.id}")
53
51
  assert last_response.ok?
54
52
  assert JSON.parse(last_response.body)['percent_complete'] == 0
55
53
  end
56
-
54
+
57
55
  should "be able to clean up a job when we're done with it" do
58
56
  id = Job.last.id
59
57
  delete("/jobs/#{id}")
60
58
  assert last_response.successful? && last_response.empty?
61
59
  assert !Job.find_by_id(id)
62
60
  end
63
-
61
+
64
62
  end
65
-
63
+
66
64
  end
@@ -8,63 +8,65 @@ class EmptyAction < CloudCrowd::Action
8
8
  end
9
9
 
10
10
  class ActionTest < Test::Unit::TestCase
11
-
11
+
12
12
  context "A CloudCrowd::Action" do
13
-
13
+
14
14
  setup do
15
15
  @store = CloudCrowd::AssetStore.new
16
16
  @args = [CloudCrowd::PROCESSING, 'file://' + File.expand_path(__FILE__), {'job_id' => 1, 'work_unit_id' => 1}, @store]
17
17
  @action = CloudCrowd.actions['word_count'].new(*@args)
18
18
  end
19
-
19
+
20
20
  should "throw an exception if the 'process' method isn't implemented" do
21
21
  assert_raise(NotImplementedError) { EmptyAction.new(*@args).process }
22
22
  end
23
-
23
+
24
24
  should "have downloaded the input URL to local storage" do
25
25
  assert @action.input_path
26
26
  assert File.read(@action.input_path) == File.read(File.expand_path(__FILE__))
27
27
  end
28
-
28
+
29
29
  should "be able to save (to the filesystem while testing)" do
30
30
  assert @action.save(@action.input_path) == "file://#{@store.local_storage_path}/word_count/job_1/unit_1/test_action.rb"
31
31
  end
32
-
32
+
33
33
  should "be able to clean up after itself" do
34
34
  @action.cleanup_work_directory
35
35
  assert !File.exists?(@action.work_directory)
36
36
  end
37
-
37
+
38
38
  should "be able to generate a safe filename for a URL to write to disk" do
39
39
  name = @action.safe_filename("http://example.com/Some%20(Crazy'Kinda%7E)'Filename.txt")
40
40
  assert name == 'Some-Crazy-Kinda-Filename.txt'
41
+ name = @action.safe_filename("http://example.com/file.pdf?one=two&three=four")
42
+ assert name == 'file.pdf'
41
43
  end
42
-
44
+
43
45
  should "be able to count the number of words in this file" do
44
- assert @action.process == 212
46
+ assert @action.process == 219
45
47
  end
46
-
48
+
47
49
  should "raise an exception when backticks fail" do
48
50
  def @action.process; `utter failure 2>&1`; end
49
51
  assert_raise(CloudCrowd::Error::CommandFailed) { @action.process }
50
52
  end
51
-
53
+
52
54
  end
53
55
 
54
56
 
55
57
  context "A CloudCrowd::Action without URL input" do
56
-
58
+
57
59
  setup do
58
60
  @store = CloudCrowd::AssetStore.new
59
61
  @args = [CloudCrowd::PROCESSING, 'inputstring', {'job_id' => 1, 'work_unit_id' => 1}, @store]
60
62
  @action = CloudCrowd.actions['word_count'].new(*@args)
61
63
  end
62
-
64
+
63
65
  should "should not interpret the input data as an url" do
64
66
  assert_equal 'inputstring', @action.input
65
67
  assert_nil @action.input_path
66
68
  end
67
-
69
+
68
70
  end
69
-
71
+
70
72
  end