canvas_sync 0.10.5 → 0.10.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +32 -0
- data/lib/canvas_sync.rb +22 -10
- data/lib/canvas_sync/job.rb +14 -1
- data/lib/canvas_sync/jobs/fork_gather.rb +36 -3
- data/lib/canvas_sync/version.rb +1 -1
- data/spec/canvas_sync/canvas_sync_spec.rb +17 -0
- data/spec/canvas_sync/jobs/fork_gather_spec.rb +43 -0
- data/spec/canvas_sync/jobs/job_spec.rb +39 -5
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85296cf62f289592ac9f7a9eae904ab8f4f95fa4e721c0746fb12b47e8f3679c
|
4
|
+
data.tar.gz: 4e766399a5b695d589584454e4a0697480131bde11599887b13371d10b03c09d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 543d433fcf85fcae47fa4937c796d7efe59954e39d38c9bdae63e03e4c0bfef1613483c4d2e2994fd3e481c9da1b7f5ea3e7ab8719f43103d74a4caa128baa41
|
7
|
+
data.tar.gz: bbab77a8e574656e3030e643460694e60147f740095d6d37630d5ae6770c2eb60d0502e2a83db7cfce253035804084662e2255dd5154ed4c595c6589310e13b2
|
data/README.md
CHANGED
@@ -398,6 +398,38 @@ Available config options (if you add more, please update this!):
|
|
398
398
|
|
399
399
|
* `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.
|
400
400
|
|
401
|
+
## Handling Job errors
|
402
|
+
|
403
|
+
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`.
|
404
|
+
The value should be a String in the following format: `ModuleOrClass::AnotherModuleOrClass.class_method`.
|
405
|
+
The given method of the given class will be called when an error occurs.
|
406
|
+
The handling method should accept 2 arguments: `[error, **options]`
|
407
|
+
|
408
|
+
The current parameters provided in `**options` are:
|
409
|
+
- `job_chain`
|
410
|
+
- `job_log`
|
411
|
+
|
412
|
+
Example:
|
413
|
+
```ruby
|
414
|
+
class CanvasSyncStarterWorker
|
415
|
+
def perform
|
416
|
+
job_chain = CanvasSync.default_provisioning_report_chain(
|
417
|
+
%w[desired models],
|
418
|
+
options: {
|
419
|
+
global: {
|
420
|
+
on_failure: 'CanvasSyncStarterWorker.handle_canvas_sync_error',
|
421
|
+
}
|
422
|
+
}
|
423
|
+
)
|
424
|
+
CanvasSync.invoke_next(job_chain)
|
425
|
+
end
|
426
|
+
|
427
|
+
def self.handle_canvas_sync_error(error, **options)
|
428
|
+
# Do Stuff
|
429
|
+
end
|
430
|
+
end
|
431
|
+
```
|
432
|
+
|
401
433
|
## Upgrading
|
402
434
|
|
403
435
|
Re-running the generator when there's been a gem change will give you several choices if it detects conflicts between your local files and the updated generators. You can either view a diff or allow the generator to overwrite your local file. In most cases you may just want to add the code from the diff yourself so as not to break any of your customizations.
|
data/lib/canvas_sync.rb
CHANGED
@@ -101,12 +101,16 @@ module CanvasSync
|
|
101
101
|
invoke_next(job_chain)
|
102
102
|
end
|
103
103
|
|
104
|
+
def duplicate_chain(job_chain)
|
105
|
+
Marshal.load(Marshal.dump(job_chain))
|
106
|
+
end
|
107
|
+
|
104
108
|
# Invokes the next job in a chain of jobs.
|
105
109
|
#
|
106
110
|
# This should typically be called automatically by the gem where necessary.
|
107
111
|
#
|
108
112
|
# @param job_chain [Hash] A chain of jobs to execute
|
109
|
-
def invoke_next(job_chain)
|
113
|
+
def invoke_next(job_chain, extra_options: {})
|
110
114
|
return if job_chain[:jobs].empty?
|
111
115
|
|
112
116
|
# Make sure all job classes are serialized as strings
|
@@ -116,7 +120,9 @@ module CanvasSync
|
|
116
120
|
jobs = duped_job_chain[:jobs]
|
117
121
|
next_job = jobs.shift
|
118
122
|
next_job_class = next_job[:job].constantize
|
119
|
-
|
123
|
+
next_options = next_job[:options] || {}
|
124
|
+
next_options.merge!(extra_options)
|
125
|
+
next_job_class.perform_later(duped_job_chain, next_options)
|
120
126
|
end
|
121
127
|
|
122
128
|
def fork(job_log, job_chain, keys: [])
|
@@ -125,9 +131,10 @@ module CanvasSync
|
|
125
131
|
duped_job_chain[:global_options][:fork_keys] ||= []
|
126
132
|
duped_job_chain[:global_options][:fork_path] << job_log.job_id
|
127
133
|
duped_job_chain[:global_options][:fork_keys] << ['canvas_term_id']
|
134
|
+
duped_job_chain[:global_options][:on_failure] ||= 'CanvasSync::Jobs::ForkGather.handle_branch_error'
|
128
135
|
sub_items = yield duped_job_chain
|
129
|
-
sub_count =
|
130
|
-
job_log.
|
136
|
+
sub_count = sub_items.respond_to?(:count) ? sub_items.count : sub_items
|
137
|
+
job_log.fork_count = sub_count
|
131
138
|
sub_items
|
132
139
|
end
|
133
140
|
|
@@ -172,7 +179,7 @@ module CanvasSync
|
|
172
179
|
return unless models.present?
|
173
180
|
models.map! &:to_s
|
174
181
|
term_scope = term_scope.to_s if term_scope
|
175
|
-
options = options.
|
182
|
+
options = options.deep_symbolize_keys!
|
176
183
|
|
177
184
|
model_job_map = {
|
178
185
|
terms: CanvasSync::Jobs::SyncTermsJob,
|
@@ -190,7 +197,7 @@ module CanvasSync
|
|
190
197
|
jobs = []
|
191
198
|
try_add_model_job = ->(model) {
|
192
199
|
return unless models.include?(model)
|
193
|
-
jobs.push(job: model_job_map[model].to_s, options: options[model] || {})
|
200
|
+
jobs.push(job: model_job_map[model].to_s, options: options[model.to_sym] || {})
|
194
201
|
models -= [model]
|
195
202
|
}
|
196
203
|
|
@@ -223,14 +230,19 @@ module CanvasSync
|
|
223
230
|
post_provisioning_jobs = jobs
|
224
231
|
|
225
232
|
jobs = pre_provisioning_jobs
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
233
|
+
if models.present?
|
234
|
+
provisioning_job = {
|
235
|
+
job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s,
|
236
|
+
options: { term_scope: term_scope, models: models },
|
237
|
+
}
|
238
|
+
provisioning_job[:options].merge!(options[:provisioning]) if options[:provisioning].present?
|
239
|
+
jobs += Array.wrap(provisioning_job)
|
240
|
+
end
|
230
241
|
jobs += post_provisioning_jobs
|
231
242
|
|
232
243
|
global_options = { legacy_support: legacy_support }
|
233
244
|
global_options[:account_id] = account_id if account_id.present?
|
245
|
+
global_options.merge!(options[:global]) if options[:global].present?
|
234
246
|
|
235
247
|
{ jobs: jobs, global_options: global_options }
|
236
248
|
end
|
data/lib/canvas_sync/job.rb
CHANGED
@@ -13,13 +13,26 @@ module CanvasSync
|
|
13
13
|
@job_log.started_at = Time.now
|
14
14
|
@job_log.save
|
15
15
|
|
16
|
+
@job_chain = job.arguments[0] if job.arguments[0].is_a?(Hash) && job.arguments[0].include?(:jobs)
|
17
|
+
|
16
18
|
begin
|
17
19
|
block.call
|
18
20
|
@job_log.status = JobLog::SUCCESS_STATUS
|
19
21
|
rescue => e # rubocop:disable Style/RescueStandardError
|
20
22
|
@job_log.exception = "#{e.class}: #{e.message}"
|
21
|
-
@job_log.backtrace = e.backtrace
|
23
|
+
@job_log.backtrace = e.backtrace.join('\n')
|
22
24
|
@job_log.status = JobLog::ERROR_STATUS
|
25
|
+
if @job_chain&.[](:global_options)&.[](:on_failure)&.present?
|
26
|
+
begin
|
27
|
+
class_name, method = @job_chain[:global_options][:on_failure].split('.')
|
28
|
+
klass = class_name.constantize
|
29
|
+
klass.send(method.to_sym, e, job_chain: @job_chain, job_log: @job_log)
|
30
|
+
rescue => e2
|
31
|
+
@job_log.backtrace += "\n\nError Occurred while handling an Error: #{e2.class}: #{e2.message}"
|
32
|
+
@job_log.backtrace += "\n" + e2.backtrace.join('\n')
|
33
|
+
Raven.captureException(e2) if defined? Raven
|
34
|
+
end
|
35
|
+
end
|
23
36
|
raise e
|
24
37
|
ensure
|
25
38
|
if CanvasSync.config.classes_to_only_log_errors_on.include?(@job_log.job_class) && @job_log.status != JobLog::ERROR_STATUS
|
@@ -2,10 +2,9 @@ module CanvasSync
|
|
2
2
|
module Jobs
|
3
3
|
class ForkGather < CanvasSync::Job
|
4
4
|
def perform(job_chain, options)
|
5
|
-
|
5
|
+
forked_job = self.class.forked_at_job(job_chain)
|
6
6
|
|
7
|
-
if
|
8
|
-
forked_job = CanvasSync::JobLog.find_by(job_id: fork_item)
|
7
|
+
if forked_job.present?
|
9
8
|
forked_job.with_lock do
|
10
9
|
forked_job.fork_count -= 1
|
11
10
|
forked_job.save!
|
@@ -21,6 +20,40 @@ module CanvasSync
|
|
21
20
|
CanvasSync.invoke_next(job_chain)
|
22
21
|
end
|
23
22
|
end
|
23
|
+
|
24
|
+
def self.handle_branch_error(e, job_chain:, skip_invoke: false, **kwargs)
|
25
|
+
return nil unless job_chain&.[](:global_options)&.[](:fork_path).present?
|
26
|
+
|
27
|
+
duped_chain = CanvasSync.duplicate_chain(job_chain)
|
28
|
+
job_list = duped_chain[:jobs]
|
29
|
+
while job_list.count > 0
|
30
|
+
job_class_name = job_list[0][:job]
|
31
|
+
job_class = job_class_name.constantize
|
32
|
+
break if job_class <= CanvasSync::Jobs::ForkGather
|
33
|
+
job_list.shift
|
34
|
+
end
|
35
|
+
|
36
|
+
return nil unless job_list.present?
|
37
|
+
|
38
|
+
if skip_invoke
|
39
|
+
duped_chain
|
40
|
+
else
|
41
|
+
CanvasSync.invoke_next(duped_chain)
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def self.forked_at_job(job_chain)
|
49
|
+
fork_item = (job_chain[:global_options][:fork_path] || []).pop
|
50
|
+
|
51
|
+
if fork_item.present?
|
52
|
+
CanvasSync::JobLog.find_by(job_id: fork_item)
|
53
|
+
else
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
24
57
|
end
|
25
58
|
end
|
26
59
|
end
|
data/lib/canvas_sync/version.rb
CHANGED
@@ -24,6 +24,23 @@ RSpec.describe CanvasSync do
|
|
24
24
|
end
|
25
25
|
|
26
26
|
describe '.default_provisioning_report_chain' do
|
27
|
+
it 'splits an options: Hash into options for separate models' do
|
28
|
+
chain = CanvasSync.default_provisioning_report_chain(['users', 'courses'], :active, options: {
|
29
|
+
terms: { a: 1 },
|
30
|
+
users: { b: 2 },
|
31
|
+
provisioning: { c: 3 },
|
32
|
+
global: { d: 4 },
|
33
|
+
})
|
34
|
+
expect(chain).to eq({
|
35
|
+
jobs: [
|
36
|
+
{ job: CanvasSync::Jobs::SyncTermsJob.to_s, options: { a: 1 } },
|
37
|
+
{ job: CanvasSync::Jobs::SyncUsersJob.to_s, options: { b: 2 } },
|
38
|
+
{ job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s, options: { term_scope: 'active', models: ['courses'], c: 3 } }
|
39
|
+
],
|
40
|
+
global_options: { legacy_support: false, d: 4 }
|
41
|
+
})
|
42
|
+
end
|
43
|
+
|
27
44
|
context 'we are syncing users with a term scope' do
|
28
45
|
it 'syncs the users in a separate job that runs first' do
|
29
46
|
chain = CanvasSync.default_provisioning_report_chain(['users', 'courses'], :active)
|
@@ -26,5 +26,48 @@ RSpec.describe CanvasSync::Jobs::ForkGather do
|
|
26
26
|
expect(CanvasSync).to receive(:invoke_next)
|
27
27
|
CanvasSync::Jobs::ForkGather.perform_now(job_chain, {})
|
28
28
|
end
|
29
|
+
|
30
|
+
it 'pops the most-recent fork_path enrty' do
|
31
|
+
job_log.update!(fork_count: 1)
|
32
|
+
expect(CanvasSync).to receive(:invoke_next) do |*args|
|
33
|
+
expect(args[0][:global_options][:fork_path]).to eq []
|
34
|
+
end
|
35
|
+
CanvasSync::Jobs::ForkGather.perform_now(job_chain, {})
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'handle_branch_error' do
|
40
|
+
let(:error) { StandardError.new }
|
41
|
+
|
42
|
+
let(:job_chain) {
|
43
|
+
{
|
44
|
+
jobs: [
|
45
|
+
{ job: 'CanvasSync::Jobs::ReportChecker' },
|
46
|
+
{ job: 'CanvasSync::Jobs::ReportChecker' },
|
47
|
+
{ job: 'CanvasSync::Jobs::ForkGather' },
|
48
|
+
{ job: 'CanvasSync::Jobs::ReportChecker' },
|
49
|
+
],
|
50
|
+
global_options: {
|
51
|
+
fork_path: ['BLAH'],
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
it 'skips to and performs the next ForkGatherJob' do
|
57
|
+
expect(CanvasSync).to receive(:invoke_next) do |*args|
|
58
|
+
expect(args[0][:jobs][0][:job]).to eq 'CanvasSync::Jobs::ForkGather'
|
59
|
+
end
|
60
|
+
expect(CanvasSync::Jobs::ForkGather.handle_branch_error(error, job_chain: job_chain)).to be true
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'does nothing if no ForkGather is in the chain' do
|
64
|
+
job_chain[:jobs].delete_at(2)
|
65
|
+
expect(CanvasSync::Jobs::ForkGather.handle_branch_error(error, job_chain: job_chain)).to be nil
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'does nothing if no fork_path is present' do
|
69
|
+
job_chain[:global_options][:fork_path] = []
|
70
|
+
expect(CanvasSync::Jobs::ForkGather.handle_branch_error(error, job_chain: job_chain)).to be nil
|
71
|
+
end
|
29
72
|
end
|
30
73
|
end
|
@@ -1,40 +1,74 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
class GoodJob < CanvasSync::Job
|
4
|
-
def perform(argument)
|
4
|
+
def perform(job_chain, argument)
|
5
5
|
end
|
6
6
|
end
|
7
7
|
|
8
8
|
class EvilError < StandardError; end;
|
9
9
|
|
10
10
|
class BadJob < CanvasSync::Job
|
11
|
-
def perform(argument)
|
11
|
+
def perform(job_chain, argument)
|
12
12
|
raise EvilError
|
13
13
|
end
|
14
|
+
|
15
|
+
def self.evil_catcher(e, *opts)
|
16
|
+
raise EvilError
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.good_catcher(e, *opts)
|
20
|
+
|
21
|
+
end
|
14
22
|
end
|
15
23
|
|
16
24
|
RSpec.describe CanvasSync::Job do
|
17
25
|
describe '#perform' do
|
18
26
|
it 'creates a CanvasSync::JobLog and logs relevant data on it' do
|
19
27
|
expect {
|
20
|
-
GoodJob.perform_now("argument")
|
28
|
+
GoodJob.perform_now({}, "argument")
|
21
29
|
}.to change { CanvasSync::JobLog.count }.by(1)
|
22
30
|
|
23
31
|
job_log = CanvasSync::JobLog.last
|
24
32
|
expect(job_log.started_at).to_not be_nil
|
25
33
|
expect(job_log.job_class).to eq(GoodJob.to_s)
|
26
|
-
expect(job_log.job_arguments).to eq(["argument"])
|
34
|
+
expect(job_log.job_arguments).to eq([{}, "argument"])
|
27
35
|
expect(job_log.completed_at).to_not be_nil
|
28
36
|
end
|
29
37
|
|
30
38
|
it 'logs exceptions on the CanvasSync::JobLog and then re-raises' do
|
31
39
|
expect {
|
32
|
-
BadJob.perform_now("argument")
|
40
|
+
BadJob.perform_now({}, "argument")
|
33
41
|
}.to raise_exception(StandardError)
|
34
42
|
|
35
43
|
job_log = CanvasSync::JobLog.last
|
36
44
|
expect(job_log.exception).to eq("EvilError: EvilError")
|
37
45
|
expect(job_log.backtrace).to_not be_nil
|
38
46
|
end
|
47
|
+
|
48
|
+
it 'invokes an error handler' do
|
49
|
+
expect(BadJob).to receive(:good_catcher).once
|
50
|
+
expect {
|
51
|
+
BadJob.perform_now({
|
52
|
+
jobs: [],
|
53
|
+
global_options: {
|
54
|
+
on_failure: 'BadJob.good_catcher'
|
55
|
+
}
|
56
|
+
})
|
57
|
+
}.to raise_exception(StandardError)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'logs a failing error handler' do
|
61
|
+
expect(BadJob).to receive(:bad_catcher).once.and_call_original
|
62
|
+
expect {
|
63
|
+
BadJob.perform_now({
|
64
|
+
jobs: [],
|
65
|
+
global_options: {
|
66
|
+
on_failure: 'BadJob.bad_catcher'
|
67
|
+
}
|
68
|
+
})
|
69
|
+
}.to raise_exception(StandardError)
|
70
|
+
job_log = CanvasSync::JobLog.last
|
71
|
+
expect(job_log.backtrace).to include "Error Occurred while handling an Error"
|
72
|
+
end
|
39
73
|
end
|
40
74
|
end
|
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.10.
|
4
|
+
version: 0.10.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Collings
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-09-
|
11
|
+
date: 2019-09-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|