documentcloud-cloud-crowd 0.0.5 → 0.0.6

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