canvas_sync 0.17.16 → 0.17.20
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +21 -0
- data/lib/canvas_sync/batch_processor.rb +1 -1
- data/lib/canvas_sync/concerns/api_syncable.rb +5 -5
- data/lib/canvas_sync/importers/bulk_importer.rb +2 -2
- data/lib/canvas_sync/job_batches/batch.rb +9 -0
- data/lib/canvas_sync/job_batches/chain_builder.rb +8 -0
- data/lib/canvas_sync/job_batches/hier_batch_ids.lua +25 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/css/styles.less +178 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/batch_tree.js +106 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/util.js +2 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_batch_tree.erb +6 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_common.erb +13 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/batch.erb +15 -88
- data/lib/canvas_sync/job_batches/sidekiq/web.rb +93 -0
- data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +1 -1
- data/lib/canvas_sync/jobs/report_checker.rb +37 -4
- data/lib/canvas_sync/jobs/report_starter.rb +2 -2
- data/lib/canvas_sync/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz: '
|
3
|
+
metadata.gz: 761f46113fd5ade5684101672ca0fda352e0fbec602bb8fe0fdbcaf584e728ba
|
4
|
+
data.tar.gz: '0827e3a31d1e2bf7bf0afdece5a6a123f2221f2db9a4d239a4c543024641da57'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ae25ea9b6305902f220604a4d040737cca48aa6a7d89960357179ad852951bcb99a93693c3fd684ef3a12b3db89874023f48f5ac6d54dac35edb3fd7eefcc57
|
7
|
+
data.tar.gz: ed161a60791e7d326faabea6431d6352eee48ea2c23dd0ff224e902ac9e545c881f18a3d0d31bffa186e95d5d840f605e492f58ef7b68d9583da7afd011dde89
|
data/README.md
CHANGED
@@ -383,6 +383,27 @@ Available config options (if you add more, please update this!):
|
|
383
383
|
|
384
384
|
* `config.classes_to_only_log_errors_on` - use this if you are utilizing the `CanvasSync::JobLog` table, but want certain classes to only persist in the `job_logs` table if an error is encountered. This is useful if you've got a very frequently used job that's filling up your database, and only really care about tracking failures.
|
385
385
|
|
386
|
+
## Global Options
|
387
|
+
You can pass in global_options to a job chain. Global options are added to the batch_context and referenced by
|
388
|
+
various internal processes.
|
389
|
+
|
390
|
+
Pass global options into a job chain, using the options param nested in a :global key.
|
391
|
+
options: { global: {...} }
|
392
|
+
|
393
|
+
report_timeout (integer): Number of days until a Canvas report should timeout. Default is 1.
|
394
|
+
report_compilation_timeout (integer): Number of days until a Canvas report should timeout. Default is 1 hour.
|
395
|
+
You can likely pass a float to achieve sub-day timeouts, but not tested.
|
396
|
+
report_max_tries (integer): The number of times to attempt a report before giving up. A report is considered failed
|
397
|
+
if it has an 'error' status in Canvas or is deleted.
|
398
|
+
|
399
|
+
This is an example job chain with global options:
|
400
|
+
job_chain = CanvasSync.default_provisioning_report_chain(
|
401
|
+
MODELS_TO_SYNC,
|
402
|
+
term_scope: :active,
|
403
|
+
full_sync_every: 'sunday',
|
404
|
+
options: { global: { report_timeout: 2 } }
|
405
|
+
)
|
406
|
+
|
386
407
|
## Handling Job errors
|
387
408
|
|
388
409
|
If you need custom handling for when a CanvasSync Job fails, you can add an `:on_failure` option to you Job Chain's `:global_options`.
|
@@ -2,7 +2,7 @@ module CanvasSync
|
|
2
2
|
# An array that "processes" after so many items are added.
|
3
3
|
#
|
4
4
|
# Example Usage:
|
5
|
-
# batches = BatchProcessor.new(of: 1000) do |batch|
|
5
|
+
# batches = CanvasSync::BatchProcessor.new(of: 1000) do |batch|
|
6
6
|
# # Process the batch somehow
|
7
7
|
# end
|
8
8
|
# enumerator_of_some_kind.each { |item| batches << item }
|
@@ -30,27 +30,27 @@ module CanvasSync::Concerns
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def bulk_sync_from_api_result(api_array, conflict_target: :canvas_id, import_args: {}, all_pages: true, batch_size: 1000)
|
33
|
-
columns = api_sync_options.keys
|
33
|
+
columns = api_sync_options[:field_map].keys
|
34
34
|
|
35
35
|
update_conditions = {
|
36
|
-
condition: Importers::BulkImporter.condition_sql(self, columns),
|
36
|
+
condition: CanvasSync::Importers::BulkImporter.condition_sql(self, columns),
|
37
37
|
columns: columns,
|
38
38
|
}
|
39
39
|
update_conditions[:conflict_target] = conflict_target if conflict_target.present?
|
40
40
|
options = { validate: false, on_duplicate_key_update: update_conditions }.merge(import_args)
|
41
41
|
|
42
42
|
if all_pages
|
43
|
-
batcher = BatchProcessor.new(of: batch_size) do |batch|
|
43
|
+
batcher = CanvasSync::BatchProcessor.new(of: batch_size) do |batch|
|
44
44
|
import(columns, batch, options)
|
45
45
|
end
|
46
46
|
api_array.all_pages_each do |api_item|
|
47
|
-
item = new.assign_from_api_params(
|
47
|
+
item = new.assign_from_api_params(api_item)
|
48
48
|
batcher << item
|
49
49
|
end
|
50
50
|
batcher.flush
|
51
51
|
else
|
52
52
|
items = api_array.map do |api_item|
|
53
|
-
new.assign_from_api_params(
|
53
|
+
new.assign_from_api_params(api_item)
|
54
54
|
end
|
55
55
|
import(columns, batch, options)
|
56
56
|
end
|
@@ -29,7 +29,7 @@ module CanvasSync
|
|
29
29
|
database_conflict_column_name = conflict_target ? mapping[conflict_target][:database_column_name] : nil
|
30
30
|
|
31
31
|
row_ids = {}
|
32
|
-
batcher = BatchProcessor.new(of: batch_size) do |batch|
|
32
|
+
batcher = CanvasSync::BatchProcessor.new(of: batch_size) do |batch|
|
33
33
|
row_ids = {}
|
34
34
|
perform_import(klass, database_column_names, batch, database_conflict_column_name, import_args)
|
35
35
|
end
|
@@ -96,7 +96,7 @@ module CanvasSync
|
|
96
96
|
# started_at = Time.now
|
97
97
|
# run_the_users_sync!
|
98
98
|
# changed = User.where("updated_at >= ?", started_at)
|
99
|
-
def self.condition_sql(klass, columns, report_start)
|
99
|
+
def self.condition_sql(klass, columns, report_start = nil)
|
100
100
|
columns_str = columns.map { |c| "#{klass.quoted_table_name}.#{c}" }.join(", ")
|
101
101
|
excluded_str = columns.map { |c| "EXCLUDED.#{c}" }.join(", ")
|
102
102
|
condition_sql = "(#{columns_str}) IS DISTINCT FROM (#{excluded_str})"
|
@@ -28,6 +28,7 @@ module CanvasSync
|
|
28
28
|
|
29
29
|
BID_EXPIRE_TTL = 2_592_000
|
30
30
|
SCHEDULE_CALLBACK = RedisScript.new(Pathname.new(__FILE__) + "../schedule_callback.lua")
|
31
|
+
BID_HIERARCHY = RedisScript.new(Pathname.new(__FILE__) + "../hier_batch_ids.lua")
|
31
32
|
|
32
33
|
attr_reader :bid
|
33
34
|
|
@@ -423,6 +424,14 @@ module CanvasSync
|
|
423
424
|
def push_callbacks(args, queue)
|
424
425
|
Batch::Callback::worker_class.enqueue_all(args, queue)
|
425
426
|
end
|
427
|
+
|
428
|
+
def bid_hierarchy(bid, depth: 4, per_depth: 5, slice: nil)
|
429
|
+
args = [bid, depth, per_depth]
|
430
|
+
args << slice if slice
|
431
|
+
redis do |r|
|
432
|
+
BID_HIERARCHY.call(r, [], args)
|
433
|
+
end
|
434
|
+
end
|
426
435
|
end
|
427
436
|
end
|
428
437
|
|
@@ -172,6 +172,14 @@ module CanvasSync
|
|
172
172
|
mapper[key] ||= []
|
173
173
|
end
|
174
174
|
|
175
|
+
# TODO: Add a Chain progress web View
|
176
|
+
# Augment Batch tree-view with Chain data
|
177
|
+
# > [DONE] Tree view w/o Chain will only show Parent/Current batches and Job Counts
|
178
|
+
# > If augmented with Chain data, the above will be annotated with Chain-related info and will be able to show Jobs defined in the Chain
|
179
|
+
# > Chain-jobs will be supplied chain_id and chain_step_id metadata
|
180
|
+
# > Using server-middleware, if a Chain-job (has chain_id and chain_step_id) creates a Batch, tag the Batch w/ the chain_id and chain_step_id
|
181
|
+
# > UI will map Batches to Chain-steps using the chain_step_id. UI will add entries for any Chain-steps that were not tied to a Batch
|
182
|
+
# > [DONE] Use a Lua script to find child batch IDs. Support max_depth, items_per_depth, top_depth_slice parameters
|
175
183
|
def enqueue_job(job_def)
|
176
184
|
job_class = job_def[:job].constantize
|
177
185
|
job_options = job_def[:parameters] || []
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
local function add_bids(root, depth)
|
3
|
+
local result_data = {}
|
4
|
+
|
5
|
+
if depth > 0 then
|
6
|
+
local sbids
|
7
|
+
if depth == tonumber(ARGV[2]) and ARGV[4] then
|
8
|
+
local min, max = ARGV[4]:match('(%d+):(%d+)')
|
9
|
+
sbids = redis.call('ZRANGE', 'BID-' .. root .. '-bids', min, max)
|
10
|
+
else
|
11
|
+
sbids = redis.call('ZRANGE', 'BID-' .. root .. '-bids', 0, tonumber(ARGV[3]) - 1)
|
12
|
+
end
|
13
|
+
|
14
|
+
local sub_data = {}
|
15
|
+
for _,v in ipairs(sbids) do
|
16
|
+
table.insert(sub_data, add_bids(v, depth - 1))
|
17
|
+
end
|
18
|
+
|
19
|
+
return { root, sub_data }
|
20
|
+
end
|
21
|
+
|
22
|
+
return { root, result_data}
|
23
|
+
end
|
24
|
+
|
25
|
+
return add_bids(ARGV[1], tonumber(ARGV[2]))
|
@@ -0,0 +1,178 @@
|
|
1
|
+
|
2
|
+
@color-green: #25c766;
|
3
|
+
@color-red: #c7254e;
|
4
|
+
@color-yellow: #c4c725;
|
5
|
+
|
6
|
+
.code-wrap.batch-context .args-extended {
|
7
|
+
white-space: pre;
|
8
|
+
|
9
|
+
.key {
|
10
|
+
white-space: pre-wrap;
|
11
|
+
margin-left: 2em;
|
12
|
+
text-indent: -2em;
|
13
|
+
display: inline-block;
|
14
|
+
}
|
15
|
+
|
16
|
+
.own {
|
17
|
+
color: @color-green;
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
|
22
|
+
.batch-tree {
|
23
|
+
.status-block {
|
24
|
+
.tree-stat {
|
25
|
+
margin: 0 4px;
|
26
|
+
|
27
|
+
&.pending {
|
28
|
+
color: @color-yellow;
|
29
|
+
}
|
30
|
+
&.failed {
|
31
|
+
color: @color-red;
|
32
|
+
}
|
33
|
+
&.success {
|
34
|
+
color: @color-green;
|
35
|
+
}
|
36
|
+
&.total {
|
37
|
+
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
.text-inactive {
|
43
|
+
color: darken(#fff, 35%);
|
44
|
+
font-size: 80%;
|
45
|
+
}
|
46
|
+
|
47
|
+
.tree-header {
|
48
|
+
position: relative;
|
49
|
+
|
50
|
+
.status-block {
|
51
|
+
position: absolute;
|
52
|
+
bottom: 0;
|
53
|
+
width: 100%;
|
54
|
+
|
55
|
+
margin-right: 8px;
|
56
|
+
font-size: 90%;
|
57
|
+
text-align: right;
|
58
|
+
|
59
|
+
.tree-stat {
|
60
|
+
font-style: italic;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
.tree-entry {
|
66
|
+
> .header {
|
67
|
+
display: flex;
|
68
|
+
align-items: center;
|
69
|
+
|
70
|
+
.header-inner {
|
71
|
+
padding: 4px 0;
|
72
|
+
border-bottom: 1px dashed white;
|
73
|
+
display: flex;
|
74
|
+
align-items: center;
|
75
|
+
flex: 1;
|
76
|
+
}
|
77
|
+
|
78
|
+
&:hover {
|
79
|
+
background-color: rgba(0,0,0,0.20);
|
80
|
+
border-radius: 3px;
|
81
|
+
}
|
82
|
+
|
83
|
+
.row-toggle {
|
84
|
+
width: 16px;
|
85
|
+
height: 16px;
|
86
|
+
text-align: center;
|
87
|
+
align-self: center;
|
88
|
+
border-radius: 50%;
|
89
|
+
border: 1px solid #999;
|
90
|
+
text-decoration: none;
|
91
|
+
margin: 0 4px;
|
92
|
+
font-size: 16px;
|
93
|
+
line-height: 15px;
|
94
|
+
|
95
|
+
&.not_applicable {
|
96
|
+
opacity: 0;
|
97
|
+
pointer-events: none;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
.main {
|
102
|
+
flex: 1;
|
103
|
+
display: flex;
|
104
|
+
align-items: baseline;
|
105
|
+
|
106
|
+
.bid {
|
107
|
+
font-family: monospace;
|
108
|
+
padding: 3px 6px;
|
109
|
+
background: rgba(0,0,0,0.2);
|
110
|
+
border-radius: 3px;
|
111
|
+
font-size: 12px;
|
112
|
+
margin: 0 12px 0 0;
|
113
|
+
|
114
|
+
&:hover {
|
115
|
+
.bid-goto {
|
116
|
+
display: inline-block;
|
117
|
+
padding: 0 0 0 4px;
|
118
|
+
font-size: 200%;
|
119
|
+
line-height: 10px;
|
120
|
+
vertical-align: sub;
|
121
|
+
text-decoration: dotted;
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
.bid-goto {
|
126
|
+
display: none;
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
.goto-link {
|
132
|
+
margin: 0 8px;
|
133
|
+
display: inline-block;
|
134
|
+
height: 16px;
|
135
|
+
font-size: 90%;
|
136
|
+
border-bottom: 1px dotted white;
|
137
|
+
}
|
138
|
+
|
139
|
+
.status-label {
|
140
|
+
font-family: monospace;
|
141
|
+
padding: 3px 6px;
|
142
|
+
background: rgba(0,0,0,0.2);
|
143
|
+
border-radius: 3px;
|
144
|
+
font-size: 12px;
|
145
|
+
margin: 0 12px 0 0;
|
146
|
+
|
147
|
+
&.deleted {
|
148
|
+
background: #99999933;
|
149
|
+
}
|
150
|
+
&.failed, &.complete {
|
151
|
+
background: #99000033;
|
152
|
+
}
|
153
|
+
&.success {
|
154
|
+
background: #00990033;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
.status-block {
|
159
|
+
width: 10em;
|
160
|
+
text-align: center;
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
> .subitems {
|
165
|
+
padding-left: 16px;
|
166
|
+
|
167
|
+
>.load-more {
|
168
|
+
padding: 4px 0;
|
169
|
+
text-align: center;
|
170
|
+
border-bottom: 1px dashed white;
|
171
|
+
a {
|
172
|
+
border-bottom: 1px dotted white;
|
173
|
+
text-decoration: none;
|
174
|
+
}
|
175
|
+
}
|
176
|
+
}
|
177
|
+
}
|
178
|
+
}
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import { h, Component, render } from 'https://unpkg.com/preact?module';
|
2
|
+
import htm from 'https://unpkg.com/htm?module';
|
3
|
+
import { root_url } from './util.js';
|
4
|
+
|
5
|
+
// Initialize htm with Preact
|
6
|
+
const html = htm.bind(h);
|
7
|
+
|
8
|
+
const StatusBlock = (props) => html`
|
9
|
+
<div class="status-block ${props.class || ''}">
|
10
|
+
${props.title && props.title + ':'}
|
11
|
+
<span class="tree-stat pending">${props.pending_count}</span>
|
12
|
+
|
|
13
|
+
<span class="tree-stat failed">${props.failed_count}</span>
|
14
|
+
|
|
15
|
+
<span class="tree-stat success">${props.successful_count}</span>
|
16
|
+
/
|
17
|
+
<span class="tree-stat total">${props.total_count}</span>
|
18
|
+
</div>
|
19
|
+
`
|
20
|
+
|
21
|
+
class TreeLevel extends Component {
|
22
|
+
get bid() {
|
23
|
+
return this.props.data.bid;
|
24
|
+
}
|
25
|
+
|
26
|
+
get batch() {
|
27
|
+
return this.props.data;
|
28
|
+
}
|
29
|
+
|
30
|
+
load_more = async (event) => {
|
31
|
+
event.preventDefault();
|
32
|
+
const l = this.batch.batches.items.length;
|
33
|
+
const resp = await fetch(`${root_url}batches/${this.bid}/tree?slice=${l}:${l + 5 - 1}`)
|
34
|
+
const result = await resp.json()
|
35
|
+
const newEntries = result.batches.items;
|
36
|
+
for (let ent of newEntries) {
|
37
|
+
this.batch.batches.items.push(ent)
|
38
|
+
}
|
39
|
+
this.forceUpdate()
|
40
|
+
}
|
41
|
+
|
42
|
+
toggle = (event) => {
|
43
|
+
event.preventDefault();
|
44
|
+
this.setState({ collapsed: !this.state.collapsed })
|
45
|
+
}
|
46
|
+
|
47
|
+
render() {
|
48
|
+
const { data: bd } = this.props;
|
49
|
+
|
50
|
+
let sub_entries = [];
|
51
|
+
let sub_batches = bd.batches.items;
|
52
|
+
for (let b of sub_batches) {
|
53
|
+
sub_entries.push(html`<${TreeLevel} data=${b} />`)
|
54
|
+
}
|
55
|
+
|
56
|
+
let fully_loaded = !(sub_batches.length < bd.batches.total_count);
|
57
|
+
|
58
|
+
const load_more_elem = html`<div class="load-more">
|
59
|
+
${sub_entries.length} / ${bd.batches.total_count} Items Loaded - <a href="#" onClick=${this.load_more}>Load More</a>
|
60
|
+
</div>`
|
61
|
+
|
62
|
+
return html`<div class="tree-entry tree-batch">
|
63
|
+
<div class="header">
|
64
|
+
<a class="row-toggle ${!bd.batches.total_count && 'not_applicable'}" onClick=${this.toggle} href="#">
|
65
|
+
${this.state.collapsed ? '+' : '-'}
|
66
|
+
</a>
|
67
|
+
|
68
|
+
<div class="header-inner">
|
69
|
+
<div class="main">
|
70
|
+
<span class="bid">
|
71
|
+
${bd.bid}
|
72
|
+
<a class="bid-goto" href="${root_url}batches/${bd.bid}">⇢</a>
|
73
|
+
</span>
|
74
|
+
${bd.description || (bd.status == 'deleted' && html`<i class="text-inactive">Deleted</i>`) || html`<i class="text-inactive">No Description</i>`}
|
75
|
+
</div>
|
76
|
+
|
77
|
+
<span class="status-label ${bd.status}">${bd.status}</span>
|
78
|
+
|
79
|
+
${bd.status != 'deleted' && html`
|
80
|
+
<${StatusBlock} class="job-status" title="Jobs" ...${bd.jobs} />
|
81
|
+
<${StatusBlock} class="batch-status" title="Batches" ...${bd.batches} />
|
82
|
+
`}
|
83
|
+
</div>
|
84
|
+
</div>
|
85
|
+
|
86
|
+
<div class="subitems ${this.state.collapsed ? 'hidden' : ''}">
|
87
|
+
${sub_entries}
|
88
|
+
${!fully_loaded && load_more_elem}
|
89
|
+
</div>
|
90
|
+
</div>`
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
class TreeRoot extends Component {
|
95
|
+
render() {
|
96
|
+
const tree_data = JSON.parse(document.querySelector('#batch-tree #initial-data').innerHTML);
|
97
|
+
return html`
|
98
|
+
<div class="tree-header">
|
99
|
+
<${StatusBlock} pending_count="pending" failed_count="failed" successful_count="successful" total_count="total" />
|
100
|
+
</div>
|
101
|
+
<${TreeLevel} data=${tree_data} />
|
102
|
+
`;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
render(html`<${TreeRoot} />`, document.querySelector('#batch-tree'));
|
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
<% add_to_head do %>
|
3
|
+
<meta name="sidekiq-baseurl" content="<%= root_path %>" />
|
4
|
+
<% if dev_mode? %>
|
5
|
+
<link href="<%= root_path %>batches_assets/css/styles.less" media="screen" rel="stylesheet/less" type="text/css" />
|
6
|
+
<script async type="text/javascript" src="https://unpkg.com/less@4.1.1/dist/less.js" data-async="true"></script>
|
7
|
+
<% else %>
|
8
|
+
<link href="<%= root_path %>batches_assets/css/styles.less" media="screen" rel="stylesheet/less" type="text/css" />
|
9
|
+
<script async type="text/javascript" src="https://unpkg.com/less@4.1.1/dist/less.js" data-async="true"></script>
|
10
|
+
<%# TODO: Pre-compile LESS %>
|
11
|
+
<!-- <link href="<%= root_path %>batches_assets/css/styles.css" media="screen" rel="stylesheet" type="text/css" /> -->
|
12
|
+
<% end %>
|
13
|
+
<% end %>
|
@@ -1,3 +1,5 @@
|
|
1
|
+
<%= erb get_template(:_common) %>
|
2
|
+
|
1
3
|
<h3><%= t('Batch') %></h3>
|
2
4
|
<% status = CanvasSync::JobBatches::Batch::Status.new(@batch) %>
|
3
5
|
|
@@ -17,70 +19,13 @@
|
|
17
19
|
<td><%= @batch.description %></td>
|
18
20
|
</tr>
|
19
21
|
<tr>
|
20
|
-
<th colspan="2" scope=row><%= t('
|
22
|
+
<th colspan="2" scope=row><%= t('Context') %></td>
|
21
23
|
<td>
|
22
|
-
<code class="code-wrap">
|
23
|
-
<div class="args-extended"><%= @batch
|
24
|
+
<code class="code-wrap batch-context">
|
25
|
+
<div class="args-extended"><%= format_context(@batch) %></div>
|
24
26
|
</code>
|
25
27
|
</td>
|
26
28
|
</tr>
|
27
|
-
<tr>
|
28
|
-
<th colspan="2" scope=row><%= t('Full Context') %></td>
|
29
|
-
<td>
|
30
|
-
<code class="code-wrap">
|
31
|
-
<div class="args-extended"><%= @batch.context.flatten.to_json %></div>
|
32
|
-
</code>
|
33
|
-
</td>
|
34
|
-
</tr>
|
35
|
-
|
36
|
-
<tr>
|
37
|
-
<th colspan="3">Jobs</th>
|
38
|
-
</tr>
|
39
|
-
<tr>
|
40
|
-
<th></th>
|
41
|
-
<th><%= t('Pending') %></th>
|
42
|
-
<td><%= status.pending %></th>
|
43
|
-
</tr>
|
44
|
-
<tr>
|
45
|
-
<th></th>
|
46
|
-
<th><%= t('Failed') %></th>
|
47
|
-
<td><%= status.failures %></th>
|
48
|
-
</tr>
|
49
|
-
<tr>
|
50
|
-
<th></th>
|
51
|
-
<th><%= t('Complete') %></th>
|
52
|
-
<td><%= status.completed_count %></th>
|
53
|
-
</tr>
|
54
|
-
<tr>
|
55
|
-
<th></th>
|
56
|
-
<th><%= t('Total') %></th>
|
57
|
-
<td><%= status.job_count %></th>
|
58
|
-
</tr>
|
59
|
-
|
60
|
-
<tr>
|
61
|
-
<th colspan="3">Batches</th>
|
62
|
-
</tr>
|
63
|
-
<tr>
|
64
|
-
<th></th>
|
65
|
-
<th><%= t('Pending') %></th>
|
66
|
-
<td><%= status.child_count - status.successful_children_count %></th>
|
67
|
-
</tr>
|
68
|
-
<tr>
|
69
|
-
<th></th>
|
70
|
-
<th><%= t('Failed') %></th>
|
71
|
-
<td><%= status.failed_children_count %></th>
|
72
|
-
</tr>
|
73
|
-
<tr>
|
74
|
-
<th></th>
|
75
|
-
<th><%= t('Success') %></th>
|
76
|
-
<td><%= status.successful_children_count %></th>
|
77
|
-
</tr>
|
78
|
-
<tr>
|
79
|
-
<th></th>
|
80
|
-
<th><%= t('Total') %></th>
|
81
|
-
<td><%= status.child_count %></th>
|
82
|
-
</tr>
|
83
|
-
|
84
29
|
</tbody>
|
85
30
|
</table>
|
86
31
|
</div>
|
@@ -88,51 +33,33 @@
|
|
88
33
|
<header class="row">
|
89
34
|
<div class="col-sm-5">
|
90
35
|
<h3>
|
91
|
-
<%= t('
|
36
|
+
<%= t('Child Batches') %>
|
92
37
|
</h3>
|
93
38
|
</div>
|
94
|
-
<%
|
95
|
-
@current_page = @current_jobs_page
|
96
|
-
@total_size = @total_jobs_size
|
97
|
-
%>
|
98
|
-
<% if @jobs.any? && @total_size > @count.to_i %>
|
99
|
-
<div class="col-sm-4">
|
100
|
-
<%= erb get_template(:_pagination), locals: { url: "#{root_path}batches/#{@batch.bid}", page_key: :job_page } %>
|
101
|
-
</div>
|
102
|
-
<% end %>
|
103
39
|
</header>
|
104
40
|
|
105
|
-
|
106
|
-
<div class="table_container">
|
107
|
-
<%= erb get_template(:_jobs_table), locals: { jobs: @jobs } %>
|
108
|
-
</div>
|
109
|
-
<% end %>
|
41
|
+
<%= erb get_template(:_batch_tree), locals: { } %>
|
110
42
|
|
111
43
|
<header class="row">
|
112
44
|
<div class="col-sm-5">
|
113
45
|
<h3>
|
114
|
-
<%= t('
|
46
|
+
<%= t('Jobs') %>
|
47
|
+
<i>(Queued and Current)</i>
|
115
48
|
</h3>
|
116
49
|
</div>
|
117
50
|
<%
|
118
|
-
@current_page = @
|
119
|
-
@total_size = @
|
51
|
+
@current_page = @current_jobs_page
|
52
|
+
@total_size = @total_jobs_size
|
120
53
|
%>
|
121
|
-
<% if @
|
54
|
+
<% if @jobs.any? && @total_size > @count.to_i %>
|
122
55
|
<div class="col-sm-4">
|
123
|
-
<%= erb get_template(:_pagination), locals: { url: "#{root_path}batches/#{@batch.bid}", page_key: :
|
56
|
+
<%= erb get_template(:_pagination), locals: { url: "#{root_path}batches/#{@batch.bid}", page_key: :job_page } %>
|
124
57
|
</div>
|
125
58
|
<% end %>
|
126
59
|
</header>
|
127
60
|
|
128
|
-
<% if @
|
61
|
+
<% if @jobs.any? %>
|
129
62
|
<div class="table_container">
|
130
|
-
<%= erb get_template(:
|
63
|
+
<%= erb get_template(:_jobs_table), locals: { jobs: @jobs } %>
|
131
64
|
</div>
|
132
65
|
<% end %>
|
133
|
-
|
134
|
-
<form class="form-horizontal" action="<%= root_path %>batches/<%= @batch.bid %>" method="post">
|
135
|
-
<%= csrf_tag %>
|
136
|
-
<a class="btn btn-default" href="<%= root_path %>batches"><%= t('GoBack') %></a>
|
137
|
-
<input class="btn btn-danger" type="submit" name="delete" value="<%= t('Delete') %>" data-confirm="<%= t('AreYouSure') %>" />
|
138
|
-
</form>
|
@@ -9,11 +9,19 @@ require_relative "web/helpers"
|
|
9
9
|
|
10
10
|
module CanvasSync::JobBatches::Sidekiq
|
11
11
|
module Web
|
12
|
+
DEV_MODE = (defined?(Rails) && !Rails.env.production?) || !!ENV["SIDEKIQ_WEB_TESTING"]
|
13
|
+
|
12
14
|
def self.registered(app) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
13
15
|
app.helpers do
|
14
16
|
include Web::Helpers
|
17
|
+
|
18
|
+
def dev_mode?
|
19
|
+
DEV_MODE
|
20
|
+
end
|
15
21
|
end
|
16
22
|
|
23
|
+
# =============== BATCHES =============== #
|
24
|
+
|
17
25
|
app.get "/batches" do
|
18
26
|
@count = (params['count'] || 25).to_i
|
19
27
|
@current_page, @total_size, @batches = page('batches', params['page'], @count)
|
@@ -26,6 +34,8 @@ module CanvasSync::JobBatches::Sidekiq
|
|
26
34
|
@bid = params[:bid]
|
27
35
|
@batch = CanvasSync::JobBatches::Batch.new(@bid)
|
28
36
|
|
37
|
+
@tree_data = tree_data(@bid)
|
38
|
+
|
29
39
|
@count = (params['count'] || 25).to_i
|
30
40
|
@current_batches_page, @total_batches_size, @sub_batches = page("BID-#{@batch.bid}-bids", params['batch_page'], @count)
|
31
41
|
@sub_batches = @sub_batches.map {|b, score| CanvasSync::JobBatches::Batch.new(b) }
|
@@ -36,6 +46,81 @@ module CanvasSync::JobBatches::Sidekiq
|
|
36
46
|
erb(get_template(:batch))
|
37
47
|
end
|
38
48
|
|
49
|
+
app.get "/batches/:bid/tree" do
|
50
|
+
@bid = params[:bid]
|
51
|
+
|
52
|
+
json(tree_data(@bid, slice: params[:slice]))
|
53
|
+
end
|
54
|
+
|
55
|
+
app.helpers do
|
56
|
+
def tree_data(root_bid, slice: nil)
|
57
|
+
tree_bids = CanvasSync::JobBatches::Batch.bid_hierarchy(root_bid, slice: slice)
|
58
|
+
|
59
|
+
CanvasSync::JobBatches::Batch.redis do |r|
|
60
|
+
layer_data = ->(layer, parent = nil) {
|
61
|
+
bid = layer[0]
|
62
|
+
batch = CanvasSync::JobBatches::Batch.new(bid)
|
63
|
+
|
64
|
+
jobs_total = r.hget("BID-#{bid}", "job_count").to_i
|
65
|
+
jobs_pending = r.hget("BID-#{bid}", 'pending').to_i
|
66
|
+
jobs_failed = r.scard("BID-#{bid}-failed").to_i
|
67
|
+
jobs_success = jobs_total - jobs_pending
|
68
|
+
|
69
|
+
batches_total = r.hget("BID-#{bid}", 'children').to_i
|
70
|
+
batches_success = r.scard("BID-#{bid}-batches-success").to_i
|
71
|
+
batches_pending = batches_total - batches_success
|
72
|
+
batches_failed = r.scard("BID-#{bid}-batches-failed").to_i
|
73
|
+
|
74
|
+
status = 'in_progress'
|
75
|
+
status = 'complete' if batches_pending == batches_failed && jobs_pending == jobs_failed
|
76
|
+
status = 'success' if batches_pending == 0 && jobs_pending == 0
|
77
|
+
status = 'deleted' if bid != root_bid && !batch.parent_bid
|
78
|
+
|
79
|
+
{
|
80
|
+
bid: bid,
|
81
|
+
created_at: r.hget("BID-#{bid}", 'created_at'),
|
82
|
+
status: status,
|
83
|
+
parent_bid: parent ? parent.bid : batch.parent_bid,
|
84
|
+
description: batch.description,
|
85
|
+
jobs: {
|
86
|
+
pending_count: jobs_pending,
|
87
|
+
successful_count: jobs_success,
|
88
|
+
failed_count: jobs_failed,
|
89
|
+
total_count: jobs_total,
|
90
|
+
# items: batches.map{|b| layer_data[b] },
|
91
|
+
},
|
92
|
+
batches: {
|
93
|
+
pending_count: batches_pending,
|
94
|
+
successful_count: batches_success,
|
95
|
+
failed_count: batches_failed,
|
96
|
+
total_count: batches_total,
|
97
|
+
items: layer[1].map{|b| layer_data[b, batch] },
|
98
|
+
},
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
data = layer_data[tree_bids]
|
103
|
+
data[:batches][:slice] = slice if slice
|
104
|
+
data
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def format_context(batch)
|
109
|
+
bits = []
|
110
|
+
own_keys = batch.context.own.keys
|
111
|
+
batch.context.flatten.each do |k,v|
|
112
|
+
added = own_keys.include? k
|
113
|
+
bits << " <span class=\"key #{added ? 'own' : 'inherited'}\">\"#{k}\": #{v.to_json},</span>"
|
114
|
+
end
|
115
|
+
bits = [
|
116
|
+
"{ // <span class=\"own\">Added</span> / <span class=\"inherited\">Inherited</span>",
|
117
|
+
*bits,
|
118
|
+
'}'
|
119
|
+
]
|
120
|
+
bits.join("\n")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
39
124
|
app.post "/batches/all" do
|
40
125
|
if params['delete']
|
41
126
|
drain_zset('batches') do |batches|
|
@@ -107,6 +192,14 @@ module CanvasSync::JobBatches::Sidekiq
|
|
107
192
|
end
|
108
193
|
|
109
194
|
if defined?(::Sidekiq::Web)
|
195
|
+
rules = []
|
196
|
+
rules = [[:all, {"Cache-Control" => "public, max-age=86400"}]] unless CanvasSync::JobBatches::Sidekiq::Web::DEV_MODE
|
197
|
+
|
198
|
+
::Sidekiq::Web.use Rack::Static, urls: ["/batches_assets"],
|
199
|
+
root: File.expand_path("#{File.dirname(__FILE__)}/web"),
|
200
|
+
cascade: true,
|
201
|
+
header_rules: rules
|
202
|
+
|
110
203
|
::Sidekiq::Web.register CanvasSync::JobBatches::Sidekiq::Web
|
111
204
|
::Sidekiq::Web.tabs["Batches"] = "batches"
|
112
205
|
::Sidekiq::Web.tabs["Pools"] = "pools"
|
@@ -46,7 +46,7 @@ module CanvasSync
|
|
46
46
|
m = Regexp.last_match
|
47
47
|
day = m[1]
|
48
48
|
skip = m[2] || "1"
|
49
|
-
|
49
|
+
DateTime.now.send(:"#{day}?") && last_full_sync.end_of_day <= (skip.to_i.weeks.ago.end_of_day)
|
50
50
|
when opt.match?(%r{^(\d+)\%$})
|
51
51
|
m = Regexp.last_match
|
52
52
|
rand(100) < m[1].to_i
|
@@ -4,8 +4,9 @@ module CanvasSync
|
|
4
4
|
# Re-enqueues itself if the report is still processing on Canvas.
|
5
5
|
# Enqueues the ReportProcessor when the report has completed.
|
6
6
|
class ReportChecker < CanvasSync::Job
|
7
|
-
REPORT_TIMEOUT =
|
7
|
+
REPORT_TIMEOUT = 24.hours
|
8
8
|
COMPILATION_TIMEOUT = 1.hour
|
9
|
+
MAX_TRIES = 3
|
9
10
|
|
10
11
|
# @param report_name [Hash] e.g., 'provisioning_csv'
|
11
12
|
# @param report_id [Integer]
|
@@ -13,6 +14,7 @@ module CanvasSync
|
|
13
14
|
# @param options [Hash] hash of options that will be passed to the job processor
|
14
15
|
# @return [nil]
|
15
16
|
def perform(report_name, report_id, processor, options, checker_context = {}) # rubocop:disable Metrics/AbcSize
|
17
|
+
max_tries = options[:report_max_tries] || batch_context[:report_max_tries] || MAX_TRIES
|
16
18
|
account_id = options[:account_id] || batch_context[:account_id] || "self"
|
17
19
|
report_status = CanvasSync.get_canvas_sync_client(batch_context)
|
18
20
|
.report_status(account_id, report_name, report_id)
|
@@ -27,9 +29,17 @@ module CanvasSync
|
|
27
29
|
report_id,
|
28
30
|
)
|
29
31
|
when "error", "deleted"
|
30
|
-
|
32
|
+
checker_context[:failed_attempts] ||= 0
|
33
|
+
checker_context[:failed_attempts] += 1
|
34
|
+
failed_attempts = checker_context[:failed_attempts]
|
35
|
+
message = "Report failed to process; status was #{report_status} for report_name: #{report_name}, report_id: #{report_id}, #{current_organization.name}. This report has now failed #{checker_context[:failed_attempts]} time." # rubocop:disable Metrics/LineLength
|
31
36
|
Rails.logger.error(message)
|
32
|
-
|
37
|
+
if failed_attempts >= max_tries
|
38
|
+
Rails.logger.error("This report has failed #{failed_attempts} times. Giving up.")
|
39
|
+
raise message
|
40
|
+
else
|
41
|
+
restart_report(options, report_name, processor, checker_context)
|
42
|
+
end
|
33
43
|
else
|
34
44
|
report_timeout = parse_timeout(options[:report_timeout] || batch_context[:report_timeout] || REPORT_TIMEOUT)
|
35
45
|
if timeout_met?(options[:sync_start_time], report_timeout)
|
@@ -51,7 +61,7 @@ module CanvasSync
|
|
51
61
|
report_id,
|
52
62
|
processor,
|
53
63
|
options,
|
54
|
-
checker_context
|
64
|
+
checker_context
|
55
65
|
)
|
56
66
|
end
|
57
67
|
end
|
@@ -66,6 +76,29 @@ module CanvasSync
|
|
66
76
|
def parse_timeout(val)
|
67
77
|
val
|
68
78
|
end
|
79
|
+
|
80
|
+
def restart_report(options, report_name, processor, checker_context)
|
81
|
+
account_id = options[:account_id] || batch_context[:account_id] || "self"
|
82
|
+
options[:sync_start_time] = DateTime.now.utc.iso8601
|
83
|
+
new_context = {}
|
84
|
+
new_context[:failed_attempts] = checker_context[:failed_attempts]
|
85
|
+
report_id = start_report(account_id, report_name, options[:report_params])
|
86
|
+
CanvasSync::Jobs::ReportChecker
|
87
|
+
.set(wait: report_checker_wait_time)
|
88
|
+
.perform_later(
|
89
|
+
report_name,
|
90
|
+
report_id,
|
91
|
+
processor,
|
92
|
+
options,
|
93
|
+
new_context
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def start_report(account_id, report_name, report_params)
|
98
|
+
report = CanvasSync.get_canvas_sync_client(batch_context)
|
99
|
+
.start_report(account_id, report_name, report_params)
|
100
|
+
report["id"]
|
101
|
+
end
|
69
102
|
end
|
70
103
|
end
|
71
104
|
end
|
@@ -12,7 +12,7 @@ module CanvasSync
|
|
12
12
|
def perform(report_name, report_params, processor, options, allow_redownloads: false)
|
13
13
|
account_id = options[:account_id] || batch_context[:account_id] || "self"
|
14
14
|
options[:sync_start_time] = DateTime.now.utc.iso8601
|
15
|
-
|
15
|
+
options[:report_params] = report_params
|
16
16
|
report_id = start_report(account_id, report_name, report_params)
|
17
17
|
# TODO: Restore report caching support (does nayone actually use it?)
|
18
18
|
# report_id = if allow_redownloads
|
@@ -28,7 +28,7 @@ module CanvasSync
|
|
28
28
|
report_name,
|
29
29
|
report_id,
|
30
30
|
processor,
|
31
|
-
options
|
31
|
+
options
|
32
32
|
)
|
33
33
|
end
|
34
34
|
end
|
data/lib/canvas_sync/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: canvas_sync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.17.
|
4
|
+
version: 0.17.20
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Collings
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-08-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -442,6 +442,7 @@ files:
|
|
442
442
|
- lib/canvas_sync/job_batches/callback.rb
|
443
443
|
- lib/canvas_sync/job_batches/chain_builder.rb
|
444
444
|
- lib/canvas_sync/job_batches/context_hash.rb
|
445
|
+
- lib/canvas_sync/job_batches/hier_batch_ids.lua
|
445
446
|
- lib/canvas_sync/job_batches/hincr_max.lua
|
446
447
|
- lib/canvas_sync/job_batches/jobs/base_job.rb
|
447
448
|
- lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb
|
@@ -453,8 +454,13 @@ files:
|
|
453
454
|
- lib/canvas_sync/job_batches/schedule_callback.lua
|
454
455
|
- lib/canvas_sync/job_batches/sidekiq.rb
|
455
456
|
- lib/canvas_sync/job_batches/sidekiq/web.rb
|
457
|
+
- lib/canvas_sync/job_batches/sidekiq/web/batches_assets/css/styles.less
|
458
|
+
- lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/batch_tree.js
|
459
|
+
- lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/util.js
|
456
460
|
- lib/canvas_sync/job_batches/sidekiq/web/helpers.rb
|
461
|
+
- lib/canvas_sync/job_batches/sidekiq/web/views/_batch_tree.erb
|
457
462
|
- lib/canvas_sync/job_batches/sidekiq/web/views/_batches_table.erb
|
463
|
+
- lib/canvas_sync/job_batches/sidekiq/web/views/_common.erb
|
458
464
|
- lib/canvas_sync/job_batches/sidekiq/web/views/_jobs_table.erb
|
459
465
|
- lib/canvas_sync/job_batches/sidekiq/web/views/_pagination.erb
|
460
466
|
- lib/canvas_sync/job_batches/sidekiq/web/views/batch.erb
|