canvas_sync 0.17.0.beta14 → 0.17.3.beta1

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +58 -0
  3. data/lib/canvas_sync.rb +24 -5
  4. data/lib/canvas_sync/importers/bulk_importer.rb +7 -4
  5. data/lib/canvas_sync/job_batches/batch.rb +75 -95
  6. data/lib/canvas_sync/job_batches/callback.rb +19 -29
  7. data/lib/canvas_sync/job_batches/context_hash.rb +8 -5
  8. data/lib/canvas_sync/job_batches/hincr_max.lua +5 -0
  9. data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +99 -0
  10. data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +6 -65
  11. data/lib/canvas_sync/job_batches/pool.rb +209 -0
  12. data/lib/canvas_sync/job_batches/redis_model.rb +67 -0
  13. data/lib/canvas_sync/job_batches/redis_script.rb +163 -0
  14. data/lib/canvas_sync/job_batches/sidekiq.rb +22 -1
  15. data/lib/canvas_sync/job_batches/status.rb +0 -5
  16. data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +4 -2
  17. data/lib/canvas_sync/jobs/report_starter.rb +1 -0
  18. data/lib/canvas_sync/processors/assignment_groups_processor.rb +3 -2
  19. data/lib/canvas_sync/processors/assignments_processor.rb +3 -2
  20. data/lib/canvas_sync/processors/context_module_items_processor.rb +3 -2
  21. data/lib/canvas_sync/processors/context_modules_processor.rb +3 -2
  22. data/lib/canvas_sync/processors/normal_processor.rb +2 -1
  23. data/lib/canvas_sync/processors/provisioning_report_processor.rb +10 -2
  24. data/lib/canvas_sync/processors/submissions_processor.rb +3 -2
  25. data/lib/canvas_sync/version.rb +1 -1
  26. data/spec/dummy/log/test.log +67741 -0
  27. data/spec/job_batching/batch_aware_job_spec.rb +1 -0
  28. data/spec/job_batching/batch_spec.rb +72 -15
  29. data/spec/job_batching/callback_spec.rb +1 -1
  30. data/spec/job_batching/flow_spec.rb +0 -1
  31. data/spec/job_batching/integration/fail_then_succeed.rb +42 -0
  32. data/spec/job_batching/integration_helper.rb +6 -4
  33. data/spec/job_batching/sidekiq_spec.rb +1 -0
  34. data/spec/job_batching/status_spec.rb +1 -17
  35. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28f1b87e0b26bf68a13a9a1558d8c78746a4058d2fa8a822dc62c5b3b7ad92ea
4
- data.tar.gz: 0bbc9ec37d31e848a8ec4997dc91391720deb826b2a93b52528661193788ebe4
3
+ metadata.gz: a67693c73d6b2bd34ffd9cf6ac17038c4d226eab30e1795ebbbd6a40d03e1bff
4
+ data.tar.gz: 2d42669d68de6ef9242e3d956ff15c82bb18fc07ee5d9e94c5eb1ac283928599
5
5
  SHA512:
6
- metadata.gz: 1cf0b4e64133d94b2841e77399a0dc3a94de42e17832587af14144d9ed28fb1df9953df3486590473abbe9745a8b919c4f910155ff71abe8eb8cc373d0d2704a
7
- data.tar.gz: 166edeb5b97e5879b70eebbdf9a3f9848bc91de5a71c5cb6066860b4a6001a272faec24ddf78661d9a5322b3234d1dc69a214bf1e8e8d2c7c16cec6336f01e7a
6
+ metadata.gz: 8d820e38c40e77e5199ef07af41a475ad7a35d36198ee315cb552324b1159592e329d6401779ad218ed7a7c30abdbe7df2995ad26b132a1da8c33f0dea83a516
7
+ data.tar.gz: 035e631264ef58221f1163e60e4e8b4167a1930800c4e54bf143f148a0e56a2447d81fa0d8150f6d2ade81a9dfb5eebec07f826413cae0fd958543fb68fe9e0f
data/README.md CHANGED
@@ -246,6 +246,64 @@ A batch can be created using `Sidekiq::Batch` or `CanvasSync::JobBatching::Batch
246
246
 
247
247
  Also see `canvas_sync/jobs/begin_sync_chain_job`, `canvas_sync/Job_batches/jobs/serial_batch_job`, or `canvas_sync/Job_batches/jobs/concurrent_batch_job` for example usage.
248
248
 
249
+ Example:
250
+ ```ruby
251
+ batch = CanvasSync::JobBatches::Batch.new
252
+ batch.description = "Some Batch" # Optional, but can be useful for debugging
253
+
254
+ batch.on(:complete, "SomeClass.on_complete", kw_param: 1)
255
+ batch.on(:success, "SomeClass.on_success", some_param: 'foo')
256
+
257
+ # Add context to the batch. Can be accessed as batch_context on any jobs within the batch.
258
+ # Nested Batches will have their contexts merged
259
+ batch.context = {
260
+ some_value: 'blah',
261
+ }
262
+
263
+ batch.jobs do
264
+ # Enqueue jobs like normal
265
+ end
266
+ ```
267
+
268
+ #### Job Pools
269
+ A job pool is like a custom Sidekiq Queue. You can add jobs to it and it will empty itself out into one of the actual queues.
270
+ However, it adds some options for tweaking the logic:
271
+ - `concurrency` (default: `nil`) - Define how many jobs from the pool can run at once.
272
+ - `order` (default: `fifo`) - Define how the pool will empty itself
273
+ - `fifo` - First-In First-Out, a traditional queue
274
+ - `lifo` - Last-In First-Out
275
+ - `random` - Pluck and run jobs in random order
276
+ - `priority` - Execute jobs in a priority order (NB: Due to Redis, this priority-random, meaning that items with the same priority will be run in random order, not fifo)
277
+ - `clean_when_empty` (default: `true`) - Automatically clean the pool when it is empty
278
+ - `on_failed_job` (default `:wait`) - If a Job fails, should the pool `:continue` and still enqueue the next job or `:wait` for the job to succeed
279
+
280
+ Example:
281
+ ```ruby
282
+ pool = CanvasSync::JobBatches::Pool.new(concurrency: 4, order: :priority, clean_when_empty: false)
283
+ pool_id = pool.pid
284
+
285
+ # Add a job to the pool
286
+ pool << {
287
+ job: SomeJob, # The Class of a ActiveJob Job or Sidekiq Worker
288
+ parameters: [1, 2, 3], # Array of params to pass th e Job
289
+ priority: 100, # Only effective if order=:priority, higher is higher
290
+ }
291
+
292
+ # Add many jobs to the pool
293
+ pool.add_jobs([
294
+ {
295
+ job: SomeJob, # The Class of a ActiveJob Job or Sidekiq Worker
296
+ parameters: [1, 2, 3], # Array of params to pass th e Job
297
+ priority: 100, # Only effective if order=:priority, higher is higher
298
+ },
299
+ # ...
300
+ ])
301
+
302
+ # ...Later
303
+ CanvasSync::JobBatches::Pool.from_pid(pool_id).cleanup_redis
304
+
305
+ ```
306
+
249
307
  ## Legacy Support
250
308
 
251
309
  ### Legacy Mappings
@@ -42,6 +42,22 @@ module CanvasSync
42
42
  xlist
43
43
  ].freeze
44
44
 
45
+ SUPPORTED_TERM_SCOPE_MODELS = %w[
46
+ assignments
47
+ submissions
48
+ assignment_groups
49
+ context_modules
50
+ context_module_items
51
+ ].freeze
52
+
53
+ DEFAULT_TERM_SCOPE_MODELS = %w[
54
+ assignments
55
+ submissions
56
+ assignment_groups
57
+ context_modules
58
+ context_module_items
59
+ ].freeze
60
+
45
61
  SUPPORTED_LIVE_EVENTS = %w[
46
62
  course
47
63
  enrollment
@@ -109,6 +125,7 @@ module CanvasSync
109
125
  def default_provisioning_report_chain(
110
126
  models,
111
127
  term_scope: nil,
128
+ term_scoped_models: DEFAULT_TERM_SCOPE_MODELS,
112
129
  legacy_support: false,
113
130
  account_id: nil,
114
131
  updated_after: nil,
@@ -165,6 +182,10 @@ module CanvasSync
165
182
  try_add_model_job.call('roles')
166
183
  try_add_model_job.call('admins')
167
184
 
185
+ (SUPPORTED_TERM_SCOPE_MODELS - term_scoped_models).each do |mdl|
186
+ try_add_model_job.call(mdl)
187
+ end
188
+
168
189
  ###############################
169
190
  # Per-term provisioning jobs
170
191
  ###############################
@@ -174,11 +195,9 @@ module CanvasSync
174
195
  current_chain << per_term_chain
175
196
  current_chain = per_term_chain
176
197
 
177
- try_add_model_job.call('assignments')
178
- try_add_model_job.call('submissions')
179
- try_add_model_job.call('assignment_groups')
180
- try_add_model_job.call('context_modules')
181
- try_add_model_job.call('context_module_items')
198
+ term_scoped_models.each do |mdl|
199
+ try_add_model_job.call(mdl)
200
+ end
182
201
 
183
202
  current_chain.insert(
184
203
  generate_provisioning_jobs(models, options, only_split: ['users'])
@@ -64,13 +64,12 @@ module CanvasSync
64
64
  columns = columns.dup
65
65
 
66
66
  update_conditions = {
67
- condition: condition_sql(klass, columns),
67
+ condition: condition_sql(klass, columns, import_args[:sync_start_time]),
68
68
  columns: columns
69
69
  }
70
70
  update_conditions[:conflict_target] = conflict_target if conflict_target
71
71
 
72
72
  options = { validate: false, on_duplicate_key_update: update_conditions }.merge(import_args)
73
-
74
73
  options.delete(:on_duplicate_key_update) if options.key?(:on_duplicate_key_ignore)
75
74
  klass.import(columns, rows, options)
76
75
  end
@@ -85,10 +84,14 @@ module CanvasSync
85
84
  # started_at = Time.now
86
85
  # run_the_users_sync!
87
86
  # changed = User.where("updated_at >= ?", started_at)
88
- def self.condition_sql(klass, columns)
87
+ def self.condition_sql(klass, columns, report_start)
89
88
  columns_str = columns.map { |c| "#{klass.quoted_table_name}.#{c}" }.join(", ")
90
89
  excluded_str = columns.map { |c| "EXCLUDED.#{c}" }.join(", ")
91
- "(#{columns_str}) IS DISTINCT FROM (#{excluded_str})"
90
+ condition_sql = "(#{columns_str}) IS DISTINCT FROM (#{excluded_str})"
91
+ if klass.column_names.include?("updated_at") && report_start
92
+ condition_sql += " AND #{klass.quoted_table_name}.updated_at < '#{report_start}'"
93
+ end
94
+ condition_sql
92
95
  end
93
96
 
94
97
  def self.batch_size
@@ -4,10 +4,13 @@ begin
4
4
  rescue LoadError
5
5
  end
6
6
 
7
+ require_relative './redis_model'
8
+ require_relative './redis_script'
7
9
  require_relative './batch_aware_job'
8
10
  require_relative "./callback"
9
11
  require_relative "./context_hash"
10
12
  require_relative "./status"
13
+ require_relative "./pool"
11
14
  Dir[File.dirname(__FILE__) + "/jobs/*.rb"].each { |file| require file }
12
15
  require_relative "./chain_builder"
13
16
 
@@ -17,24 +20,9 @@ require_relative "./chain_builder"
17
20
  module CanvasSync
18
21
  module JobBatches
19
22
  class Batch
20
- class NoBlockGivenError < StandardError; end
21
-
22
- def self.batch_attr(key, read_only: true)
23
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
24
- def #{key}=(value)
25
- raise "#{key} is read-only once the batch has been started" if #{read_only.to_s} && (@initialized || @existing)
26
- @#{key} = value
27
- persist_bid_attr('#{key}', value)
28
- end
23
+ include RedisModel
29
24
 
30
- def #{key}
31
- return @#{key} if defined?(@#{key})
32
- if (@initialized || @existing)
33
- @#{key} = read_bid_attr('#{key}')
34
- end
35
- end
36
- RUBY
37
- end
25
+ class NoBlockGivenError < StandardError; end
38
26
 
39
27
  delegate :redis, to: :class
40
28
 
@@ -47,16 +35,15 @@ module CanvasSync
47
35
  @existing = !(!existing_bid || existing_bid.empty?) # Basically existing_bid.present?
48
36
  @initialized = false
49
37
  @bidkey = "BID-" + @bid.to_s
50
- @pending_attrs = {}
51
38
  @ready_to_queue = nil
52
39
  self.created_at = Time.now.utc.to_f unless @existing
53
40
  end
54
41
 
55
- batch_attr :description
56
- batch_attr :created_at
57
- batch_attr :callback_queue, read_only: false
58
- batch_attr :callback_batch, read_only: false
59
- batch_attr :allow_context_changes
42
+ redis_attr :description
43
+ redis_attr :created_at
44
+ redis_attr :callback_queue, read_only: false
45
+ redis_attr :callback_params, :json
46
+ redis_attr :allow_context_changes
60
47
 
61
48
  def context
62
49
  return @context if defined?(@context)
@@ -122,6 +109,8 @@ module CanvasSync
122
109
  @context&.save!
123
110
 
124
111
  @initialized = true
112
+ else
113
+ assert_batch_is_open
125
114
  end
126
115
 
127
116
  job_queue = @ready_to_queue = []
@@ -143,6 +132,7 @@ module CanvasSync
143
132
  if @ready_to_queue
144
133
  @ready_to_queue << jid
145
134
  else
135
+ assert_batch_is_open
146
136
  append_jobs([jid])
147
137
  end
148
138
  end
@@ -170,52 +160,41 @@ module CanvasSync
170
160
  batch.parent ? valid && valid?(batch.parent) : valid
171
161
  end
172
162
 
173
- # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
174
- def self.without_batch
163
+ def self.with_batch(batch)
164
+ batch = self.new(batch) if batch.is_a?(String)
175
165
  parent = Thread.current[:batch]
176
- Thread.current[:batch] = nil
166
+ Thread.current[:batch] = batch
177
167
  yield
178
168
  ensure
179
169
  Thread.current[:batch] = parent
180
170
  end
181
171
 
182
- private
183
-
184
- def persist_bid_attr(attribute, value)
185
- if @initialized || @existing
186
- redis do |r|
187
- r.multi do
188
- r.hset(@bidkey, attribute, value)
189
- r.expire(@bidkey, BID_EXPIRE_TTL)
190
- end
191
- end
192
- else
193
- @pending_attrs[attribute] = value
194
- end
172
+ # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
173
+ def self.without_batch(&blk)
174
+ with_batch(nil, &blk)
195
175
  end
196
176
 
197
- def read_bid_attr(attribute)
198
- redis do |r|
199
- r.hget(@bidkey, attribute)
200
- end
177
+ protected
178
+
179
+ def redis_key
180
+ @bidkey
201
181
  end
202
182
 
203
- def flush_pending_attrs
204
- redis do |r|
205
- r.mapped_hmset(@bidkey, @pending_attrs)
183
+ private
184
+
185
+ def assert_batch_is_open
186
+ unless defined?(@closed)
187
+ redis do |r|
188
+ @closed = r.hget(@bidkey, 'closed') == 'true'
189
+ end
206
190
  end
207
- @pending_attrs = {}
191
+ raise "Cannot add jobs to Batch #{} bid - it has already entered the callback-stage" if @closed
208
192
  end
209
193
 
210
194
  def append_jobs(jids, parent_bid = self.parent_bid)
211
195
  redis do |r|
212
196
  r.multi do
213
- if parent_bid
214
- r.hincrby("BID-#{parent_bid}", "total", jids.size)
215
- end
216
-
217
197
  r.hincrby(@bidkey, "pending", jids.size)
218
- r.hincrby(@bidkey, "total", jids.size)
219
198
  r.expire(@bidkey, BID_EXPIRE_TTL)
220
199
 
221
200
  if jids.size > 0
@@ -242,17 +221,6 @@ module CanvasSync
242
221
  end
243
222
  end
244
223
 
245
- # if the batch failed, and has a parent, update the parent to show one pending and failed job
246
- if parent_bid
247
- redis do |r|
248
- r.multi do
249
- r.hincrby("BID-#{parent_bid}", "pending", 1)
250
- r.sadd("BID-#{parent_bid}-failed", jid)
251
- r.expire("BID-#{parent_bid}-failed", BID_EXPIRE_TTL)
252
- end
253
- end
254
- end
255
-
256
224
  if pending.to_i == failed.to_i && children == complete
257
225
  enqueue_callbacks(:complete, bid)
258
226
  end
@@ -285,7 +253,7 @@ module CanvasSync
285
253
  end
286
254
 
287
255
  def process_successful_job(bid, jid)
288
- _, failed, pending, children, complete, success, total, parent_bid = redis do |r|
256
+ _, failed, pending, children, complete, success, parent_bid = redis do |r|
289
257
  r.multi do
290
258
  r.srem("BID-#{bid}-failed", jid)
291
259
 
@@ -294,7 +262,6 @@ module CanvasSync
294
262
  r.hincrby("BID-#{bid}", "children", 0)
295
263
  r.scard("BID-#{bid}-batches-complete")
296
264
  r.scard("BID-#{bid}-batches-success")
297
- r.hget("BID-#{bid}", "total")
298
265
  r.hget("BID-#{bid}", "parent_bid")
299
266
 
300
267
  r.srem("BID-#{bid}-jids", jid)
@@ -310,17 +277,34 @@ module CanvasSync
310
277
  end
311
278
  end
312
279
 
280
+ def possibly_enqueue_callbacks(bid, types: [:complete, :success])
281
+ pending_jobs, failed_jobs, total_batches, completed_batches, successful_batches = redis do |r|
282
+ r.multi do
283
+
284
+ end
285
+ end
286
+
287
+ if (types.include?(:complete)) && (pending_jobs.to_i == failed_jobs.to_i && total_batches.to_i == completed_batches.to_i)
288
+ enqueue_callbacks(:complete, bid)
289
+ end
290
+ if (types.include?(:success)) && (pending_jobs.to_i.zero? && total_batches.to_i == successful_batches.to_i)
291
+ enqueue_callbacks(:success, bid)
292
+ end
293
+ end
294
+
313
295
  def enqueue_callbacks(event, bid)
314
296
  batch_key = "BID-#{bid}"
315
297
  callback_key = "#{batch_key}-callbacks-#{event}"
316
- already_processed, _, callbacks, queue, parent_bid, callback_batch = redis do |r|
298
+ already_processed, _, _, callbacks, queue, parent_bid, callback_params = redis do |r|
299
+ return unless r.exists?(batch_key)
317
300
  r.multi do
318
301
  r.hget(batch_key, event)
302
+ r.hset(batch_key, "closed", true)
319
303
  r.hset(batch_key, event, true)
320
304
  r.smembers(callback_key)
321
305
  r.hget(batch_key, "callback_queue")
322
306
  r.hget(batch_key, "parent_bid")
323
- r.hget(batch_key, "callback_batch")
307
+ r.hget(batch_key, "callback_params")
324
308
  end
325
309
  end
326
310
 
@@ -328,6 +312,7 @@ module CanvasSync
328
312
 
329
313
  queue ||= "default"
330
314
  parent_bid = !parent_bid || parent_bid.empty? ? nil : parent_bid # Basically parent_bid.blank?
315
+ callback_params = JSON.parse(callback_params) if callback_params.present?
331
316
  callback_args = callbacks.reduce([]) do |memo, jcb|
332
317
  cb = JSON.load(jcb)
333
318
  memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
@@ -335,38 +320,32 @@ module CanvasSync
335
320
 
336
321
  opts = {"bid" => bid, "event" => event}
337
322
 
338
- # Run callback batch finalize synchronously
339
- if callback_batch
340
- # Extract opts from cb_args or use current
341
- # Pass in stored event as callback finalize is processed on complete event
342
- cb_opts = callback_args.first&.at(2) || opts
323
+ if callback_args.present? && !callback_params.present?
324
+ logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
343
325
 
344
- logger.debug {"Run callback batch bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
345
- # Finalize now
346
- finalizer = Batch::Callback::Finalize.new
347
- status = Status.new bid
348
- finalizer.dispatch(status, cb_opts)
326
+ with_batch(parent_bid) do
327
+ cb_batch = self.new
328
+ cb_batch.callback_params = {
329
+ for_bid: bid,
330
+ event: event,
331
+ }
332
+ opts['callback_bid'] = cb_batch.bid
349
333
 
350
- return
334
+ logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
335
+ cb_batch.jobs do
336
+ push_callbacks callback_args, queue
337
+ end
338
+ end
351
339
  end
352
340
 
353
- logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
354
-
355
- if callback_args.empty?
356
- # Finalize now
357
- finalizer = Batch::Callback::Finalize.new
358
- status = Status.new bid
359
- finalizer.dispatch(status, opts)
360
- else
361
- # Otherwise finalize in sub batch complete callback
362
- cb_batch = self.new
363
- cb_batch.callback_batch = true
364
- logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
365
- cb_batch.on(:complete, "#{Batch::Callback::Finalize.to_s}#dispatch", opts)
366
- cb_batch.jobs do
367
- push_callbacks callback_args, queue
368
- end
341
+ if callback_params.present?
342
+ opts['origin'] = callback_params
369
343
  end
344
+
345
+ logger.debug {"Run batch finalizer bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
346
+ finalizer = Batch::Callback::Finalize.new
347
+ status = Status.new bid
348
+ finalizer.dispatch(status, opts)
370
349
  end
371
350
 
372
351
  def cleanup_redis(bid)
@@ -381,6 +360,7 @@ module CanvasSync
381
360
  "BID-#{bid}-batches-success",
382
361
  "BID-#{bid}-batches-complete",
383
362
  "BID-#{bid}-batches-failed",
363
+ "BID-#{bid}-bids",
384
364
  "BID-#{bid}-jids",
385
365
  )
386
366
  end
@@ -397,7 +377,7 @@ module CanvasSync
397
377
  private
398
378
 
399
379
  def push_callbacks(args, queue)
400
- Batch::Callback::Worker.enqueue_all(args, queue)
380
+ Batch::Callback::worker_class.enqueue_all(args, queue)
401
381
  end
402
382
  end
403
383
  end