canvas_sync 0.16.5 → 0.17.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -137
  3. data/app/models/canvas_sync/sync_batch.rb +5 -0
  4. data/db/migrate/20201018210836_create_canvas_sync_sync_batches.rb +11 -0
  5. data/lib/canvas_sync.rb +35 -97
  6. data/lib/canvas_sync/importers/bulk_importer.rb +4 -7
  7. data/lib/canvas_sync/job.rb +4 -10
  8. data/lib/canvas_sync/job_batches/batch.rb +403 -0
  9. data/lib/canvas_sync/job_batches/batch_aware_job.rb +62 -0
  10. data/lib/canvas_sync/job_batches/callback.rb +152 -0
  11. data/lib/canvas_sync/job_batches/chain_builder.rb +220 -0
  12. data/lib/canvas_sync/job_batches/context_hash.rb +147 -0
  13. data/lib/canvas_sync/job_batches/jobs/base_job.rb +7 -0
  14. data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +19 -0
  15. data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +75 -0
  16. data/lib/canvas_sync/job_batches/sidekiq.rb +93 -0
  17. data/lib/canvas_sync/job_batches/status.rb +83 -0
  18. data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +35 -0
  19. data/lib/canvas_sync/jobs/report_checker.rb +3 -6
  20. data/lib/canvas_sync/jobs/report_processor_job.rb +2 -5
  21. data/lib/canvas_sync/jobs/report_starter.rb +28 -20
  22. data/lib/canvas_sync/jobs/sync_accounts_job.rb +3 -5
  23. data/lib/canvas_sync/jobs/sync_admins_job.rb +2 -4
  24. data/lib/canvas_sync/jobs/sync_assignment_groups_job.rb +2 -4
  25. data/lib/canvas_sync/jobs/sync_assignments_job.rb +2 -4
  26. data/lib/canvas_sync/jobs/sync_context_module_items_job.rb +2 -4
  27. data/lib/canvas_sync/jobs/sync_context_modules_job.rb +2 -4
  28. data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +4 -34
  29. data/lib/canvas_sync/jobs/sync_roles_job.rb +2 -5
  30. data/lib/canvas_sync/jobs/sync_simple_table_job.rb +11 -32
  31. data/lib/canvas_sync/jobs/sync_submissions_job.rb +2 -4
  32. data/lib/canvas_sync/jobs/sync_terms_job.rb +25 -8
  33. data/lib/canvas_sync/processors/assignment_groups_processor.rb +2 -3
  34. data/lib/canvas_sync/processors/assignments_processor.rb +2 -3
  35. data/lib/canvas_sync/processors/context_module_items_processor.rb +2 -3
  36. data/lib/canvas_sync/processors/context_modules_processor.rb +2 -3
  37. data/lib/canvas_sync/processors/normal_processor.rb +1 -2
  38. data/lib/canvas_sync/processors/provisioning_report_processor.rb +2 -10
  39. data/lib/canvas_sync/processors/submissions_processor.rb +2 -3
  40. data/lib/canvas_sync/version.rb +1 -1
  41. data/spec/canvas_sync/canvas_sync_spec.rb +136 -153
  42. data/spec/canvas_sync/jobs/job_spec.rb +9 -17
  43. data/spec/canvas_sync/jobs/report_checker_spec.rb +1 -3
  44. data/spec/canvas_sync/jobs/report_processor_job_spec.rb +0 -3
  45. data/spec/canvas_sync/jobs/report_starter_spec.rb +19 -28
  46. data/spec/canvas_sync/jobs/sync_admins_job_spec.rb +1 -4
  47. data/spec/canvas_sync/jobs/sync_assignment_groups_job_spec.rb +2 -1
  48. data/spec/canvas_sync/jobs/sync_assignments_job_spec.rb +3 -2
  49. data/spec/canvas_sync/jobs/sync_context_module_items_job_spec.rb +3 -2
  50. data/spec/canvas_sync/jobs/sync_context_modules_job_spec.rb +3 -2
  51. data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +3 -35
  52. data/spec/canvas_sync/jobs/sync_roles_job_spec.rb +1 -4
  53. data/spec/canvas_sync/jobs/sync_simple_table_job_spec.rb +5 -12
  54. data/spec/canvas_sync/jobs/sync_submissions_job_spec.rb +2 -1
  55. data/spec/canvas_sync/jobs/sync_terms_job_spec.rb +1 -4
  56. data/spec/dummy/config/environments/test.rb +2 -0
  57. data/spec/dummy/db/schema.rb +9 -1
  58. data/spec/job_batching/batch_aware_job_spec.rb +100 -0
  59. data/spec/job_batching/batch_spec.rb +372 -0
  60. data/spec/job_batching/callback_spec.rb +38 -0
  61. data/spec/job_batching/flow_spec.rb +88 -0
  62. data/spec/job_batching/integration/integration.rb +57 -0
  63. data/spec/job_batching/integration/nested.rb +88 -0
  64. data/spec/job_batching/integration/simple.rb +47 -0
  65. data/spec/job_batching/integration/workflow.rb +134 -0
  66. data/spec/job_batching/integration_helper.rb +48 -0
  67. data/spec/job_batching/sidekiq_spec.rb +124 -0
  68. data/spec/job_batching/status_spec.rb +92 -0
  69. data/spec/job_batching/support/base_job.rb +14 -0
  70. data/spec/job_batching/support/sample_callback.rb +2 -0
  71. data/spec/spec_helper.rb +17 -0
  72. metadata +85 -8
  73. data/lib/canvas_sync/job_chain.rb +0 -102
  74. data/lib/canvas_sync/jobs/fork_gather.rb +0 -74
  75. data/spec/canvas_sync/jobs/fork_gather_spec.rb +0 -73
@@ -0,0 +1,7 @@
1
+ module CanvasSync
2
+ module JobBatches
3
+ class BaseJob < ActiveJob::Base
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ require_relative './base_job'
2
+
3
+ module CanvasSync
4
+ module JobBatches
5
+ class ConcurrentBatchJob < BaseJob
6
+ def perform(sub_jobs, context: nil)
7
+ Batch.new.tap do |b|
8
+ b.description = "Concurrent Batch Root"
9
+ b.context = context
10
+ b.jobs do
11
+ sub_jobs.each do |j|
12
+ ChainBuilder.enqueue_job(j)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,75 @@
1
+ require_relative './base_job'
2
+
3
+ module CanvasSync
4
+ module JobBatches
5
+ class SerialBatchJob < BaseJob
6
+ def perform(sub_jobs, context: nil)
7
+ serial_id = SecureRandom.urlsafe_base64(10)
8
+
9
+ root_batch = Batch.new
10
+
11
+ Batch.redis do |r|
12
+ r.multi do
13
+ mapped_sub_jobs = sub_jobs.map do |j|
14
+ j = ActiveJob::Arguments.serialize([j])
15
+ JSON.unparse(j)
16
+ end
17
+ r.hset("SERBID-#{serial_id}", "root_bid", root_batch.bid)
18
+ r.expire("SERBID-#{serial_id}", Batch::BID_EXPIRE_TTL)
19
+ r.rpush("SERBID-#{serial_id}-jobs", mapped_sub_jobs)
20
+ r.expire("SERBID-#{serial_id}-jobs", Batch::BID_EXPIRE_TTL)
21
+ end
22
+ end
23
+
24
+ root_batch.description = "Serial Batch Root (#{serial_id})"
25
+ root_batch.allow_context_changes = true
26
+ root_batch.context = context
27
+ root_batch.on(:success, "#{self.class.to_s}.cleanup_redis", serial_batch_id: serial_id)
28
+ root_batch.jobs do
29
+ self.class.perform_next_sequence_job(serial_id)
30
+ end
31
+ end
32
+
33
+ def self.cleanup_redis(status, options)
34
+ serial_id = options['serial_batch_id']
35
+ Batch.redis do |r|
36
+ r.del(
37
+ "SERBID-#{serial_id}",
38
+ "SERBID-#{serial_id}-jobs",
39
+ )
40
+ end
41
+ end
42
+
43
+ def self.job_succeeded_callback(status, options)
44
+ serial_id = options['serial_batch_id']
45
+ perform_next_sequence_job(serial_id)
46
+ end
47
+
48
+ protected
49
+
50
+ def self.perform_next_sequence_job(serial_id)
51
+ root_bid, next_job_json = Batch.redis do |r|
52
+ r.multi do
53
+ r.hget("SERBID-#{serial_id}", "root_bid")
54
+ r.lpop("SERBID-#{serial_id}-jobs")
55
+ end
56
+ end
57
+
58
+ return unless next_job_json.present?
59
+
60
+ next_job = JSON.parse(next_job_json)
61
+ next_job = ActiveJob::Arguments.deserialize(next_job)[0]
62
+
63
+ Batch.new(root_bid).jobs do
64
+ Batch.new.tap do |batch|
65
+ batch.description = "Serial Batch Fiber (#{serial_id})"
66
+ batch.on(:success, "#{self.to_s}.job_succeeded_callback", serial_batch_id: serial_id)
67
+ batch.jobs do
68
+ ChainBuilder.enqueue_job(next_job)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,93 @@
1
+ begin
2
+ require 'sidekiq/batch'
3
+ rescue LoadError
4
+ end
5
+
6
+ module CanvasSync
7
+ module JobBatches
8
+ module Sidekiq
9
+ module WorkerExtension
10
+ def bid
11
+ Thread.current[:batch].bid
12
+ end
13
+
14
+ def batch
15
+ Thread.current[:batch]
16
+ end
17
+
18
+ def valid_within_batch?
19
+ batch.valid?
20
+ end
21
+ end
22
+
23
+ class ClientMiddleware
24
+ def call(_worker, msg, _queue, _redis_pool = nil)
25
+ if (batch = Thread.current[:batch])
26
+ batch.increment_job_queue(msg['jid']) if (msg[:bid] = batch.bid)
27
+ end
28
+ yield
29
+ end
30
+ end
31
+
32
+ class ServerMiddleware
33
+ def call(_worker, msg, _queue)
34
+ if (bid = msg['bid'])
35
+ begin
36
+ Thread.current[:batch] = Batch.new(bid)
37
+ yield
38
+ Thread.current[:batch] = nil
39
+ Batch.process_successful_job(bid, msg['jid'])
40
+ rescue
41
+ Batch.process_failed_job(bid, msg['jid'])
42
+ raise
43
+ ensure
44
+ Thread.current[:batch] = nil
45
+ end
46
+ else
47
+ yield
48
+ end
49
+ end
50
+ end
51
+
52
+ def self.configure
53
+ if defined?(::Sidekiq::Batch) && ::Sidekiq::Batch != JobBatches::Batch
54
+ print "WARNING: Detected Sidekiq Pro or sidekiq-batch. CanvasSync JobBatches may not be fully compatible!"
55
+ end
56
+
57
+ ::Sidekiq.configure_client do |config|
58
+ config.client_middleware do |chain|
59
+ chain.remove ::Sidekiq::Batch::Middleware::ClientMiddleware if defined?(::Sidekiq::Batch::Middleware::ClientMiddleware)
60
+ chain.add JobBatches::Sidekiq::ClientMiddleware
61
+ end
62
+ end
63
+ ::Sidekiq.configure_server do |config|
64
+ config.client_middleware do |chain|
65
+ chain.remove ::Sidekiq::Batch::Middleware::ClientMiddleware if defined?(::Sidekiq::Batch::Middleware::ClientMiddleware)
66
+ chain.add JobBatches::Sidekiq::ClientMiddleware
67
+ end
68
+
69
+ config.server_middleware do |chain|
70
+ chain.remove ::Sidekiq::Batch::Middleware::ServerMiddleware if defined?(::Sidekiq::Batch::Middleware::ServerMiddleware)
71
+ chain.add JobBatches::Sidekiq::ServerMiddleware
72
+ end
73
+
74
+ config.death_handlers << ->(job, ex) do
75
+ return unless job['bid'].present?
76
+
77
+ if defined?(::Apartment)
78
+ ::Apartment::Tenant.switch(job['apartment'] || 'public') do
79
+ Sidekiq::Batch.process_dead_job(job['bid'], job['jid'])
80
+ end
81
+ else
82
+ Sidekiq::Batch.process_dead_job(job['bid'], job['jid'])
83
+ end
84
+ end
85
+ end
86
+ ::Sidekiq.const_set(:Batch, CanvasSync::JobBatches::Batch)
87
+ # This alias helps apartment-sidekiq set itself up correctly
88
+ ::Sidekiq::Batch.const_set(:Server, CanvasSync::JobBatches::Sidekiq::ServerMiddleware)
89
+ ::Sidekiq::Worker.send(:include, JobBatches::Sidekiq::WorkerExtension)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,83 @@
1
+ module CanvasSync
2
+ module JobBatches
3
+ class Batch
4
+ class Status
5
+ attr_reader :bid
6
+
7
+ def initialize(bid)
8
+ @bid = bid
9
+ end
10
+
11
+ def join
12
+ raise "Not supported"
13
+ end
14
+
15
+ def pending
16
+ Batch.redis { |r| r.hget("BID-#{bid}", 'pending') }.to_i
17
+ end
18
+
19
+ def failures
20
+ Batch.redis { |r| r.scard("BID-#{bid}-failed") }.to_i
21
+ end
22
+
23
+ def created_at
24
+ Batch.redis { |r| r.hget("BID-#{bid}", 'created_at') }
25
+ end
26
+
27
+ def total
28
+ Batch.redis { |r| r.hget("BID-#{bid}", 'total') }.to_i
29
+ end
30
+
31
+ def parent_bid
32
+ Batch.redis { |r| r.hget("BID-#{bid}", "parent_bid") }
33
+ end
34
+
35
+ def failure_info
36
+ Batch.redis { |r| r.smembers("BID-#{bid}-failed") } || []
37
+ end
38
+
39
+ def complete?
40
+ 'true' == Batch.redis { |r| r.hget("BID-#{bid}", 'complete') }
41
+ end
42
+
43
+ def success?
44
+ 'true' == Batch.redis { |r| r.hget("BID-#{bid}", 'success') }
45
+ end
46
+
47
+ def child_count
48
+ Batch.redis { |r| r.hget("BID-#{bid}", 'children') }.to_i
49
+ end
50
+
51
+ def completed_children_count
52
+ Batch.redis { |r| r.scard("BID-#{bid}-batches-complete") }.to_i
53
+ end
54
+
55
+ def successful_children_count
56
+ Batch.redis { |r| r.scard("BID-#{bid}-batches-success") }.to_i
57
+ end
58
+
59
+ def failed_children_count
60
+ Batch.redis { |r| r.scard("BID-#{bid}-batches-failed") }.to_i
61
+ end
62
+
63
+ def data
64
+ {
65
+ bid: bid,
66
+ total: total,
67
+ failures: failures,
68
+ pending: pending,
69
+ created_at: created_at,
70
+ complete: complete?,
71
+ success: success?,
72
+ failure_info: failure_info,
73
+ parent_bid: parent_bid,
74
+ child_count: child_count,
75
+ completed_children_count: completed_children_count,
76
+ successful_children_count: successful_children_count,
77
+ failed_children_count: failed_children_count,
78
+ }
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,35 @@
1
+ module CanvasSync
2
+ module Jobs
3
+ class BeginSyncChainJob < CanvasSync::Job
4
+ def perform(chain_definition, globals = {})
5
+ if !globals[:updated_after].present? || globals[:updated_after] == true
6
+ last_batch = SyncBatch.where(status: 'completed').last
7
+ globals[:updated_after] = last_batch&.started_at&.iso8601
8
+ end
9
+
10
+ sync_batch = SyncBatch.create!(
11
+ started_at: DateTime.now,
12
+ status: 'pending',
13
+ )
14
+
15
+ JobBatches::Batch.new.tap do |b|
16
+ b.description = "CanvasSync Root Batch"
17
+ b.on(:complete, "#{self.class.to_s}.batch_completed", sync_batch_id: sync_batch.id)
18
+ b.on(:success, "#{self.class.to_s}.batch_completed", sync_batch_id: sync_batch.id)
19
+ b.context = globals
20
+ b.jobs do
21
+ JobBatches::SerialBatchJob.perform_now(chain_definition)
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.batch_completed(status, options)
27
+ sbatch = SyncBatch.find(options['sync_batch_id'])
28
+ sbatch.update!(
29
+ status: status.success? ? 'completed' : 'failed',
30
+ completed_at: DateTime.now,
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -4,21 +4,19 @@ 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
- # @param job_chain [Hash]
8
7
  # @param report_name [Hash] e.g., 'provisioning_csv'
9
8
  # @param report_id [Integer]
10
9
  # @param processor [String] a stringified report processor class name
11
10
  # @param options [Hash] hash of options that will be passed to the job processor
12
11
  # @return [nil]
13
- def perform(job_chain, report_name, report_id, processor, options) # rubocop:disable Metrics/AbcSize
14
- account_id = options[:account_id] || job_chain[:global_options][:account_id] || "self"
15
- report_status = CanvasSync.get_canvas_sync_client(job_chain[:global_options])
12
+ def perform(report_name, report_id, processor, options) # rubocop:disable Metrics/AbcSize
13
+ account_id = options[:account_id] || batch_context[:account_id] || "self"
14
+ report_status = CanvasSync.get_canvas_sync_client(batch_context)
16
15
  .report_status(account_id, report_name, report_id)
17
16
 
18
17
  case report_status["status"].downcase
19
18
  when "complete"
20
19
  CanvasSync::Jobs::ReportProcessorJob.perform_later(
21
- job_chain,
22
20
  report_name,
23
21
  report_status["attachment"]["url"],
24
22
  processor,
@@ -33,7 +31,6 @@ module CanvasSync
33
31
  CanvasSync::Jobs::ReportChecker
34
32
  .set(wait: report_checker_wait_time)
35
33
  .perform_later(
36
- job_chain,
37
34
  report_name,
38
35
  report_id,
39
36
  processor,
@@ -6,22 +6,19 @@ module CanvasSync
6
6
  # download the report, and then pass the file path and options into the
7
7
  # process method on the processor.
8
8
  class ReportProcessorJob < CanvasSync::Job
9
- # @param job_chain [Hash]
10
9
  # @param report_name [Hash] e.g., 'provisioning_csv'
11
10
  # @param report_url [String]
12
11
  # @param processor [String] a stringified report processor class name
13
12
  # @param options [Hash] hash of options that will be passed to the job processor
14
13
  # @return [nil]
15
- def perform(job_chain, report_name, report_url, processor, options, report_id)
14
+ def perform(report_name, report_url, processor, options, report_id)
16
15
  @job_log.update_attributes(job_class: processor)
17
16
  download(report_name, report_url) do |file_path|
18
- options = job_chain[:global_options].merge(options).merge({
17
+ options = batch_context.merge(options).merge({
19
18
  report_processor_job_id: @job_log.job_id
20
19
  })
21
20
  processor.constantize.process(file_path, options, report_id)
22
21
  end
23
-
24
- CanvasSync.invoke_next(job_chain)
25
22
  end
26
23
 
27
24
  private
@@ -2,7 +2,6 @@ module CanvasSync
2
2
  module Jobs
3
3
  # Starts a Canvas report and enqueues a ReportChecker
4
4
  class ReportStarter < CanvasSync::Job
5
- # @param job_chain [Hash]
6
5
  # @param report_name [Hash] e.g., 'provisioning_csv'
7
6
  # @param report_params [Hash] The Canvas report parameters
8
7
  # @param processor [String] a stringified report processor class name
@@ -10,31 +9,39 @@ module CanvasSync
10
9
  # @param allow_redownloads [Boolean] whether you want the job_chain to cache this report,
11
10
  # so that any later jobs in the chain will use the same generated report
12
11
  # @return [nil]
13
- def perform(job_chain, report_name, report_params, processor, options, allow_redownloads: false)
14
- account_id = options[:account_id] || job_chain[:global_options][:account_id] || "self"
15
- options[:sync_start_time] = DateTime.now.utc.iso8601
16
- report_id = if allow_redownloads
17
- get_cached_report(job_chain, account_id, report_name, report_params)
18
- else
19
- start_report(job_chain, account_id, report_name, report_params)
20
- end
12
+ def perform(report_name, report_params, processor, options, allow_redownloads: false)
13
+ account_id = options[:account_id] || batch_context[:account_id] || "self"
21
14
 
22
- CanvasSync::Jobs::ReportChecker.set(wait: report_checker_wait_time).perform_later(
23
- job_chain,
24
- report_name,
25
- report_id,
26
- processor,
27
- options,
28
- )
15
+ report_id = start_report(account_id, report_name, report_params)
16
+ # TODO: Restore report caching support (does nayone actually use it?)
17
+ # report_id = if allow_redownloads
18
+ # get_cached_report(account_id, report_name, report_params)
19
+ # else
20
+ # start_report(account_id, report_name, report_params)
21
+ # end
22
+
23
+ batch = JobBatches::Batch.new
24
+ batch.description = "CanvasSync #{report_name} Fiber"
25
+ batch.jobs do
26
+ CanvasSync::Jobs::ReportChecker.set(wait: report_checker_wait_time).perform_later(
27
+ report_name,
28
+ report_id,
29
+ processor,
30
+ options,
31
+ )
32
+ end
29
33
  end
30
34
 
31
35
  protected
32
36
 
33
- def merge_report_params(job_chain, options={}, params={}, term_scope: true)
34
- term_scope = job_chain[:global_options][:canvas_term_id] if term_scope == true
37
+ def merge_report_params(options={}, params={}, term_scope: true)
38
+ term_scope = batch_context[:canvas_term_id] if term_scope == true
35
39
  if term_scope.present?
36
40
  params[:enrollment_term_id] = term_scope
37
41
  end
42
+ if (updated_after = batch_context[:updated_after]).present?
43
+ params[:updated_after] = updated_after
44
+ end
38
45
  params.merge!(options[:report_params]) if options[:report_params].present?
39
46
  { parameters: params }
40
47
  end
@@ -42,6 +49,7 @@ module CanvasSync
42
49
  private
43
50
 
44
51
  def get_cached_report(job_chain, account_id, report_name, report_params)
52
+ # TODO: job_chain[:global_options] is no longer available and batch_context won't work for this
45
53
  if job_chain[:global_options][report_name].present?
46
54
  job_chain[:global_options][report_name]
47
55
  else
@@ -51,8 +59,8 @@ module CanvasSync
51
59
  end
52
60
  end
53
61
 
54
- def start_report(job_chain, account_id, report_name, report_params)
55
- report = CanvasSync.get_canvas_sync_client(job_chain[:global_options])
62
+ def start_report(account_id, report_name, report_params)
63
+ report = CanvasSync.get_canvas_sync_client(batch_context)
56
64
  .start_report(account_id, report_name, report_params)
57
65
  report["id"]
58
66
  end