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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1cc1054e379b920c5acee75583700fdffaae885a4cea48df1d8de97ed70c02e
4
- data.tar.gz: 83741af3ec47ee4b72db5e4a354a8713c002fc6930cfe352a80c5f57a36dba4a
3
+ metadata.gz: 85296cf62f289592ac9f7a9eae904ab8f4f95fa4e721c0746fb12b47e8f3679c
4
+ data.tar.gz: 4e766399a5b695d589584454e4a0697480131bde11599887b13371d10b03c09d
5
5
  SHA512:
6
- metadata.gz: dc7ad8ad007bda5edb7cf980b767113075a4be84cf1c2a228cf8287f8aba2c587bb170574a0168216b0bcaa19c8c395224d37249954b23203a50df234e5d9599
7
- data.tar.gz: be307cba8401af1b7697d4c843d260e290517aca0f3d3866a72db6f327f6d3726df894a5306f2b39f2e10eacbb22465a45b5b8398c810304d53145ef59a17ad7
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.
@@ -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
- next_job_class.perform_later(duped_job_chain, next_job[:options])
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 = sub_items.respond_to?(:count) ? sub_items.count : sub_items
130
- job_log.update!(fork_count: sub_count)
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.with_indifferent_access
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
- jobs += Array.wrap({
227
- job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s,
228
- options: { term_scope: term_scope, models: models },
229
- }) if models.present?
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
@@ -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
- fork_item = (job_chain[:global_options][:fork_path] || []).pop
5
+ forked_job = self.class.forked_at_job(job_chain)
6
6
 
7
- if fork_item.present?
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
@@ -1,3 +1,3 @@
1
1
  module CanvasSync
2
- VERSION = "0.10.5".freeze
2
+ VERSION = "0.10.6".freeze
3
3
  end
@@ -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.5
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-17 00:00:00.000000000 Z
11
+ date: 2019-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler