documentcloud-cloud-crowd 0.0.5 → 0.0.6

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.
Files changed (47) hide show
  1. data/README +59 -50
  2. data/actions/process_pdfs.rb +3 -3
  3. data/actions/word_count.rb +14 -0
  4. data/cloud-crowd.gemspec +27 -13
  5. data/config/config.example.yml +8 -11
  6. data/examples/graphics_magick_example.rb +40 -44
  7. data/examples/process_pdfs_example.rb +39 -29
  8. data/examples/word_count_example.rb +41 -0
  9. data/lib/cloud-crowd.rb +20 -17
  10. data/lib/cloud_crowd/action.rb +26 -9
  11. data/lib/cloud_crowd/app.rb +26 -4
  12. data/lib/cloud_crowd/asset_store.rb +69 -40
  13. data/lib/cloud_crowd/command_line.rb +6 -4
  14. data/lib/cloud_crowd/daemon.rb +65 -25
  15. data/lib/cloud_crowd/exceptions.rb +5 -0
  16. data/lib/cloud_crowd/helpers/resources.rb +2 -2
  17. data/lib/cloud_crowd/models/job.rb +9 -13
  18. data/lib/cloud_crowd/models/work_unit.rb +23 -15
  19. data/lib/cloud_crowd/models/worker_record.rb +61 -0
  20. data/lib/cloud_crowd/models.rb +7 -1
  21. data/lib/cloud_crowd/schema.rb +12 -3
  22. data/lib/cloud_crowd/worker.rb +48 -10
  23. data/public/css/admin_console.css +174 -4
  24. data/public/css/reset.css +17 -27
  25. data/public/images/bullet_green.png +0 -0
  26. data/public/images/bullet_white.png +0 -0
  27. data/public/images/cloud_hand.png +0 -0
  28. data/public/images/header_back.png +0 -0
  29. data/public/images/logo.png +0 -0
  30. data/public/images/server_error.png +0 -0
  31. data/public/images/sidebar_bottom.png +0 -0
  32. data/public/images/sidebar_top.png +0 -0
  33. data/public/images/worker_info.png +0 -0
  34. data/public/images/worker_info_loading.gif +0 -0
  35. data/public/js/admin_console.js +127 -10
  36. data/public/js/excanvas.pack.js +1 -0
  37. data/public/js/jquery-1.3.2.min.js +19 -0
  38. data/public/js/jquery.flot.pack.js +1 -0
  39. data/test/acceptance/test_word_count.rb +49 -0
  40. data/test/blueprints.rb +6 -5
  41. data/test/config/config.yml +1 -4
  42. data/test/test_helper.rb +1 -0
  43. data/test/unit/test_job.rb +12 -4
  44. data/test/unit/test_work_unit.rb +2 -2
  45. data/views/index.erb +69 -14
  46. metadata +23 -6
  47. data/public/js/jquery-1.3.2.js +0 -4376
@@ -5,10 +5,17 @@ module CloudCrowd
5
5
  # units that they are able to handle (for which they have an action in their
6
6
  # actions directory). If communication with the central server is interrupted,
7
7
  # the WorkUnit will repeatedly attempt to complete its unit -- every
8
- # +worker_retry_wait+ seconds. Any exceptions that take place during
8
+ # Worker::RETRY_WAIT seconds. Any exceptions that take place during
9
9
  # the course of the Action will cause the Worker to mark the WorkUnit as
10
10
  # having failed.
11
11
  class Worker
12
+
13
+ # The time between worker check-ins with the central server, informing
14
+ # it of the current status, and simply that it's still alive.
15
+ CHECK_IN_INTERVAL = 60
16
+
17
+ # Wait five seconds to retry, after internal communcication errors.
18
+ RETRY_WAIT = 5
12
19
 
13
20
  attr_reader :action
14
21
 
@@ -18,6 +25,7 @@ module CloudCrowd
18
25
  def initialize
19
26
  @id = $$
20
27
  @hostname = Socket.gethostname
28
+ @name = "#{@id}@#{@hostname}"
21
29
  @store = AssetStore.new
22
30
  @server = CloudCrowd.central_server
23
31
  @enabled_actions = CloudCrowd.actions.keys
@@ -37,7 +45,7 @@ module CloudCrowd
37
45
  keep_trying_to "complete work unit" do
38
46
  data = completion_params.merge({:status => 'succeeded', :output => result})
39
47
  unit_json = @server["/work/#{data[:id]}"].put(data)
40
- log "finished #{@action_name} in #{data[:time]} seconds"
48
+ log "finished #{display_work_unit} in #{data[:time]} seconds"
41
49
  clear_work_unit
42
50
  setup_work_unit(unit_json)
43
51
  end
@@ -46,14 +54,37 @@ module CloudCrowd
46
54
  # Mark the current work unit as failed, returning the exception to central.
47
55
  def fail_work_unit(exception)
48
56
  keep_trying_to "mark work unit as failed" do
49
- data = completion_params.merge({:status => 'failed', :output => exception.message})
57
+ data = completion_params.merge({:status => 'failed', :output => {'output' => exception.message}.to_json})
50
58
  unit_json = @server["/work/#{data[:id]}"].put(data)
51
- log "failed #{@action_name} in #{data[:time]} seconds\n#{exception.message}\n#{exception.backtrace}"
59
+ log "failed #{display_work_unit} in #{data[:time]} seconds\n#{exception.message}\n#{exception.backtrace}"
52
60
  clear_work_unit
53
61
  setup_work_unit(unit_json)
54
62
  end
55
63
  end
56
64
 
65
+ # Check in with the central server. Let it know the condition of the work
66
+ # thread, the action and status we're processing, and our hostname and PID.
67
+ def check_in(thread_status)
68
+ keep_trying_to "check in with central" do
69
+ @server["/worker"].put({
70
+ :name => @name,
71
+ :thread_status => thread_status
72
+ })
73
+ end
74
+ end
75
+
76
+ # Inform the central server that this worker is finished. This is the only
77
+ # remote method that doesn't retry on connection errors -- if the worker
78
+ # can't connect to the central server while it's trying to shutdown, it
79
+ # should close, regardless.
80
+ def check_out
81
+ @server["/worker"].put({
82
+ :name => @name,
83
+ :terminated => true
84
+ })
85
+ log 'exiting'
86
+ end
87
+
57
88
  # We expect and require internal communication between the central server
58
89
  # and the workers to succeed. If it fails for any reason, log it, and then
59
90
  # keep trying the same request.
@@ -61,11 +92,10 @@ module CloudCrowd
61
92
  begin
62
93
  yield
63
94
  rescue Exception => e
64
- wait_time = CloudCrowd.config[:worker_retry_wait]
65
- log "failed to #{title} -- retry in #{wait_time} seconds"
95
+ log "failed to #{title} -- retry in #{RETRY_WAIT} seconds"
66
96
  log e.message
67
97
  log e.backtrace
68
- sleep wait_time
98
+ sleep RETRY_WAIT
69
99
  retry
70
100
  end
71
101
  end
@@ -75,6 +105,11 @@ module CloudCrowd
75
105
  @action_name && @input && @options
76
106
  end
77
107
 
108
+ # Loggable string of the current work unit.
109
+ def display_work_unit
110
+ "unit ##{@options['work_unit_id']} (#{@action_name})"
111
+ end
112
+
78
113
  # Executes the current work unit, catching all exceptions as failures.
79
114
  def run_work_unit
80
115
  begin
@@ -103,7 +138,10 @@ module CloudCrowd
103
138
 
104
139
  # Common parameters to send back to central.
105
140
  def base_params
106
- @base_params ||= {:enabled_actions => @enabled_actions.join(',')}
141
+ @base_params ||= {
142
+ :worker_name => @name,
143
+ :worker_actions => @enabled_actions.join(',')
144
+ }
107
145
  end
108
146
 
109
147
  # Common parameters to send back to central upon unit completion,
@@ -124,13 +162,13 @@ module CloudCrowd
124
162
  @options['job_id'] = unit['job_id']
125
163
  @options['work_unit_id'] = unit['id']
126
164
  @options['attempts'] ||= unit['attempts']
127
- log "fetched work unit for #{@action_name}"
165
+ log "fetched #{display_work_unit}"
128
166
  return true
129
167
  end
130
168
 
131
169
  # Log a message to the daemon log. Includes PID for identification.
132
170
  def log(message)
133
- puts "Worker ##{@id}: #{message}"
171
+ puts "Worker ##{@id}: #{message}" unless ENV['RACK_ENV'] == 'test'
134
172
  end
135
173
 
136
174
  # When we're done with a unit, clear out our instance variables to make way
@@ -1,21 +1,74 @@
1
1
  body {
2
- background: #c7c7c7;
2
+ background: #979797;
3
3
  font-family: Arial;
4
4
  font-size: 12px;
5
+ color: #252525;
5
6
  }
6
7
 
8
+ .small_caps {
9
+ font: 11px Arial, sans-serif;
10
+ text-transform: uppercase;
11
+ }
12
+
13
+ #header {
14
+ height: 110px;
15
+ position: absolute;
16
+ top: 0; left: 0; right: 0;
17
+ background: url(/images/header_back.png);
18
+ }
19
+ #logo {
20
+ position: absolute;
21
+ left: 37px; top: 9px;
22
+ width: 236px; height: 91px;
23
+ background: url(/images/logo.png);
24
+ }
25
+
26
+ #disconnected {
27
+ position: absolute;
28
+ top: 122px; right: 15px;
29
+ background: #7f7f7f;
30
+ color: #333;
31
+ border: 1px solid #555;
32
+ font-size: 10px;
33
+ line-height: 18px;
34
+ padding: 3px 4px 1px 4px;
35
+ -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px;
36
+ }
37
+ #disconnected .server_error {
38
+ float: left;
39
+ width: 16px; height: 16px;
40
+ background: url(/images/server_error.png);
41
+ opacity: 0.7;
42
+ margin-right: 3px;
43
+ }
44
+
7
45
  #queue {
8
46
  position: absolute;
9
- top: 100px; left: 100px; right: 100px;
47
+ top: 16px; left: 327px; right: 15px;
10
48
  height: 77px;
49
+ overflow: hidden;
11
50
  }
51
+ #no_jobs {
52
+ text-align: left;
53
+ position: absolute;
54
+ bottom: 8px; right: 8px;
55
+ color: #999;
56
+ display: none;
57
+ }
58
+ #queue.no_jobs #no_jobs {
59
+ display: block;
60
+ }
12
61
  #queue_fill {
13
62
  position: absolute;
14
- width: 100%; height: 75px;
63
+ left: 0; right: 0; top: 0;
64
+ height: 75px;
15
65
  border: 1px solid #5c5c5c;
16
- -moz-border-radius: 10px; -webkit-border-radius: 10px;
66
+ -moz-border-radius: 10px; -webkit-border-radius: 10px; border-radius: 10px;
17
67
  background: transparent url(/images/queue_fill.png) repeat-x 0px -1px;
18
68
  }
69
+ #queue.no_jobs #queue_fill {
70
+ opacity: 0.3;
71
+ }
19
72
  #queue .job {
20
73
  position: relative;
21
74
  margin-top: 1px;
@@ -36,6 +89,9 @@ body {
36
89
  opacity: 0.5;
37
90
  overflow: hidden;
38
91
  }
92
+ #queue .completion.zero {
93
+ border: 0;
94
+ }
39
95
  #queue .percent_complete {
40
96
  position: absolute;
41
97
  bottom: 8px; left: 8px;
@@ -48,4 +104,118 @@ body {
48
104
  position: absolute;
49
105
  top: 8px; left: 8px;
50
106
  z-index: 10;
107
+ }
108
+
109
+ #sidebar {
110
+ position: absolute;
111
+ top: 120px; left: 10px; bottom: 10px;
112
+ width: 300px;
113
+ overflow: hidden;
114
+ }
115
+ #sidebar_background {
116
+ position: absolute;
117
+ top: 21px; bottom: 21px;
118
+ width: 298px;
119
+ background: #e0e0e0;
120
+ border: 1px solid #8b8b8b;
121
+ border-top: 0; border-bottom: 0;
122
+ }
123
+ .sidebar_back {
124
+ position: absolute;
125
+ height: 21px; width: 300px;
126
+ }
127
+ #sidebar_top {
128
+ top: 0px;
129
+ background: url(/images/sidebar_top.png);
130
+ }
131
+ #sidebar_bottom {
132
+ bottom: 0px;
133
+ background: url(/images/sidebar_bottom.png);
134
+ }
135
+ #sidebar_header {
136
+ position: absolute;
137
+ top: 5px; left: 8px;
138
+ color: #404040;
139
+ text-shadow: 0px 1px 1px #eee;
140
+ }
141
+ #sidebar_header.no_workers .no_workers,
142
+ #sidebar_header .has_workers {
143
+ display: block;
144
+ }
145
+ #sidebar_header .no_workers,
146
+ #sidebar_header.no_workers .has_workers {
147
+ display: none;
148
+ }
149
+ #workers {
150
+ position: absolute;
151
+ padding: 2px 0;
152
+ top: 21px; left: 0; bottom: 21px;
153
+ width: 298px;
154
+ overflow-y: auto; overflow-x: hidden;
155
+ }
156
+ #workers .worker {
157
+ border: 1px solid transparent;
158
+ margin: 1px 7px;
159
+ padding-left: 18px;
160
+ font-size: 11px;
161
+ line-height: 22px;
162
+ background: url(/images/bullet_white.png) no-repeat left center;
163
+ cursor: pointer;
164
+ }
165
+ #workers .worker.processing,
166
+ #workers .worker.splitting,
167
+ #workers .worker.merging {
168
+ background: url(/images/bullet_green.png) no-repeat left center;
169
+ }
170
+ #workers .worker:hover {
171
+ border: 1px solid #aaa;
172
+ border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px;
173
+ background-color: #ccc;
174
+ }
175
+
176
+ #worker_info {
177
+ position: absolute;
178
+ width: 231px; height: 79px;
179
+ margin: -9px 0 0 -20px;
180
+ background: url(/images/worker_info.png);
181
+ overflow: hidden;
182
+ cursor: pointer;
183
+ }
184
+ #worker_info_inner {
185
+ margin: 15px 15px 15px 32px;
186
+ line-height: 15px;
187
+ color: #333;
188
+ text-shadow: 0px 1px 1px #eee;
189
+ }
190
+ #worker_info.loading #worker_info_inner {
191
+ background: url(/images/worker_info_loading.gif) no-repeat right bottom;
192
+ width: 45px; height: 9px;
193
+ }
194
+ #worker_info.awake #worker_details,
195
+ #worker_sleeping {
196
+ display: block;
197
+ }
198
+ #worker_details, #worker_info.loading #worker_details,
199
+ #worker_info.loading #worker_sleeping, #worker_info.awake #worker_sleeping {
200
+ display: none;
201
+ }
202
+
203
+ #graphs {
204
+ position: absolute;
205
+ padding: 17px 15px 15px 17px;
206
+ top: 110px; left: 310px; right: 0px; bottom: 0;
207
+ overflow: hidden;
208
+ overflow-y: auto;
209
+ }
210
+ .graph_container {
211
+ margin-bottom: 25px;
212
+ }
213
+ .graph_title {
214
+ color: #333;
215
+ font-size: 16px;
216
+ text-shadow: 0px 1px 1px #eee;
217
+ margin-bottom: 10px;
218
+ }
219
+ .graph {
220
+ height: 150px;
51
221
  }
data/public/css/reset.css CHANGED
@@ -10,43 +10,33 @@ a, abbr, acronym, address, big, cite, code,
10
10
  del, dfn, em, font, img, ins, kbd, q, s, samp,
11
11
  small, strike, strong, sub, sup, tt, var,
12
12
  dl, dt, dd, ol, ul, li,
13
- fieldset, form, label, legend,
14
- table, caption, tbody, tfoot, thead, tr, th, td {
15
- margin: 0;
16
- padding: 0;
17
- border: 0;
18
- outline: 0;
19
- font-weight: inherit;
20
- font-style: inherit;
21
- font-size: 100%;
22
- font-family: inherit;
23
- vertical-align: baseline;
13
+ fieldset, form, label, legend {
14
+ margin: 0;
15
+ padding: 0;
16
+ border: 0;
17
+ outline: 0;
18
+ font-weight: inherit;
19
+ font-style: inherit;
20
+ font-size: 100%;
21
+ font-family: inherit;
22
+ vertical-align: baseline;
24
23
  }
25
24
  /* remember to define focus styles! */
26
25
  :focus {
27
- outline: 0;
26
+ outline: 0;
28
27
  }
29
28
  body {
30
- line-height: 1;
31
- color: black;
32
- background: white;
29
+ line-height: 1;
30
+ color: black;
31
+ background: white;
33
32
  }
34
33
  ol, ul {
35
- list-style: none;
36
- }
37
- /* tables still need 'cellspacing="0"' in the markup */
38
- table {
39
- border-collapse: separate;
40
- border-spacing: 0;
41
- }
42
- caption, th, td {
43
- text-align: left;
44
- font-weight: normal;
34
+ list-style: none;
45
35
  }
46
36
  blockquote:before, blockquote:after,
47
37
  q:before, q:after {
48
- content: "";
38
+ content: "";
49
39
  }
50
40
  blockquote, q {
51
- quotes: "" "";
41
+ quotes: "" "";
52
42
  }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,33 +1,86 @@
1
+ // The Console handles all of the Admin interaction with the active workers
2
+ // and job queue.
3
+ //
1
4
  // Think about pulling in the DCJS framework, instead of just raw jQuery here.
5
+ // Leaving it hacked together like this just cries out for templates, dunnit?
2
6
  window.Console = {
3
7
 
8
+ // Maximum number of data points to record and graph.
9
+ MAX_DATA_POINTS : 100,
10
+
11
+ // Milliseconds between polling the central server for updates to Job progress.
4
12
  POLL_INTERVAL : 3000,
5
13
 
14
+ // Default speed for all animations.
6
15
  ANIMATION_SPEED : 300,
7
16
 
17
+ // Keep this in sync with the map in cloud-crowd.rb
18
+ DISPLAY_STATUS_MAP : ['unknown', 'processing', 'succeeded', 'failed', 'splitting', 'merging'],
19
+
20
+ // Images to preload
21
+ PRELOAD_IMAGES : ['/images/server_error.png'],
22
+
23
+ // All options for drawing the system graphs.
24
+ GRAPH_OPTIONS : {
25
+ xaxis : {mode : 'time', timeformat : '%M:%S'},
26
+ yaxis : {tickDecimals : 0},
27
+ legend : {show : false},
28
+ grid : {backgroundColor : '#7f7f7f', color : '#555', tickColor : '#666', borderWidth : 2}
29
+ },
30
+ JOBS_COLOR : '#db3a0f',
31
+ WORKERS_COLOR : '#a1003d',
32
+ WORK_UNITS_COLOR : '#ffba14',
33
+
34
+ // Starting the console begins polling the server.
8
35
  initialize : function() {
36
+ this._jobsHistory = [];
37
+ this._workersHistory = [];
38
+ this._workUnitsHistory = [];
39
+ this._histories = [this._jobsHistory, this._workersHistory, this._workUnitsHistory];
9
40
  this._queue = $('#jobs');
10
- this.getJobs();
41
+ this._workerInfo = $('#worker_info');
42
+ this._disconnected = $('#disconnected');
43
+ $(window).bind('resize', Console.renderGraphs);
44
+ $('#workers .worker').live('click', Console.getWorkerInfo);
45
+ this.getStatus();
46
+ $.each(this.PRELOAD_IMAGES, function(){ var i = new Image(); i.src = this; });
11
47
  },
12
48
 
13
- getJobs : function() {
14
- $.get('/jobs', null, function(resp) {
15
- Console._jobs = resp;
49
+ // Request the lastest status of all jobs and workers, re-render or update
50
+ // the DOM to reflect.
51
+ getStatus : function() {
52
+ $.ajax({url : '/status', dataType : 'json', success : function(resp) {
53
+ Console._jobs = resp.jobs;
54
+ Console._workers = resp.workers;
55
+ Console._workUnitCount = resp.work_unit_count;
56
+ Console.recordDataPoint();
57
+ if (Console._disconnected.is(':visible')) Console._disconnected.fadeOut(Console.ANIMATION_SPEED);
58
+ $('#queue').toggleClass('no_jobs', Console._jobs.length <= 0);
16
59
  Console.renderJobs();
17
- setTimeout(Console.getJobs, Console.POLL_INTERVAL);
18
- }, 'json');
60
+ Console.renderWorkers();
61
+ Console.renderGraphs();
62
+ setTimeout(Console.getStatus, Console.POLL_INTERVAL);
63
+ }, error : function(request, status, errorThrown) {
64
+ if (!Console._disconnected.is(':visible')) Console._disconnected.fadeIn(Console.ANIMATION_SPEED);
65
+ setTimeout(Console.getStatus, Console.POLL_INTERVAL);
66
+ }});
19
67
  },
20
68
 
69
+ // Render an individual job afresh.
21
70
  renderJob : function(job) {
22
- this._queue.prepend('<div class="job" id="job_' + job.id + '" style="width:' + job.width + '%; background: #' + job.color + ';"><div class="completion" style="width:' + job.percent_complete + '%;"></div><div class="percent_complete">' + job.percent_complete + '%</div><div class="job_id">#' + job.id + '</div></div>');
71
+ 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>');
23
72
  },
24
73
 
74
+ // Animate the update to an existing job in the queue.
25
75
  updateJob : function(job, jobEl) {
26
76
  jobEl.animate({width : job.width + '%'}, this.ANIMATION_SPEED);
27
- $('.completion', jobEl).animate({width : job.percent_complete + '%'}, this.ANIMATION_SPEED);
77
+ var completion = $('.completion', jobEl);
78
+ if (job.percent_complete > 0) completion.removeClass('zero');
79
+ completion.animate({width : job.percent_complete + '%'}, this.ANIMATION_SPEED);
28
80
  $('.percent_complete', jobEl).html(job.percent_complete + '%');
29
81
  },
30
82
 
83
+ // Render all jobs, calculating relative widths and completions.
31
84
  renderJobs : function() {
32
85
  var totalUnits = 0;
33
86
  var totalWidth = this._queue.width();
@@ -36,14 +89,78 @@ window.Console = {
36
89
  jobIds.push(this.id);
37
90
  totalUnits += this.work_units;
38
91
  });
92
+ $.each($('.job'), function() {
93
+ var el = this;
94
+ if (jobIds.indexOf(parseInt(el.id.replace(/\D/g, ''), 10)) < 0) {
95
+ $(el).animate({width : '0%'}, Console.ANIMATION_SPEED - 50, 'linear', function() {
96
+ $(el).remove();
97
+ });
98
+ }
99
+ });
39
100
  $.each(this._jobs, function() {
40
101
  this.width = (this.work_units / totalUnits) * 100;
41
102
  var jobEl = $('#job_' + this.id);
42
103
  jobEl[0] ? Console.updateJob(this, jobEl) : Console.renderJob(this);
43
104
  });
44
- $.each($('.job'), function() {
45
- if (jobIds.indexOf(parseInt(this.id.replace(/\D/g, ''), 10)) < 0) $(this).remove();
105
+ },
106
+
107
+ // Re-render all workers from scratch each time.
108
+ renderWorkers : function() {
109
+ var header = $('#sidebar_header');
110
+ $('.has_workers', header).html(this._workers.length + " Active Worker Daemons");
111
+ header.toggleClass('no_workers', this._workers.length <= 0);
112
+ $('#workers').html($.map(this._workers, function(w) {
113
+ return '<div class="worker ' + w.status + '" rel="' + w.name + '">' + w.name + '</div>';
114
+ }).join(''));
115
+ },
116
+
117
+ // Record the current state and re-render all graphs.
118
+ recordDataPoint : function() {
119
+ var timestamp = (new Date()).getTime();
120
+ this._jobsHistory.push([timestamp, this._jobs.length]);
121
+ this._workersHistory.push([timestamp, this._workers.length]);
122
+ this._workUnitsHistory.push([timestamp, this._workUnitCount]);
123
+ $.each(this._histories, function() {
124
+ if (this.length > Console.MAX_DATA_POINTS) this.shift();
46
125
  });
126
+ },
127
+
128
+ // Convert our recorded data points into a format Flot can understand.
129
+ renderGraphs : function() {
130
+ $.plot($('#jobs_graph'), [{label : 'Jobs in Queue', color : Console.JOBS_COLOR, data : Console._jobsHistory}], Console.GRAPH_OPTIONS);
131
+ $.plot($('#workers_graph'), [{label : 'Active Workers', color : Console.WORKERS_COLOR, data : Console._workersHistory}], Console.GRAPH_OPTIONS);
132
+ $.plot($('#work_units_graph'), [{label : 'Work Units in Queue', color : Console.WORK_UNITS_COLOR, data : Console._workUnitsHistory}], Console.GRAPH_OPTIONS);
133
+ },
134
+
135
+ // Request the Worker info from the central server.
136
+ getWorkerInfo : function(e) {
137
+ e.stopImmediatePropagation();
138
+ var info = Console._workerInfo;
139
+ var row = $(this);
140
+ info.addClass('loading');
141
+ $.get('/worker/' + row.attr('rel'), null, Console.renderWorkerInfo, 'json');
142
+ info.css({top : row.offset().top, left : 325});
143
+ info.fadeIn(Console.ANIMATION_SPEED);
144
+ $(document).bind('click', Console.hideWorkerInfo);
145
+ return false;
146
+ },
147
+
148
+ // When we receieve worker info, update the bubble.
149
+ renderWorkerInfo : function(resp) {
150
+ var info = Console._workerInfo;
151
+ info.toggleClass('awake', !!resp.status);
152
+ info.removeClass('loading');
153
+ if (!resp.status) return;
154
+ $('.status', info).html(Console.DISPLAY_STATUS_MAP[resp.status]);
155
+ $('.action', info).html(resp.action);
156
+ $('.job_id', info).html(resp.job_id);
157
+ $('.work_unit_id', info).html(resp.id);
158
+ },
159
+
160
+ // Hide worker info and unbind the global hide handler.
161
+ hideWorkerInfo : function() {
162
+ $(document).unbind('click', Console.hideWorkerInfo);
163
+ Console._workerInfo.fadeOut(Console.ANIMATION_SPEED);
47
164
  }
48
165
 
49
166
  };
@@ -0,0 +1 @@
1
+ if(!window.CanvasRenderingContext2D){(function(){var m=Math;var mr=m.round;var ms=m.sin;var mc=m.cos;var Z=10;var Z2=Z/2;var G_vmlCanvasManager_={init:function(opt_doc){var doc=opt_doc||document;if(/MSIE/.test(navigator.userAgent)&&!window.opera){var self=this;doc.attachEvent("onreadystatechange",function(){self.init_(doc)})}},init_:function(doc){if(doc.readyState=="complete"){if(!doc.namespaces["g_vml_"]){doc.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml")}var ss=doc.createStyleSheet();ss.cssText="canvas{display:inline-block;overflow:hidden;"+"text-align:left;width:300px;height:150px}"+"g_vml_\\:*{behavior:url(#default#VML)}";var els=doc.getElementsByTagName("canvas");for(var i=0;i<els.length;i++){if(!els[i].getContext){this.initElement(els[i])}}}},fixElement_:function(el){var outerHTML=el.outerHTML;var newEl=el.ownerDocument.createElement(outerHTML);if(outerHTML.slice(-2)!="/>"){var tagName="/"+el.tagName;var ns;while((ns=el.nextSibling)&&ns.tagName!=tagName){ns.removeNode()}if(ns){ns.removeNode()}}el.parentNode.replaceChild(newEl,el);return newEl},initElement:function(el){el=this.fixElement_(el);el.getContext=function(){if(this.context_){return this.context_}return this.context_=new CanvasRenderingContext2D_(this)};el.attachEvent('onpropertychange',onPropertyChange);el.attachEvent('onresize',onResize);var attrs=el.attributes;if(attrs.width&&attrs.width.specified){el.style.width=attrs.width.nodeValue+"px"}else{el.width=el.clientWidth}if(attrs.height&&attrs.height.specified){el.style.height=attrs.height.nodeValue+"px"}else{el.height=el.clientHeight}return el}};function onPropertyChange(e){var el=e.srcElement;switch(e.propertyName){case'width':el.style.width=el.attributes.width.nodeValue+"px";el.getContext().clearRect();break;case'height':el.style.height=el.attributes.height.nodeValue+"px";el.getContext().clearRect();break}}function onResize(e){var el=e.srcElement;if(el.firstChild){el.firstChild.style.width=el.clientWidth+'px';el.firstChild.style.height=el.clientHeight+'px'}}G_vmlCanvasManager_.init();var dec2hex=[];for(var i=0;i<16;i++){for(var j=0;j<16;j++){dec2hex[i*16+j]=i.toString(16)+j.toString(16)}}function createMatrixIdentity(){return[[1,0,0],[0,1,0],[0,0,1]]}function matrixMultiply(m1,m2){var result=createMatrixIdentity();for(var x=0;x<3;x++){for(var y=0;y<3;y++){var sum=0;for(var z=0;z<3;z++){sum+=m1[x][z]*m2[z][y]}result[x][y]=sum}}return result}function copyState(o1,o2){o2.fillStyle=o1.fillStyle;o2.lineCap=o1.lineCap;o2.lineJoin=o1.lineJoin;o2.lineWidth=o1.lineWidth;o2.miterLimit=o1.miterLimit;o2.shadowBlur=o1.shadowBlur;o2.shadowColor=o1.shadowColor;o2.shadowOffsetX=o1.shadowOffsetX;o2.shadowOffsetY=o1.shadowOffsetY;o2.strokeStyle=o1.strokeStyle;o2.arcScaleX_=o1.arcScaleX_;o2.arcScaleY_=o1.arcScaleY_}function processStyle(styleString){var str,alpha=1;styleString=String(styleString);if(styleString.substring(0,3)=="rgb"){var start=styleString.indexOf("(",3);var end=styleString.indexOf(")",start+1);var guts=styleString.substring(start+1,end).split(",");str="#";for(var i=0;i<3;i++){str+=dec2hex[Number(guts[i])]}if((guts.length==4)&&(styleString.substr(3,1)=="a")){alpha=guts[3]}}else{str=styleString}return[str,alpha]}function processLineCap(lineCap){switch(lineCap){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function CanvasRenderingContext2D_(surfaceElement){this.m_=createMatrixIdentity();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=Z*1;this.globalAlpha=1;this.canvas=surfaceElement;var el=surfaceElement.ownerDocument.createElement('div');el.style.width=surfaceElement.clientWidth+'px';el.style.height=surfaceElement.clientHeight+'px';el.style.overflow='hidden';el.style.position='absolute';surfaceElement.appendChild(el);this.element_=el;this.arcScaleX_=1;this.arcScaleY_=1}var contextPrototype=CanvasRenderingContext2D_.prototype;contextPrototype.clearRect=function(){this.element_.innerHTML="";this.currentPath_=[]};contextPrototype.beginPath=function(){this.currentPath_=[]};contextPrototype.moveTo=function(aX,aY){this.currentPath_.push({type:"moveTo",x:aX,y:aY});this.currentX_=aX;this.currentY_=aY};contextPrototype.lineTo=function(aX,aY){this.currentPath_.push({type:"lineTo",x:aX,y:aY});this.currentX_=aX;this.currentY_=aY};contextPrototype.bezierCurveTo=function(aCP1x,aCP1y,aCP2x,aCP2y,aX,aY){this.currentPath_.push({type:"bezierCurveTo",cp1x:aCP1x,cp1y:aCP1y,cp2x:aCP2x,cp2y:aCP2y,x:aX,y:aY});this.currentX_=aX;this.currentY_=aY};contextPrototype.quadraticCurveTo=function(aCPx,aCPy,aX,aY){var cp1x=this.currentX_+2.0/3.0*(aCPx-this.currentX_);var cp1y=this.currentY_+2.0/3.0*(aCPy-this.currentY_);var cp2x=cp1x+(aX-this.currentX_)/3.0;var cp2y=cp1y+(aY-this.currentY_)/3.0;this.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,aX,aY)};contextPrototype.arc=function(aX,aY,aRadius,aStartAngle,aEndAngle,aClockwise){aRadius*=Z;var arcType=aClockwise?"at":"wa";var xStart=aX+(mc(aStartAngle)*aRadius)-Z2;var yStart=aY+(ms(aStartAngle)*aRadius)-Z2;var xEnd=aX+(mc(aEndAngle)*aRadius)-Z2;var yEnd=aY+(ms(aEndAngle)*aRadius)-Z2;if(xStart==xEnd&&!aClockwise){xStart+=0.125}this.currentPath_.push({type:arcType,x:aX,y:aY,radius:aRadius,xStart:xStart,yStart:yStart,xEnd:xEnd,yEnd:yEnd})};contextPrototype.rect=function(aX,aY,aWidth,aHeight){this.moveTo(aX,aY);this.lineTo(aX+aWidth,aY);this.lineTo(aX+aWidth,aY+aHeight);this.lineTo(aX,aY+aHeight);this.closePath()};contextPrototype.strokeRect=function(aX,aY,aWidth,aHeight){this.beginPath();this.moveTo(aX,aY);this.lineTo(aX+aWidth,aY);this.lineTo(aX+aWidth,aY+aHeight);this.lineTo(aX,aY+aHeight);this.closePath();this.stroke()};contextPrototype.fillRect=function(aX,aY,aWidth,aHeight){this.beginPath();this.moveTo(aX,aY);this.lineTo(aX+aWidth,aY);this.lineTo(aX+aWidth,aY+aHeight);this.lineTo(aX,aY+aHeight);this.closePath();this.fill()};contextPrototype.createLinearGradient=function(aX0,aY0,aX1,aY1){var gradient=new CanvasGradient_("gradient");return gradient};contextPrototype.createRadialGradient=function(aX0,aY0,aR0,aX1,aY1,aR1){var gradient=new CanvasGradient_("gradientradial");gradient.radius1_=aR0;gradient.radius2_=aR1;gradient.focus_.x=aX0;gradient.focus_.y=aY0;return gradient};contextPrototype.drawImage=function(image,var_args){var dx,dy,dw,dh,sx,sy,sw,sh;var oldRuntimeWidth=image.runtimeStyle.width;var oldRuntimeHeight=image.runtimeStyle.height;image.runtimeStyle.width='auto';image.runtimeStyle.height='auto';var w=image.width;var h=image.height;image.runtimeStyle.width=oldRuntimeWidth;image.runtimeStyle.height=oldRuntimeHeight;if(arguments.length==3){dx=arguments[1];dy=arguments[2];sx=sy=0;sw=dw=w;sh=dh=h}else if(arguments.length==5){dx=arguments[1];dy=arguments[2];dw=arguments[3];dh=arguments[4];sx=sy=0;sw=w;sh=h}else if(arguments.length==9){sx=arguments[1];sy=arguments[2];sw=arguments[3];sh=arguments[4];dx=arguments[5];dy=arguments[6];dw=arguments[7];dh=arguments[8]}else{throw"Invalid number of arguments";}var d=this.getCoords_(dx,dy);var w2=sw/2;var h2=sh/2;var vmlStr=[];var W=10;var H=10;vmlStr.push(' <g_vml_:group',' coordsize="',Z*W,',',Z*H,'"',' coordorigin="0,0"',' style="width:',W,';height:',H,';position:absolute;');if(this.m_[0][0]!=1||this.m_[0][1]){var filter=[];filter.push("M11='",this.m_[0][0],"',","M12='",this.m_[1][0],"',","M21='",this.m_[0][1],"',","M22='",this.m_[1][1],"',","Dx='",mr(d.x/Z),"',","Dy='",mr(d.y/Z),"'");var max=d;var c2=this.getCoords_(dx+dw,dy);var c3=this.getCoords_(dx,dy+dh);var c4=this.getCoords_(dx+dw,dy+dh);max.x=Math.max(max.x,c2.x,c3.x,c4.x);max.y=Math.max(max.y,c2.y,c3.y,c4.y);vmlStr.push("padding:0 ",mr(max.x/Z),"px ",mr(max.y/Z),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",filter.join(""),", sizingmethod='clip');")}else{vmlStr.push("top:",mr(d.y/Z),"px;left:",mr(d.x/Z),"px;")}vmlStr.push(' ">','<g_vml_:image src="',image.src,'"',' style="width:',Z*dw,';',' height:',Z*dh,';"',' cropleft="',sx/w,'"',' croptop="',sy/h,'"',' cropright="',(w-sx-sw)/w,'"',' cropbottom="',(h-sy-sh)/h,'"',' />','</g_vml_:group>');this.element_.insertAdjacentHTML("BeforeEnd",vmlStr.join(""))};contextPrototype.stroke=function(aFill){var lineStr=[];var lineOpen=false;var a=processStyle(aFill?this.fillStyle:this.strokeStyle);var color=a[0];var opacity=a[1]*this.globalAlpha;var W=10;var H=10;lineStr.push('<g_vml_:shape',' fillcolor="',color,'"',' filled="',Boolean(aFill),'"',' style="position:absolute;width:',W,';height:',H,';"',' coordorigin="0 0" coordsize="',Z*W,' ',Z*H,'"',' stroked="',!aFill,'"',' strokeweight="',this.lineWidth,'"',' strokecolor="',color,'"',' path="');var newSeq=false;var min={x:null,y:null};var max={x:null,y:null};for(var i=0;i<this.currentPath_.length;i++){var p=this.currentPath_[i];if(p.type=="moveTo"){lineStr.push(" m ");var c=this.getCoords_(p.x,p.y);lineStr.push(mr(c.x),",",mr(c.y))}else if(p.type=="lineTo"){lineStr.push(" l ");var c=this.getCoords_(p.x,p.y);lineStr.push(mr(c.x),",",mr(c.y))}else if(p.type=="close"){lineStr.push(" x ")}else if(p.type=="bezierCurveTo"){lineStr.push(" c ");var c=this.getCoords_(p.x,p.y);var c1=this.getCoords_(p.cp1x,p.cp1y);var c2=this.getCoords_(p.cp2x,p.cp2y);lineStr.push(mr(c1.x),",",mr(c1.y),",",mr(c2.x),",",mr(c2.y),",",mr(c.x),",",mr(c.y))}else if(p.type=="at"||p.type=="wa"){lineStr.push(" ",p.type," ");var c=this.getCoords_(p.x,p.y);var cStart=this.getCoords_(p.xStart,p.yStart);var cEnd=this.getCoords_(p.xEnd,p.yEnd);lineStr.push(mr(c.x-this.arcScaleX_*p.radius),",",mr(c.y-this.arcScaleY_*p.radius)," ",mr(c.x+this.arcScaleX_*p.radius),",",mr(c.y+this.arcScaleY_*p.radius)," ",mr(cStart.x),",",mr(cStart.y)," ",mr(cEnd.x),",",mr(cEnd.y))}if(c){if(min.x==null||c.x<min.x){min.x=c.x}if(max.x==null||c.x>max.x){max.x=c.x}if(min.y==null||c.y<min.y){min.y=c.y}if(max.y==null||c.y>max.y){max.y=c.y}}}lineStr.push(' ">');if(typeof this.fillStyle=="object"){var focus={x:"50%",y:"50%"};var width=(max.x-min.x);var height=(max.y-min.y);var dimension=(width>height)?width:height;focus.x=mr((this.fillStyle.focus_.x/width)*100+50)+"%";focus.y=mr((this.fillStyle.focus_.y/height)*100+50)+"%";var colors=[];if(this.fillStyle.type_=="gradientradial"){var inside=(this.fillStyle.radius1_/dimension*100);var expansion=(this.fillStyle.radius2_/dimension*100)-inside}else{var inside=0;var expansion=100}var insidecolor={offset:null,color:null};var outsidecolor={offset:null,color:null};this.fillStyle.colors_.sort(function(cs1,cs2){return cs1.offset-cs2.offset});for(var i=0;i<this.fillStyle.colors_.length;i++){var fs=this.fillStyle.colors_[i];colors.push((fs.offset*expansion)+inside,"% ",fs.color,",");if(fs.offset>insidecolor.offset||insidecolor.offset==null){insidecolor.offset=fs.offset;insidecolor.color=fs.color}if(fs.offset<outsidecolor.offset||outsidecolor.offset==null){outsidecolor.offset=fs.offset;outsidecolor.color=fs.color}}colors.pop();lineStr.push('<g_vml_:fill',' color="',outsidecolor.color,'"',' color2="',insidecolor.color,'"',' type="',this.fillStyle.type_,'"',' focusposition="',focus.x,', ',focus.y,'"',' colors="',colors.join(""),'"',' opacity="',opacity,'" />')}else if(aFill){lineStr.push('<g_vml_:fill color="',color,'" opacity="',opacity,'" />')}else{lineStr.push('<g_vml_:stroke',' opacity="',opacity,'"',' joinstyle="',this.lineJoin,'"',' miterlimit="',this.miterLimit,'"',' endcap="',processLineCap(this.lineCap),'"',' weight="',this.lineWidth,'px"',' color="',color,'" />')}lineStr.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",lineStr.join(""))};contextPrototype.fill=function(){this.stroke(true)};contextPrototype.closePath=function(){this.currentPath_.push({type:"close"})};contextPrototype.getCoords_=function(aX,aY){return{x:Z*(aX*this.m_[0][0]+aY*this.m_[1][0]+this.m_[2][0])-Z2,y:Z*(aX*this.m_[0][1]+aY*this.m_[1][1]+this.m_[2][1])-Z2}};contextPrototype.save=function(){var o={};copyState(this,o);this.aStack_.push(o);this.mStack_.push(this.m_);this.m_=matrixMultiply(createMatrixIdentity(),this.m_)};contextPrototype.restore=function(){copyState(this.aStack_.pop(),this);this.m_=this.mStack_.pop()};contextPrototype.translate=function(aX,aY){var m1=[[1,0,0],[0,1,0],[aX,aY,1]];this.m_=matrixMultiply(m1,this.m_)};contextPrototype.rotate=function(aRot){var c=mc(aRot);var s=ms(aRot);var m1=[[c,s,0],[-s,c,0],[0,0,1]];this.m_=matrixMultiply(m1,this.m_)};contextPrototype.scale=function(aX,aY){this.arcScaleX_*=aX;this.arcScaleY_*=aY;var m1=[[aX,0,0],[0,aY,0],[0,0,1]];this.m_=matrixMultiply(m1,this.m_)};contextPrototype.clip=function(){};contextPrototype.arcTo=function(){};contextPrototype.createPattern=function(){return new CanvasPattern_};function CanvasGradient_(aType){this.type_=aType;this.radius1_=0;this.radius2_=0;this.colors_=[];this.focus_={x:0,y:0}}CanvasGradient_.prototype.addColorStop=function(aOffset,aColor){aColor=processStyle(aColor);this.colors_.push({offset:1-aOffset,color:aColor})};function CanvasPattern_(){}G_vmlCanvasManager=G_vmlCanvasManager_;CanvasRenderingContext2D=CanvasRenderingContext2D_;CanvasGradient=CanvasGradient_;CanvasPattern=CanvasPattern_})()}