qless 0.9.3 → 0.10.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.
Files changed (65) hide show
  1. data/Gemfile +9 -3
  2. data/README.md +70 -25
  3. data/Rakefile +125 -9
  4. data/exe/install_phantomjs +21 -0
  5. data/lib/qless.rb +115 -76
  6. data/lib/qless/config.rb +11 -9
  7. data/lib/qless/failure_formatter.rb +43 -0
  8. data/lib/qless/job.rb +201 -102
  9. data/lib/qless/job_reservers/ordered.rb +7 -1
  10. data/lib/qless/job_reservers/round_robin.rb +16 -6
  11. data/lib/qless/job_reservers/shuffled_round_robin.rb +9 -2
  12. data/lib/qless/lua/qless-lib.lua +2463 -0
  13. data/lib/qless/lua/qless.lua +2012 -0
  14. data/lib/qless/lua_script.rb +63 -12
  15. data/lib/qless/middleware/memory_usage_monitor.rb +62 -0
  16. data/lib/qless/middleware/metriks.rb +45 -0
  17. data/lib/qless/middleware/redis_reconnect.rb +6 -3
  18. data/lib/qless/middleware/requeue_exceptions.rb +94 -0
  19. data/lib/qless/middleware/retry_exceptions.rb +38 -9
  20. data/lib/qless/middleware/sentry.rb +3 -7
  21. data/lib/qless/middleware/timeout.rb +64 -0
  22. data/lib/qless/queue.rb +90 -55
  23. data/lib/qless/server.rb +177 -130
  24. data/lib/qless/server/views/_job.erb +33 -15
  25. data/lib/qless/server/views/completed.erb +11 -0
  26. data/lib/qless/server/views/layout.erb +70 -11
  27. data/lib/qless/server/views/overview.erb +93 -53
  28. data/lib/qless/server/views/queue.erb +9 -8
  29. data/lib/qless/server/views/queues.erb +18 -1
  30. data/lib/qless/subscriber.rb +37 -22
  31. data/lib/qless/tasks.rb +5 -10
  32. data/lib/qless/test_helpers/worker_helpers.rb +55 -0
  33. data/lib/qless/version.rb +3 -1
  34. data/lib/qless/worker.rb +4 -413
  35. data/lib/qless/worker/base.rb +247 -0
  36. data/lib/qless/worker/forking.rb +245 -0
  37. data/lib/qless/worker/serial.rb +41 -0
  38. metadata +135 -52
  39. data/lib/qless/qless-core/cancel.lua +0 -101
  40. data/lib/qless/qless-core/complete.lua +0 -233
  41. data/lib/qless/qless-core/config.lua +0 -56
  42. data/lib/qless/qless-core/depends.lua +0 -65
  43. data/lib/qless/qless-core/deregister_workers.lua +0 -12
  44. data/lib/qless/qless-core/fail.lua +0 -117
  45. data/lib/qless/qless-core/failed.lua +0 -83
  46. data/lib/qless/qless-core/get.lua +0 -37
  47. data/lib/qless/qless-core/heartbeat.lua +0 -51
  48. data/lib/qless/qless-core/jobs.lua +0 -41
  49. data/lib/qless/qless-core/pause.lua +0 -18
  50. data/lib/qless/qless-core/peek.lua +0 -165
  51. data/lib/qless/qless-core/pop.lua +0 -314
  52. data/lib/qless/qless-core/priority.lua +0 -32
  53. data/lib/qless/qless-core/put.lua +0 -169
  54. data/lib/qless/qless-core/qless-lib.lua +0 -2354
  55. data/lib/qless/qless-core/qless.lua +0 -1862
  56. data/lib/qless/qless-core/queues.lua +0 -58
  57. data/lib/qless/qless-core/recur.lua +0 -190
  58. data/lib/qless/qless-core/retry.lua +0 -73
  59. data/lib/qless/qless-core/stats.lua +0 -92
  60. data/lib/qless/qless-core/tag.lua +0 -100
  61. data/lib/qless/qless-core/track.lua +0 -79
  62. data/lib/qless/qless-core/unfail.lua +0 -54
  63. data/lib/qless/qless-core/unpause.lua +0 -12
  64. data/lib/qless/qless-core/workers.lua +0 -69
  65. data/lib/qless/wait_until.rb +0 -19
@@ -4,13 +4,13 @@
4
4
  <div class="row">
5
5
  <div class="span6">
6
6
  <h2 style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
7
- <a href="<%= u "/jobs/#{job.jid}" %>"><%= job.jid[0..8] %>...</a> | <%= job.klass_name %>
7
+ <a href="<%= u "/jobs/#{job.jid}" %>" title="<%= job.jid %>"><%= job.jid[0..8] %>...</a> | <span title="<%= job.klass_name %>"><%= job.klass_name %></span>
8
8
  </h2>
9
9
  </div>
10
10
  <div class="span3">
11
11
  <h2 style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
12
12
  <strong>
13
- | <%= job.state %> / <a href="<%= u "/queues/#{job.queue_name}" %>"><%= job.queue_name %></a><%= job.worker_name.nil? ? "/ #{job.worker_name}" : "" %>
13
+ | <%= job.state %> / <a href="<%= u "/queues/#{CGI::escape(job.queue_name)}" %>" title="<%= job.queue_name %><%= job.worker_name.nil? ? "/ #{job.worker_name}" : "" %>"><%= job.queue_name %></a><%= job.worker_name.nil? ? "/ #{job.worker_name}" : "" %>
14
14
  </strong>
15
15
  </h2>
16
16
  </div>
@@ -20,9 +20,12 @@
20
20
  <% if (job.state != "complete") %>
21
21
  <button title="delete" class="btn btn-danger" onclick="confirmation(this, 'Delete?', function() { cancel('<%= job.jid %>', fade) })"><i class="icon-remove"></i></button>
22
22
  <% end %>
23
+ <% if (job.state == "running") %>
24
+ <button title="Time out job" class="btn btn-danger" onclick="confirmation(this, 'Time out job?', function() { timeout('<%= job.jid %>') })"><i class="icon-time"></i></button>
25
+ <% end %>
23
26
  <button title="track" class="btn<%= job.tracked ? " active" : "" %>" data-toggle="button" onclick="$(this).hasClass('active') ? untrack('<%= job.jid %>', fade) : track('<%= job.jid %>', [], fade)"><i class="icon-flag"></i></button>
24
27
  <% if (job.state == 'failed') %>
25
- <button title="retry" class="btn btn-success" onclick="retry('<%= job.jid %>', fade)"><i class="icon-repeat"></i></button>
28
+ <button title="requeue" class="btn btn-success" onclick="retry('<%= job.jid %>', fade)"><i class="icon-repeat"></i></button>
26
29
  <% end %>
27
30
  <button title="move" class="btn dropdown-toggle btn-success" data-toggle="dropdown">
28
31
  <i class="caret"></i>
@@ -56,7 +59,7 @@
56
59
  <div style="float:left; margin-right: 10px"><h3>Dependencies:</h3></div>
57
60
  <% job.dependencies.each do |jid| %>
58
61
  <div class="btn-group" style="float:left; margin-right: 10px" id="<%= sanitize_attr("#{job.jid}-dependson-#{jid}") %>">
59
- <button class="btn" onclick="window.open('<%= u "/jobs/#{jid}" %>', '_blank')"><%= jid[0...8] %>...</button>
62
+ <button class="btn" onclick="window.open('<%= u "/jobs/#{jid}" %>', '_blank')" title="<%= jid %>"><%= jid[0...8] %>...</button>
60
63
  <button class="btn dropdown-toggle" onclick="confirmation(this, 'Undepend?', function() { undepend('<%= job.jid %>', '<%= jid %>', function() { $('#<%= sanitize_attr("#{job.jid}-dependson-#{jid}") %>').remove()} ); })">
61
64
  <i class="icon-remove"></i>
62
65
  </button>
@@ -72,7 +75,7 @@
72
75
  <div style="float:left; margin-right: 10px"><h3>Dependents:</h3></div>
73
76
  <% job.dependents.each do |jid| %>
74
77
  <div class="btn-group" style="float:left; margin-right: 10px" id="<%= sanitize_attr("#{job.jid}-dependents-#{jid}") %>">
75
- <button class="btn" onclick="window.open('<%= u "/jobs/#{jid}" %>', '_blank')"><%= jid[0...8] %>...</button>
78
+ <button class="btn" onclick="window.open('<%= u "/jobs/#{jid}" %>', '_blank')" title="<%= jid %>"><%= jid[0...8] %>...</button>
76
79
  <button class="btn dropdown-toggle" onclick="confirmation(this, 'Undepend?', function() { undepend('<%= jid %>', '<%= job.jid %>', function() { $('#<%= sanitize_attr("#{job.jid}-dependents-#{jid}") %>').remove()} ); })">
77
80
  <i class="icon-remove"></i>
78
81
  </button>
@@ -92,7 +95,7 @@
92
95
  </button>
93
96
  </div>
94
97
  <% end %>
95
-
98
+
96
99
  <!-- One for adding new tags -->
97
100
  <div class="btn-group" style="float:left">
98
101
  <input class="span1 add-tag" type="text" placeholder="Add Tag" onchange="tag('<%= job.jid %>', $(this).val())"></input>
@@ -113,11 +116,26 @@
113
116
  <h3><small>History</small></h3>
114
117
  <div style="overflow-y:scroll; height: 200px">
115
118
  <% job.queue_history.reverse.each do |h| %>
116
- <pre><strong><%= h['q'] %></strong>
117
- Put: <%= strftime(h['put']) %><% if not h['popped'].nil? %>
118
- Pop: <%= strftime(h['popped']) %> by <%= h['worker'] %><% end %><% if not h['completed'].nil? %>
119
- Completed: <%= strftime(h['completed']) %><% end %><% if not h['failed'].nil? %>
120
- Failed: <%= strftime(h['failed']) %><% end %></pre>
119
+ <% if h['what'] == 'put' %>
120
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %>
121
+ in queue <strong><%= h['q'] %></strong></pre>
122
+ <% elsif h['what'] == 'popped' %>
123
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %>
124
+ by <strong><%= h['worker'] %></strong></pre>
125
+ <% elsif h['what'] == 'done' %>
126
+ <pre><strong>completed</strong> at <%= strftime(h['when']) %></pre>
127
+ <% elsif h['what'] == 'failed' %>
128
+ <% if h['worker'] %>
129
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %>
130
+ by <strong><%= h['worker'] %></strong>
131
+ in group <strong><%= h['group'] %></strong></pre>
132
+ <% else %>
133
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %>
134
+ in group <strong><%= h['group'] %></strong></pre>
135
+ <% end %>
136
+ <% else %>
137
+ <pre><strong><%= h['what'] %></strong> at <%= strftime(h['when']) %></pre>
138
+ <% end %>
121
139
  <% end %>
122
140
  </div>
123
141
  </div>
@@ -144,13 +162,13 @@
144
162
  <div class="row">
145
163
  <div class="span6">
146
164
  <h2 style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
147
- <a href="<%= u "/jobs/#{job.jid}" %>"><%= job.jid[0..8] %>...</a> | <%= job.klass_name %>
165
+ <a href="<%= u "/jobs/#{job.jid}" %>" title="<%= job.jid %>"><%= job.jid[0..8] %>...</a> | <span title="<%= job.klass_name %>"><%= job.klass_name %></span>
148
166
  </h2>
149
167
  </div>
150
168
  <div class="span3">
151
169
  <h2 style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
152
170
  <strong>
153
- | recurring / <a href="<%= u "/queues/#{job.queue_name}" %>"><%= job.queue_name %></a>
171
+ | recurring / <a href="<%= u "/queues/#{CGI::escape(job.queue_name)}" %>" title="<%= job.queue_name %>"><%= job.queue_name %></a>
154
172
  </strong>
155
173
  </h2>
156
174
  </div>
@@ -183,7 +201,7 @@
183
201
  </div>
184
202
  </div>
185
203
  </div>
186
-
204
+
187
205
  <div class="row">
188
206
  <div class="span12 tags" style="margin-bottom: 3px;">
189
207
  <% job.tags.each do |tag| %>
@@ -194,7 +212,7 @@
194
212
  </button>
195
213
  </div>
196
214
  <% end %>
197
-
215
+
198
216
  <!-- One for adding new tags -->
199
217
  <div class="btn-group" style="float:left">
200
218
  <input class="span1 add-tag" type="text" placeholder="Add Tag" onchange="tag('<%= job.jid %>', $(this).val())"></input>
@@ -0,0 +1,11 @@
1
+ <% if jobs.empty? %>
2
+ <div class="page-header">
3
+ <h1>No Completed Jobs<small>(yet)</small></h1>
4
+ </div>
5
+ <% else %>
6
+ <div class="page-header">
7
+ <h1>Completed Jobs <small>You must be doing something right!</small></h1>
8
+ </div>
9
+ <% end %>
10
+
11
+ <%= erb :_job_list, :locals => { :jobs => jobs, :queues => queues } %>
@@ -12,13 +12,13 @@
12
12
  <link href="<%= u '/css/docs.css' %>" rel="stylesheet">
13
13
  <link href="<%= u '/css/jquery.noty.css' %>" rel="stylesheet">
14
14
  <link href="<%= u '/css/noty_theme_twitter.css' %>" rel="stylesheet">
15
- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
15
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
16
16
 
17
17
  <style type="text/css">
18
18
  body {
19
19
  padding-top: 60px;
20
20
  }
21
-
21
+
22
22
  .btn-group span {
23
23
  /* This is ugly. Anyone want to change it? */
24
24
  border-color: #e6e6e6 #e6e6e6 #bfbfbf;
@@ -30,7 +30,7 @@
30
30
  line-height: 18px;
31
31
  padding: 4px 10px 4px;
32
32
  }
33
-
33
+
34
34
  .btn-group input, .btn-group span {
35
35
  position: relative;
36
36
  float: left;
@@ -40,7 +40,7 @@
40
40
  border-radius: 0;
41
41
  margin-bottom: 0px;
42
42
  }
43
-
43
+
44
44
  .btn-group input:first-child, .btn-group span:first-child {
45
45
  margin-left: 0;
46
46
  -webkit-border-top-left-radius: 4px;
@@ -50,6 +50,15 @@
50
50
  -moz-border-radius-bottomleft: 4px;
51
51
  border-bottom-left-radius: 4px;
52
52
  }
53
+
54
+ .large-text {
55
+ font-size:18px;
56
+ }
57
+
58
+ .queue-column {
59
+ min-width:300px;
60
+ }
61
+
53
62
  </style>
54
63
 
55
64
  <script type="text/javascript">
@@ -84,7 +93,7 @@
84
93
  url: '<%= u "/move" %>',
85
94
  data: {id:jid, queue:queue},
86
95
  success: function() { flash('Moved ' + jid + ' to ' + queue, 'success', 1500); cb(jid, queue); },
87
- erorr: function() { flash('Failed to move ' + jid + ' to ' + queue); }
96
+ error: function() { flash('Failed to move ' + jid + ' to ' + queue); }
88
97
  });
89
98
  }
90
99
 
@@ -94,7 +103,7 @@
94
103
  url: '<%= u "/retry" %>',
95
104
  data: {id:jid},
96
105
  success: function() { flash('Retrying ' + jid, 'success', 1500); if (cb) { cb(jid, 'retry'); } },
97
- erorr: function() { flash('Failed to retry ' + jid); }
106
+ error: function() { flash('Failed to retry ' + jid); }
98
107
  });
99
108
  }
100
109
 
@@ -198,13 +207,13 @@
198
207
  action();
199
208
  });
200
209
  }
201
-
210
+
202
211
  /* Helper function for adding a tag to a job */
203
212
  var tag = function(jid, tag) {
204
213
  var data = {};
205
214
  data[jid] = [tag];
206
215
  // The button group of the 'add tag' bit
207
- var group =
216
+ var group =
208
217
  _ajax({
209
218
  url: '<%= u "/tag" %>',
210
219
  data: data,
@@ -222,13 +231,13 @@
222
231
  }
223
232
  });
224
233
  }
225
-
234
+
226
235
  /* Helper function for untagging a job */
227
236
  var untag = function(jid, tag) {
228
237
  var data = {};
229
238
  data[jid] = [tag];
230
239
  // The button group of the 'add tag' bit
231
- var group =
240
+ var group =
232
241
  _ajax({
233
242
  url: '<%= u "/untag" %>',
234
243
  data: data,
@@ -241,7 +250,7 @@
241
250
  }
242
251
  });
243
252
  }
244
-
253
+
245
254
  /* Helper function for changing a job's priority */
246
255
  var priority = function(jid, priority) {
247
256
  var p = parseInt(priority);
@@ -272,6 +281,56 @@
272
281
  }
273
282
  }
274
283
 
284
+ var pause = function(queue) {
285
+ _ajax({
286
+ url: '<%= u "/pause" %>',
287
+ data: {
288
+ 'queue': queue
289
+ }, success: function(data) {
290
+ var button = $('#' + queue + '-pause');
291
+ button.attr('title', 'Unpause').attr(
292
+ 'data-original-title', 'Unpause');
293
+ button.addClass('btn-success').removeClass('btn-warning');
294
+ button.children().addClass('icon-play').removeClass('icon-pause');
295
+ button.attr('onclick', 'unpause("' + queue + '")');
296
+ }, error: function() {
297
+ flash('Couldn\'t pause queue ' + queue);
298
+ }
299
+ })
300
+ }
301
+
302
+ var unpause = function(queue) {
303
+ _ajax({
304
+ url: '<%= u "/unpause" %>',
305
+ data: {
306
+ 'queue': queue
307
+ }, success: function(data) {
308
+ var button = $('#' + queue + '-pause');
309
+ button.attr('title', 'Pause').attr(
310
+ 'data-original-title', 'Pause');
311
+ button.addClass('btn-warning').removeClass('btn-success');
312
+ button.children().addClass('icon-pause').removeClass('icon-play');
313
+ button.attr('onclick', 'pause("' + queue + '")');
314
+ }, error: function() {
315
+ flash('Couldn\'t unpause queue ' + queue);
316
+ }
317
+ })
318
+ }
319
+
320
+ var timeout = function(jid) {
321
+ _ajax({
322
+ url: '<%= u "/timeout" %>',
323
+ data: {
324
+ 'jid': jid
325
+ }, success: function(data) {
326
+ flash('Job timed out', 'success');
327
+ }, error: function(data) {
328
+ flash('Failed to time out job: ' + data);
329
+ console.log(data);
330
+ }
331
+ })
332
+ }
333
+
275
334
  $(document).ready(function() {
276
335
  $('button').tooltip({delay:200});
277
336
  });
@@ -7,23 +7,49 @@
7
7
  <h1>Queues <small>And their job counts</small></h1>
8
8
  </div>
9
9
 
10
- <% queues.each do |queue| %>
11
- <div class="row">
12
- <div class="span4">
13
- <h3><a href="<%= u "/queues/#{queue['name']}" %>"><%= queue['name'] %></a></h3>
14
- </div>
15
- <div class="span8">
16
- <h3> |
17
- <%= queue['running'] %> /
18
- <%= queue['waiting'] %> /
19
- <%= queue['scheduled'] %> /
20
- <%= queue['stalled'] %> /
21
- <%= queue['depends'] %> /
22
- <%= queue['recurring'] %> <small>(running / waiting / scheduled / stalled / depends / recurring)</small>
23
- </h3>
24
- </div>
25
- </div>
10
+ <table class="table">
11
+ <thead>
12
+ <tr>
13
+ <th></th>
14
+ <th>running</th>
15
+ <th>waiting</th>
16
+ <th>scheduled</th>
17
+ <th>stalled</th>
18
+ <th>depends</th>
19
+ <th>recurring</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <% queues.each do |queue| %>
24
+ <tr class="queue-row">
25
+ <td class="queue-column large-text">
26
+ <% if queue['paused'] %>
27
+ <button
28
+ id="<%= queue['name'] %>-pause"
29
+ title="Unpause"
30
+ class="btn btn-success"
31
+ onclick="unpause('<%= queue['name'] %>')"><i class="icon-play"></i>
32
+ </button>
33
+ <% else %>
34
+ <button
35
+ id="<%= queue['name'] %>-pause"
36
+ title="Pause"
37
+ class="btn btn-warning"
38
+ onclick="pause('<%= queue['name'] %>')"><i class="icon-pause"></i>
39
+ </button>
40
+ <% end %>
41
+ <a href="<%= u "/queues/#{CGI::escape(queue['name'])}" %>"><%= queue['name'] %></a>
42
+ </td>
43
+ <td><%= queue['running'] %></td>
44
+ <td><%= queue['waiting'] %></td>
45
+ <td><%= queue['scheduled'] %></td>
46
+ <td><%= queue['stalled'] %></td>
47
+ <td><%= queue['depends'] %></td>
48
+ <td><%= queue['recurring'] %></td>
49
+ </tr>
26
50
  <% end %>
51
+ </tbody>
52
+ </table>
27
53
  <% end %>
28
54
 
29
55
  <% if failed.empty? %>
@@ -34,22 +60,23 @@
34
60
  <div class="page-header">
35
61
  <h1>Failed Jobs <small>D'oh!</small></h1>
36
62
  </div>
37
- <div class="l-sidebyside failed-job">
38
- <div class="min-col-size">
39
- <% failed.sort_by { |t, count| -count }.each do |t, count| %>
40
- <div class="row">
41
- <h3><a href="<%= u "/failed/#{t}" %>"><%= t %></a></h3>
42
- </div>
43
- <% end %>
44
- </div>
45
- <div>
46
- <% failed.sort_by { |t, count| -count }.each do |t, count| %>
47
- <div class="row">
48
- <h3>| <%= count %> <small>Jobs</small></h3>
49
- </div>
50
- <% end %>
51
- </div>
52
- </div>
63
+
64
+ <table class="table">
65
+ <thead>
66
+ <tr>
67
+ <th>failure</th>
68
+ <th>count</th>
69
+ </tr>
70
+ </thead>
71
+ <tbody>
72
+ <% failed.sort_by { |t, count| -count }.each do |t, count| %>
73
+ <tr class="failed-row">
74
+ <td class="large-text"><a href="<%= u "/failed/#{t}" %>"><%= t %></a></td>
75
+ <td><%= count %></td>
76
+ </tr>
77
+ <% end %>
78
+ </tbody>
79
+ </table>
53
80
  <% end %>
54
81
 
55
82
  <% if tracked['jobs'].empty? %>
@@ -61,16 +88,22 @@
61
88
  <h1>Tracked Jobs <small>These <i>are</i> the droids you're looking for</small></h1>
62
89
  </div>
63
90
  <% counts = Hash.new; tracked['jobs'].each { |job| counts[job.state] ||= 0; counts[job.state] += 1 } %>
64
- <% counts.sort_by { |state, count| - count }.each do |state, count| %>
65
- <div class="row">
66
- <div class="span4">
67
- <h3><a href="<%= u "/track##{state}" %>"><%= state %></a></h3>
68
- </div>
69
- <div class="span8">
70
- <h3>| <%= count %> <small>Jobs</small></h3>
71
- </div>
72
- </div>
73
- <% end %>
91
+ <table class="table">
92
+ <thead>
93
+ <tr>
94
+ <th>state</th>
95
+ <th>count</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody>
99
+ <% counts.sort_by { |state, count| - count }.each do |state, count| %>
100
+ <tr class="tracked-row">
101
+ <td class="large-text"><a href="<%= u "/track##{state}" %>"><%= state %></a></td>
102
+ <td><%= count %></td>
103
+ </tr>
104
+ <% end %>
105
+ </tbody>
106
+ </table>
74
107
  <% end %>
75
108
 
76
109
  <% if workers.empty? %>
@@ -81,15 +114,22 @@
81
114
  <div class="page-header">
82
115
  <h1>Current Workers <small>And their job counts</small></h1>
83
116
  </div>
84
-
85
- <% workers.each do |worker| %>
86
- <div class="row">
87
- <div class="span4">
88
- <h3><a href="<%= u "/workers/#{worker['name']}" %>"><%= worker['name'] %></a></h3>
89
- </div>
90
- <div class="span8">
91
- <h3>| <%= worker['jobs'] %> / <%= worker['stalled'] %> <small>Running / Stalled</small></h3>
92
- </div>
93
- </div>
94
- <% end %>
117
+ <table class="table">
118
+ <thead>
119
+ <tr>
120
+ <th></th>
121
+ <th>running</th>
122
+ <th>stalled</th>
123
+ </tr>
124
+ </thead>
125
+ <tbody>
126
+ <% workers.each do |worker| %>
127
+ <tr class='worker-row'>
128
+ <td class="large-text"><a href="<%= u "/workers/#{worker['name']}" %>"><%= worker['name'] %></a></td>
129
+ <td><%= worker['jobs'] %></td>
130
+ <td><%= worker['stalled'] %></td>
131
+ </tr>
132
+ <% end %>
133
+ </tbody>
134
+ </table>
95
135
  <% end %>