canvas-jobs 0.9.13 → 0.9.14
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.
- checksums.yaml +4 -4
- data/lib/delayed/backend/base.rb +1 -1
- data/lib/delayed/pool.rb +23 -19
- data/lib/delayed/server.rb +120 -0
- data/lib/delayed/server/helpers.rb +28 -0
- data/lib/delayed/server/public/css/app.css +12 -0
- data/lib/delayed/server/public/js/app.js +132 -0
- data/lib/delayed/server/views/index.erb +90 -0
- data/lib/delayed/server/views/layout.erb +47 -0
- data/lib/delayed/version.rb +1 -1
- data/spec/delayed/pool_spec.rb +11 -0
- data/spec/delayed/server/helpers_spec.rb +1 -0
- data/spec/delayed/server_spec.rb +63 -0
- data/spec/gemfiles/32.gemfile.lock +50 -50
- data/spec/gemfiles/40.gemfile.lock +128 -0
- data/spec/gemfiles/41.gemfile.lock +134 -0
- data/spec/gemfiles/42.gemfile.lock +153 -0
- data/spec/spec_helper.rb +7 -4
- metadata +64 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd2ac418daa325d715cb4388d48f7f08c44fdea2
|
4
|
+
data.tar.gz: 4cc439b8bea527dd524c45e5e82186af2d5352c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6daa755b75bbc555e0d87e6e7a6905462e3bf71ed64a238e99cec05cf583febd53f3d85429afafb2d9247f817302806179ab5444da2f0529571ff9991b2c64fa
|
7
|
+
data.tar.gz: 55148fbafe336859c77fce7df57da578653acd15b18f0141c8a8f13817ca23ff9685221b0d0cff758469bc62ff055aef010e298504321153c62830aa84e752eb
|
data/lib/delayed/backend/base.rb
CHANGED
data/lib/delayed/pool.rb
CHANGED
@@ -22,25 +22,7 @@ class Pool
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def run
|
25
|
-
|
26
|
-
opts.banner = "Usage #{$0} <command> <options>"
|
27
|
-
opts.separator %{\nWhere <command> is one of:
|
28
|
-
start start the jobs daemon
|
29
|
-
stop stop the jobs daemon
|
30
|
-
run start and run in the foreground
|
31
|
-
restart stop and then start the jobs daemon
|
32
|
-
status show daemon status
|
33
|
-
}
|
34
|
-
|
35
|
-
opts.separator "\n<options>"
|
36
|
-
opts.on("-c", "--config", "Use alternate config file (default #{options[:config_file]})") { |c| options[:config_file] = c }
|
37
|
-
opts.on("-p", "--pid", "Use alternate folder for PID files (default #{options[:pid_folder]})") { |p| options[:pid_folder] = p }
|
38
|
-
opts.on("--no-tail", "Don't tail the logs (only affects non-daemon mode)") { options[:tail_logs] = false }
|
39
|
-
opts.on("--with-prejudice", "When stopping, interrupt jobs in progress, instead of letting them drain") { options[:kill] ||= true }
|
40
|
-
opts.on("--with-extreme-prejudice", "When stopping, immediately kill jobs in progress, instead of letting them drain") { options[:kill] = 9 }
|
41
|
-
opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
|
42
|
-
end
|
43
|
-
op.parse!(@args)
|
25
|
+
parse_cli_options!
|
44
26
|
|
45
27
|
read_config(options[:config_file])
|
46
28
|
|
@@ -80,6 +62,28 @@ class Pool
|
|
80
62
|
end
|
81
63
|
end
|
82
64
|
|
65
|
+
def parse_cli_options!
|
66
|
+
op = OptionParser.new do |opts|
|
67
|
+
opts.banner = "Usage #{$0} <command> <options>"
|
68
|
+
opts.separator %{\nWhere <command> is one of:
|
69
|
+
start start the jobs daemon
|
70
|
+
stop stop the jobs daemon
|
71
|
+
run start and run in the foreground
|
72
|
+
restart stop and then start the jobs daemon
|
73
|
+
status show daemon status
|
74
|
+
}
|
75
|
+
|
76
|
+
opts.separator "\n<options>"
|
77
|
+
opts.on("-c", "--config [CONFIG_PATH]", "Use alternate config file (default #{options[:config_file]})") { |c| options[:config_file] = c }
|
78
|
+
opts.on("-p", "--pid", "Use alternate folder for PID files (default #{options[:pid_folder]})") { |p| options[:pid_folder] = p }
|
79
|
+
opts.on("--no-tail", "Don't tail the logs (only affects non-daemon mode)") { options[:tail_logs] = false }
|
80
|
+
opts.on("--with-prejudice", "When stopping, interrupt jobs in progress, instead of letting them drain") { options[:kill] ||= true }
|
81
|
+
opts.on("--with-extreme-prejudice", "When stopping, immediately kill jobs in progress, instead of letting them drain") { options[:kill] = 9 }
|
82
|
+
opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
|
83
|
+
end
|
84
|
+
op.parse!(@args)
|
85
|
+
end
|
86
|
+
|
83
87
|
protected
|
84
88
|
|
85
89
|
def procname
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'sinatra/json'
|
3
|
+
require 'json'
|
4
|
+
require 'delayed_job'
|
5
|
+
|
6
|
+
module Delayed
|
7
|
+
class Server < Sinatra::Base
|
8
|
+
APP_DIR = File.dirname(File.expand_path(__FILE__))
|
9
|
+
set :views, File.join(APP_DIR, 'server', 'views')
|
10
|
+
set :public_folder, File.join(APP_DIR, 'server', 'public')
|
11
|
+
|
12
|
+
def initialize(*args, &block)
|
13
|
+
super()
|
14
|
+
# Rails will take care of establishing the DB connection for us if there is
|
15
|
+
# an application present
|
16
|
+
if using_active_record? && !ActiveRecord::Base.connected?
|
17
|
+
ActiveRecord::Base.establish_connection(ENV['DATABASE_URL'])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def using_active_record?
|
22
|
+
Delayed::Job == Delayed::Backend::ActiveRecord::Job
|
23
|
+
end
|
24
|
+
|
25
|
+
# Ensure we're connected to the DB before processing the request
|
26
|
+
before do
|
27
|
+
if ActiveRecord::Base.respond_to?(:verify_active_connections!) && using_active_record?
|
28
|
+
ActiveRecord::Base.verify_active_connections!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Release any used connections back to the pool
|
33
|
+
after do
|
34
|
+
ActiveRecord::Base.clear_active_connections! if using_active_record?
|
35
|
+
end
|
36
|
+
|
37
|
+
configure :development do
|
38
|
+
require 'sinatra/reloader'
|
39
|
+
register Sinatra::Reloader
|
40
|
+
end
|
41
|
+
|
42
|
+
helpers do
|
43
|
+
# this can't get required until the class has been opened for the first time
|
44
|
+
require 'delayed/server/helpers'
|
45
|
+
include Delayed::Server::Helpers
|
46
|
+
end
|
47
|
+
|
48
|
+
get '/' do
|
49
|
+
erb :index
|
50
|
+
end
|
51
|
+
|
52
|
+
get '/running' do
|
53
|
+
content_type :json
|
54
|
+
json({
|
55
|
+
draw: params['draw'].to_i,
|
56
|
+
recordsTotal: Delayed::Job.running.count,
|
57
|
+
recordsFiltered: Delayed::Job.running.count,
|
58
|
+
data: Delayed::Job.running_jobs.map{ |j|
|
59
|
+
j.as_json(include_root: false, except: [:handler, :last_error])
|
60
|
+
},
|
61
|
+
})
|
62
|
+
end
|
63
|
+
|
64
|
+
get '/tags' do
|
65
|
+
content_type :json
|
66
|
+
json({
|
67
|
+
draw: params['draw'].to_i,
|
68
|
+
data: Delayed::Job.tag_counts('current', 10)
|
69
|
+
})
|
70
|
+
end
|
71
|
+
|
72
|
+
DEFAULT_PAGE_SIZE = 10
|
73
|
+
MAX_PAGE_SIZE = 100
|
74
|
+
get '/jobs' do
|
75
|
+
content_type :json
|
76
|
+
flavor = params['flavor'] || 'current'
|
77
|
+
page_size = extract_page_size
|
78
|
+
offset = Integer(params['start'] || 0)
|
79
|
+
case flavor
|
80
|
+
when 'id'
|
81
|
+
jobs = Delayed::Job.where(id: params['search_term'])
|
82
|
+
total_records = 1
|
83
|
+
when 'future', 'current', 'failed'
|
84
|
+
jobs = Delayed::Job.list_jobs(flavor, page_size, offset)
|
85
|
+
total_records = Delayed::Job.jobs_count(flavor)
|
86
|
+
else
|
87
|
+
query = params['search_term']
|
88
|
+
if query.present?
|
89
|
+
jobs = Delayed::Job.list_jobs(flavor, page_size, offset, query)
|
90
|
+
else
|
91
|
+
jobs = []
|
92
|
+
end
|
93
|
+
total_records = Delayed::Job.jobs_count(flavor, query)
|
94
|
+
end
|
95
|
+
json({
|
96
|
+
draw: params['draw'].to_i,
|
97
|
+
recordsTotal: total_records,
|
98
|
+
recordsFiltered: jobs.size,
|
99
|
+
data: build_jobs_json(jobs),
|
100
|
+
})
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def extract_page_size
|
106
|
+
page_size = Integer(params['length'] || DEFAULT_PAGE_SIZE)
|
107
|
+
# if dataTables wants all of the records it will send us -1 but we don't
|
108
|
+
# want the potential to kill our servers with this request so we'll limit it
|
109
|
+
page_size = DEFAULT_PAGE_SIZE if page_size == -1
|
110
|
+
[page_size, MAX_PAGE_SIZE].min
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def build_jobs_json(jobs)
|
115
|
+
json = jobs.map{ |j|
|
116
|
+
j.as_json(root: false, except: [:handler, :last_error])
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Delayed
|
2
|
+
class Server
|
3
|
+
module Helpers
|
4
|
+
def h(text)
|
5
|
+
Rack::Utils.escape_html(text)
|
6
|
+
end
|
7
|
+
|
8
|
+
def url_path(*path_parts)
|
9
|
+
[path_prefix, path_parts].join('/').squeeze('/')
|
10
|
+
end
|
11
|
+
|
12
|
+
def path_prefix
|
13
|
+
request.env['SCRIPT_NAME']
|
14
|
+
end
|
15
|
+
|
16
|
+
def render_javascript_env
|
17
|
+
{
|
18
|
+
Routes: {
|
19
|
+
root: path_prefix,
|
20
|
+
running: url_path('running'),
|
21
|
+
tags: url_path('tags'),
|
22
|
+
jobs: url_path('jobs'),
|
23
|
+
}
|
24
|
+
}.to_json
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
(function () {
|
2
|
+
var Delayed = {};
|
3
|
+
|
4
|
+
Delayed.Render = {};
|
5
|
+
Delayed.Render.attempts = function (previousAttempts, callType, jobData) {
|
6
|
+
var thisAttempt = parseInt(previousAttempts, 10) + 1;
|
7
|
+
switch(callType) {
|
8
|
+
case "display":
|
9
|
+
return thisAttempt + "/" + jobData.max_attempts;
|
10
|
+
default:
|
11
|
+
return thisAttempt;
|
12
|
+
}
|
13
|
+
};
|
14
|
+
|
15
|
+
Delayed.Render.runTime = function (lockedAt, callType) {
|
16
|
+
var elapsed, days, hours, minutes, seconds, remaining, formattedTime;
|
17
|
+
elapsed = Math.round((new Date().getTime() - Date.parse(lockedAt)) / 1000);
|
18
|
+
switch(callType) {
|
19
|
+
case "display": {
|
20
|
+
days = Math.floor(elapsed / 86400);
|
21
|
+
remaining = elapsed - (days * 86400);
|
22
|
+
hours = Math.floor(remaining / 3600);
|
23
|
+
remaining = remaining - (hours * 3600);
|
24
|
+
minutes = Math.floor(remaining / 60);
|
25
|
+
seconds = remaining - (minutes * 60);
|
26
|
+
formattedTime = ""
|
27
|
+
if(days > 0) {
|
28
|
+
formattedTime = days + "d "
|
29
|
+
}
|
30
|
+
formattedTime = formattedTime + ("0" + hours).slice(-2) + ":" + ("0" + minutes).slice(-2) + ":" + ("0" + seconds).slice(-2);
|
31
|
+
return formattedTime;
|
32
|
+
}
|
33
|
+
default:
|
34
|
+
return elapsed;
|
35
|
+
}
|
36
|
+
};
|
37
|
+
|
38
|
+
$(document).ready(function () {
|
39
|
+
var runningTable, runningInterval, tagsTable, tagsInterval, jobsTable, jobsInterval
|
40
|
+
runningTable = $('#running').DataTable({
|
41
|
+
"autoWidth": false,
|
42
|
+
"bSort": false,
|
43
|
+
"paging": false,
|
44
|
+
"processing": true,
|
45
|
+
"searching": false,
|
46
|
+
"scrollY": "200px",
|
47
|
+
"serverSide": true,
|
48
|
+
"order": [[5, "desc"]],
|
49
|
+
"ajax": ENV.Routes.running,
|
50
|
+
"columns": [
|
51
|
+
{"data": "id"},
|
52
|
+
{"data": "locked_by", "className": "worker"},
|
53
|
+
{"data": "tag", "className": "tag"},
|
54
|
+
{"data": "attempts", "render": Delayed.Render.attempts, "className": "attempts"},
|
55
|
+
{"data": "strand", "className": "strand"},
|
56
|
+
{"data": "locked_at", "render": Delayed.Render.runTime}
|
57
|
+
]
|
58
|
+
});
|
59
|
+
runningInterval = setInterval(function () { runningTable.ajax.reload(); }, 2000);
|
60
|
+
|
61
|
+
tagsTable = $("#tags").DataTable({
|
62
|
+
"autoWidth": false,
|
63
|
+
"bSort": false,
|
64
|
+
"paging": false,
|
65
|
+
"processing": true,
|
66
|
+
"searching": false,
|
67
|
+
"scrollY": "200px",
|
68
|
+
"order": [[1, "desc"]],
|
69
|
+
"ajax": ENV.Routes.tags,
|
70
|
+
"columns": [
|
71
|
+
{"data": "tag", "className": "tag"},
|
72
|
+
{"data": "count"}
|
73
|
+
]
|
74
|
+
});
|
75
|
+
tagsInterval = setInterval(function () { tagsTable.ajax.reload(); }, 10000);
|
76
|
+
|
77
|
+
jobsTable = $("#jobs").DataTable({
|
78
|
+
"autoWidth": false,
|
79
|
+
"bSort": false,
|
80
|
+
"paging": true,
|
81
|
+
"processing": true,
|
82
|
+
"searching": false,
|
83
|
+
"scrollY": "200px",
|
84
|
+
"serverSide": true,
|
85
|
+
"order": [[1, "desc"]],
|
86
|
+
"ajax": {
|
87
|
+
"url": ENV.Routes.jobs,
|
88
|
+
"data": function (data) {
|
89
|
+
data.flavor = $("select#current_jobs_flavor").val();
|
90
|
+
data.search_term = $("input#jobs_search_term").val();
|
91
|
+
}
|
92
|
+
},
|
93
|
+
"columns": [
|
94
|
+
{"data": "id"},
|
95
|
+
{"data": "tag", "className": "tag"},
|
96
|
+
{"data": "attempts", "render": Delayed.Render.attempts},
|
97
|
+
{"data": "priority"},
|
98
|
+
{"data": "strand"},
|
99
|
+
{"data": "run_at"}
|
100
|
+
]
|
101
|
+
});
|
102
|
+
|
103
|
+
$('body').on('click', '.refresh_jobs_link', function(event) {
|
104
|
+
event.preventDefault();
|
105
|
+
jobsTable.ajax.reload();
|
106
|
+
return true
|
107
|
+
});
|
108
|
+
|
109
|
+
$('body').on('change', 'select#current_jobs_flavor', function (event) {
|
110
|
+
event.preventDefault();
|
111
|
+
$selectBox = $(this);
|
112
|
+
$selectedOption = $($selectBox.children(':selected'));
|
113
|
+
$searchDiv = $('div#job_search');
|
114
|
+
|
115
|
+
if ($selectedOption.data('requires-search')) {
|
116
|
+
$searchDiv.show();
|
117
|
+
} else {
|
118
|
+
$searchDiv.hide();
|
119
|
+
}
|
120
|
+
|
121
|
+
jobsTable.ajax.reload();
|
122
|
+
return true
|
123
|
+
});
|
124
|
+
|
125
|
+
$('body').on('keyup', '#jobs_search_term', function (event) {
|
126
|
+
if (event.keyCode == 13) {
|
127
|
+
jobsTable.ajax.reload();
|
128
|
+
}
|
129
|
+
return true
|
130
|
+
});
|
131
|
+
});
|
132
|
+
})();
|
@@ -0,0 +1,90 @@
|
|
1
|
+
<div class="row">
|
2
|
+
<div class="col-md-8">
|
3
|
+
<h2>Running</h2>
|
4
|
+
<table id="running" class="table table-striped table-bordered">
|
5
|
+
<thead>
|
6
|
+
<tr>
|
7
|
+
<th>ID</th>
|
8
|
+
<th>Worker</th>
|
9
|
+
<th>Tag</th>
|
10
|
+
<th>Attempt</th>
|
11
|
+
<th>Strand</th>
|
12
|
+
<th>Runtime</th>
|
13
|
+
</tr>
|
14
|
+
</thead>
|
15
|
+
<tbody>
|
16
|
+
</tbody>
|
17
|
+
</table>
|
18
|
+
</div>
|
19
|
+
<div class="col-md-4">
|
20
|
+
<h2>Tags</h2>
|
21
|
+
<table id="tags" class="table table-striped table-bordered">
|
22
|
+
<thead>
|
23
|
+
<tr>
|
24
|
+
<th>Tag</th>
|
25
|
+
<th>Count</th>
|
26
|
+
</tr>
|
27
|
+
</thead>
|
28
|
+
<tbody>
|
29
|
+
</tbody>
|
30
|
+
</table>
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<div class="row">
|
35
|
+
<div class="col-md-12">
|
36
|
+
<h2>Jobs List</h2>
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<div class="row">
|
41
|
+
<div class="form-group">
|
42
|
+
<div class="col-md-2">
|
43
|
+
<select id="current_jobs_flavor" class="form-control">
|
44
|
+
<option value="current">Current</option>
|
45
|
+
<option value="future">Future</option>
|
46
|
+
<option value="failed">Failed</option>
|
47
|
+
<option value="id" data-requires-search="true">ID</option>
|
48
|
+
<option value="strand" data-requires-search="true">Strand</option>
|
49
|
+
<option value="tag" data-requires-search="true">Tag</option>
|
50
|
+
</select>
|
51
|
+
</div>
|
52
|
+
<div class="col-md-3">
|
53
|
+
<!-- this inline style makes me sad but doing it in the stylesheet seems to break bootstrap -->
|
54
|
+
<div class="input-group" id="job_search" style="display: none;">
|
55
|
+
<input type="text" class="form-control" id="jobs_search_term" placeholder="">
|
56
|
+
<span class="input-group-btn">
|
57
|
+
<a href="" class="btn btn-default refresh_jobs_link" aria-label="Run Search">
|
58
|
+
Filter <span class="glyphicon glyphicon-filter"></span>
|
59
|
+
</a>
|
60
|
+
</span>
|
61
|
+
</div>
|
62
|
+
</div>
|
63
|
+
<div class="col-md-6">
|
64
|
+
</div>
|
65
|
+
<div class="col-md-1">
|
66
|
+
<a href="" class="btn btn-default pull-right refresh_jobs_link" aria-label="Refresh Jobs Table">
|
67
|
+
<span class="glyphicon glyphicon-refresh"></span> Refresh
|
68
|
+
</a>
|
69
|
+
</div>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
|
73
|
+
<div class="row job-list-row">
|
74
|
+
<div class="col-md-12">
|
75
|
+
<table id="jobs" class="table table-striped table-bordered">
|
76
|
+
<thead>
|
77
|
+
<tr>
|
78
|
+
<th>ID</th>
|
79
|
+
<th>Tag</th>
|
80
|
+
<th>Attempt</th>
|
81
|
+
<th>Priority</th>
|
82
|
+
<th>Strand</th>
|
83
|
+
<th>Run At</th>
|
84
|
+
</tr>
|
85
|
+
</thead>
|
86
|
+
<tbody>
|
87
|
+
</tbody>
|
88
|
+
</table>
|
89
|
+
</div>
|
90
|
+
</div>
|