cloud-crowd 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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