canvas_sync 0.16.5 → 0.17.0.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 (80) hide show
  1. checksums.yaml +5 -5
  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/importers/bulk_importer.rb +4 -7
  6. data/lib/canvas_sync/job.rb +4 -10
  7. data/lib/canvas_sync/job_batches/batch.rb +399 -0
  8. data/lib/canvas_sync/job_batches/batch_aware_job.rb +62 -0
  9. data/lib/canvas_sync/job_batches/callback.rb +153 -0
  10. data/lib/canvas_sync/job_batches/chain_builder.rb +203 -0
  11. data/lib/canvas_sync/job_batches/context_hash.rb +147 -0
  12. data/lib/canvas_sync/job_batches/jobs/base_job.rb +7 -0
  13. data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +18 -0
  14. data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +73 -0
  15. data/lib/canvas_sync/job_batches/sidekiq.rb +91 -0
  16. data/lib/canvas_sync/job_batches/status.rb +63 -0
  17. data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +34 -0
  18. data/lib/canvas_sync/jobs/report_checker.rb +3 -6
  19. data/lib/canvas_sync/jobs/report_processor_job.rb +2 -5
  20. data/lib/canvas_sync/jobs/report_starter.rb +28 -20
  21. data/lib/canvas_sync/jobs/sync_accounts_job.rb +3 -5
  22. data/lib/canvas_sync/jobs/sync_admins_job.rb +2 -4
  23. data/lib/canvas_sync/jobs/sync_assignment_groups_job.rb +2 -4
  24. data/lib/canvas_sync/jobs/sync_assignments_job.rb +2 -4
  25. data/lib/canvas_sync/jobs/sync_context_module_items_job.rb +2 -4
  26. data/lib/canvas_sync/jobs/sync_context_modules_job.rb +2 -4
  27. data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +4 -31
  28. data/lib/canvas_sync/jobs/sync_roles_job.rb +2 -5
  29. data/lib/canvas_sync/jobs/sync_simple_table_job.rb +11 -32
  30. data/lib/canvas_sync/jobs/sync_submissions_job.rb +2 -4
  31. data/lib/canvas_sync/jobs/sync_terms_job.rb +22 -7
  32. data/lib/canvas_sync/processors/assignment_groups_processor.rb +2 -3
  33. data/lib/canvas_sync/processors/assignments_processor.rb +2 -3
  34. data/lib/canvas_sync/processors/context_module_items_processor.rb +2 -3
  35. data/lib/canvas_sync/processors/context_modules_processor.rb +2 -3
  36. data/lib/canvas_sync/processors/normal_processor.rb +1 -2
  37. data/lib/canvas_sync/processors/provisioning_report_processor.rb +2 -10
  38. data/lib/canvas_sync/processors/submissions_processor.rb +2 -3
  39. data/lib/canvas_sync/version.rb +1 -1
  40. data/lib/canvas_sync.rb +34 -97
  41. data/spec/canvas_sync/canvas_sync_spec.rb +126 -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 +363 -0
  60. data/spec/job_batching/callback_spec.rb +38 -0
  61. data/spec/job_batching/flow_spec.rb +91 -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 +10 -0
  72. metadata +91 -23
  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
  76. data/spec/dummy/db/test.sqlite3 +0 -0
  77. data/spec/dummy/log/development.log +0 -1248
  78. data/spec/dummy/log/test.log +0 -43258
  79. data/spec/support/fixtures/reports/provisioning_csv_unzipped/courses.csv +0 -3
  80. data/spec/support/fixtures/reports/provisioning_csv_unzipped/users.csv +0 -4
@@ -0,0 +1,399 @@
1
+
2
+ begin
3
+ require 'sidekiq'
4
+ rescue LoadError
5
+ end
6
+
7
+ require_relative './batch_aware_job'
8
+ require_relative "./callback"
9
+ require_relative "./context_hash"
10
+ require_relative "./status"
11
+ Dir[File.dirname(__FILE__) + "/jobs/*.rb"].each { |file| require file }
12
+ require_relative "./chain_builder"
13
+
14
+ # Implement Job Batching similar to Sidekiq::Batch. Supports ActiveJob and Sidekiq, or a mix thereof.
15
+ # Much of this code is modifed/extended from https://github.com/breamware/sidekiq-batch
16
+
17
+ module CanvasSync
18
+ module JobBatches
19
+ 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
29
+
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
38
+
39
+ delegate :redis, to: :class
40
+
41
+ BID_EXPIRE_TTL = 2_592_000
42
+
43
+ attr_reader :bid
44
+
45
+ def initialize(existing_bid = nil)
46
+ @bid = existing_bid || SecureRandom.urlsafe_base64(10)
47
+ @existing = !(!existing_bid || existing_bid.empty?) # Basically existing_bid.present?
48
+ @initialized = false
49
+ @bidkey = "BID-" + @bid.to_s
50
+ @pending_attrs = {}
51
+ @ready_to_queue = []
52
+ self.created_at = Time.now.utc.to_f unless @existing
53
+ end
54
+
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
60
+
61
+ def context
62
+ return @context if defined?(@context)
63
+
64
+ if (@initialized || @existing)
65
+ @context = ContextHash.new(bid)
66
+ else
67
+ @context = ContextHash.new(bid, {})
68
+ end
69
+ end
70
+
71
+ def context=(value)
72
+ raise "context is read-only once the batch has been started" if (@initialized || @existing) # && !allow_context_changes
73
+ raise "context must be a Hash" unless value.is_a?(Hash) || value.nil?
74
+ return nil if value.nil? && @context.nil?
75
+
76
+ value = {} if value.nil?
77
+ value = value.local if value.is_a?(ContextHash)
78
+
79
+ @context ||= ContextHash.new(bid, {})
80
+ @context.set_local(value)
81
+ # persist_bid_attr('context', JSON.unparse(@context.local))
82
+ end
83
+
84
+ def save_context_changes
85
+ @context&.save!
86
+ end
87
+
88
+ def on(event, callback, options = {})
89
+ return unless Callback::VALID_CALLBACKS.include?(event.to_s)
90
+ callback_key = "#{@bidkey}-callbacks-#{event}"
91
+ redis do |r|
92
+ r.multi do
93
+ r.sadd(callback_key, JSON.unparse({
94
+ callback: callback,
95
+ opts: options
96
+ }))
97
+ r.expire(callback_key, BID_EXPIRE_TTL)
98
+ end
99
+ end
100
+ end
101
+
102
+ def jobs
103
+ raise NoBlockGivenError unless block_given?
104
+
105
+ if !@existing && !@initialized
106
+ parent_bid = Thread.current[:batch]&.bid
107
+
108
+ redis do |r|
109
+ r.multi do
110
+ r.hset(@bidkey, "parent_bid", parent_bid.to_s) if parent_bid
111
+ r.expire(@bidkey, BID_EXPIRE_TTL)
112
+ end
113
+ end
114
+
115
+ flush_pending_attrs
116
+ @context&.save!
117
+
118
+ @initialized = true
119
+ end
120
+
121
+ @ready_to_queue = []
122
+
123
+ begin
124
+ parent = Thread.current[:batch]
125
+ Thread.current[:batch] = self
126
+ yield
127
+ ensure
128
+ Thread.current[:batch] = parent
129
+ end
130
+
131
+ redis do |r|
132
+ r.multi do
133
+ if parent_bid
134
+ r.hincrby("BID-#{parent_bid}", "children", 1)
135
+ r.hincrby("BID-#{parent_bid}", "total", @ready_to_queue.size)
136
+ r.expire("BID-#{parent_bid}", BID_EXPIRE_TTL)
137
+ end
138
+
139
+ r.hincrby(@bidkey, "pending", @ready_to_queue.size)
140
+ r.hincrby(@bidkey, "total", @ready_to_queue.size)
141
+ r.expire(@bidkey, BID_EXPIRE_TTL)
142
+
143
+ if @ready_to_queue.size > 0
144
+ r.sadd(@bidkey + "-jids", @ready_to_queue)
145
+ r.expire(@bidkey + "-jids", BID_EXPIRE_TTL)
146
+ end
147
+ end
148
+ end
149
+
150
+ @ready_to_queue
151
+ end
152
+
153
+ def increment_job_queue(jid)
154
+ @ready_to_queue << jid
155
+ end
156
+
157
+ def invalidate_all
158
+ redis do |r|
159
+ r.setex("invalidated-bid-#{bid}", BID_EXPIRE_TTL, 1)
160
+ end
161
+ end
162
+
163
+ def parent_bid
164
+ redis do |r|
165
+ r.hget(@bidkey, "parent_bid")
166
+ end
167
+ end
168
+
169
+ def parent
170
+ if parent_bid
171
+ Batch.new(parent_bid)
172
+ end
173
+ end
174
+
175
+ def valid?(batch = self)
176
+ valid = !redis { |r| r.exists("invalidated-bid-#{batch.bid}") }
177
+ batch.parent ? valid && valid?(batch.parent) : valid
178
+ end
179
+
180
+ # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
181
+ def self.without_batch
182
+ parent = Thread.current[:batch]
183
+ Thread.current[:batch] = nil
184
+ yield
185
+ ensure
186
+ Thread.current[:batch] = parent
187
+ end
188
+
189
+ private
190
+
191
+ def persist_bid_attr(attribute, value)
192
+ if @initialized || @existing
193
+ redis do |r|
194
+ r.multi do
195
+ r.hset(@bidkey, attribute, value)
196
+ r.expire(@bidkey, BID_EXPIRE_TTL)
197
+ end
198
+ end
199
+ else
200
+ @pending_attrs[attribute] = value
201
+ end
202
+ end
203
+
204
+ def read_bid_attr(attribute)
205
+ redis do |r|
206
+ r.hget(@bidkey, attribute)
207
+ end
208
+ end
209
+
210
+ def flush_pending_attrs
211
+ redis do |r|
212
+ r.mapped_hmset(@bidkey, @pending_attrs)
213
+ end
214
+ @pending_attrs = {}
215
+ end
216
+
217
+ class << self
218
+ def process_failed_job(bid, jid)
219
+ _, pending, failed, children, complete, parent_bid = redis do |r|
220
+ r.multi do
221
+ r.sadd("BID-#{bid}-failed", jid)
222
+
223
+ r.hincrby("BID-#{bid}", "pending", 0)
224
+ r.scard("BID-#{bid}-failed")
225
+ r.hincrby("BID-#{bid}", "children", 0)
226
+ r.scard("BID-#{bid}-complete")
227
+ r.hget("BID-#{bid}", "parent_bid")
228
+
229
+ r.expire("BID-#{bid}-failed", BID_EXPIRE_TTL)
230
+ end
231
+ end
232
+
233
+ # if the batch failed, and has a parent, update the parent to show one pending and failed job
234
+ if parent_bid
235
+ redis do |r|
236
+ r.multi do
237
+ r.hincrby("BID-#{parent_bid}", "pending", 1)
238
+ r.sadd("BID-#{parent_bid}-failed", jid)
239
+ r.expire("BID-#{parent_bid}-failed", BID_EXPIRE_TTL)
240
+ end
241
+ end
242
+ end
243
+
244
+ if pending.to_i == failed.to_i && children == complete
245
+ enqueue_callbacks(:complete, bid)
246
+ end
247
+ end
248
+
249
+ def process_dead_job(bid, jid)
250
+ _, failed, children, complete, parent_bid = redis do |r|
251
+ r.multi do
252
+ r.sadd("BID-#{bid}-dead", jid)
253
+
254
+ r.scard("BID-#{bid}-dead")
255
+ r.hincrby("BID-#{bid}", "children", 0)
256
+ r.scard("BID-#{bid}-complete")
257
+ r.hget("BID-#{bid}", "parent_bid")
258
+
259
+ r.expire("BID-#{bid}-dead", BID_EXPIRE_TTL)
260
+ end
261
+ end
262
+
263
+ if parent_bid
264
+ redis do |r|
265
+ r.multi do
266
+ r.sadd("BID-#{parent_bid}-dead", jid)
267
+ r.expire("BID-#{parent_bid}-dead", BID_EXPIRE_TTL)
268
+ end
269
+ end
270
+ end
271
+
272
+ enqueue_callbacks(:dead, bid)
273
+ end
274
+
275
+ def process_successful_job(bid, jid)
276
+ failed, pending, children, complete, success, total, parent_bid = redis do |r|
277
+ r.multi do
278
+ r.scard("BID-#{bid}-failed")
279
+ r.hincrby("BID-#{bid}", "pending", -1)
280
+ r.hincrby("BID-#{bid}", "children", 0)
281
+ r.scard("BID-#{bid}-complete")
282
+ r.scard("BID-#{bid}-success")
283
+ r.hget("BID-#{bid}", "total")
284
+ r.hget("BID-#{bid}", "parent_bid")
285
+
286
+ r.srem("BID-#{bid}-failed", jid)
287
+ r.srem("BID-#{bid}-jids", jid)
288
+ r.expire("BID-#{bid}", BID_EXPIRE_TTL)
289
+ end
290
+ end
291
+
292
+ all_success = pending.to_i.zero? && children == success
293
+ # if complete or successfull call complete callback (the complete callback may then call successful)
294
+ if (pending.to_i == failed.to_i && children == complete) || all_success
295
+ enqueue_callbacks(:complete, bid)
296
+ enqueue_callbacks(:success, bid) if all_success
297
+ end
298
+ end
299
+
300
+ def enqueue_callbacks(event, bid)
301
+ batch_key = "BID-#{bid}"
302
+ callback_key = "#{batch_key}-callbacks-#{event}"
303
+ already_processed, _, callbacks, queue, parent_bid, callback_batch = redis do |r|
304
+ r.multi do
305
+ r.hget(batch_key, event)
306
+ r.hset(batch_key, event, true)
307
+ r.smembers(callback_key)
308
+ r.hget(batch_key, "callback_queue")
309
+ r.hget(batch_key, "parent_bid")
310
+ r.hget(batch_key, "callback_batch")
311
+ end
312
+ end
313
+
314
+ return if already_processed == 'true'
315
+
316
+ queue ||= "default"
317
+ parent_bid = !parent_bid || parent_bid.empty? ? nil : parent_bid # Basically parent_bid.blank?
318
+ callback_args = callbacks.reduce([]) do |memo, jcb|
319
+ cb = JSON.load(jcb)
320
+ memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
321
+ end
322
+
323
+ opts = {"bid" => bid, "event" => event}
324
+
325
+ # Run callback batch finalize synchronously
326
+ if callback_batch
327
+ # Extract opts from cb_args or use current
328
+ # Pass in stored event as callback finalize is processed on complete event
329
+ cb_opts = callback_args.first&.at(2) || opts
330
+
331
+ logger.debug {"Run callback batch bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
332
+ # Finalize now
333
+ finalizer = Batch::Callback::Finalize.new
334
+ status = Status.new bid
335
+ finalizer.dispatch(status, cb_opts)
336
+
337
+ return
338
+ end
339
+
340
+ logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
341
+
342
+ if callback_args.empty?
343
+ # Finalize now
344
+ finalizer = Batch::Callback::Finalize.new
345
+ status = Status.new bid
346
+ finalizer.dispatch(status, opts)
347
+ else
348
+ # Otherwise finalize in sub batch complete callback
349
+ cb_batch = self.new
350
+ cb_batch.callback_batch = true
351
+ logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
352
+ cb_batch.on(:complete, "#{Batch::Callback::Finalize.to_s}#dispatch", opts)
353
+ cb_batch.jobs do
354
+ push_callbacks callback_args, queue
355
+ end
356
+ end
357
+ end
358
+
359
+ def cleanup_redis(bid)
360
+ logger.debug {"Cleaning redis of batch #{bid}"}
361
+ redis do |r|
362
+ r.del(
363
+ "BID-#{bid}",
364
+ "BID-#{bid}-callbacks-complete",
365
+ "BID-#{bid}-callbacks-success",
366
+ "BID-#{bid}-failed",
367
+
368
+ "BID-#{bid}-success",
369
+ "BID-#{bid}-complete",
370
+ "BID-#{bid}-jids",
371
+ )
372
+ end
373
+ end
374
+
375
+ def redis(*args, &blk)
376
+ defined?(::Sidekiq) ? ::Sidekiq.redis(*args, &blk) : nil # TODO
377
+ end
378
+
379
+ def logger
380
+ defined?(::Sidekiq) ? ::Sidekiq.logger : Rails.logger
381
+ end
382
+
383
+ private
384
+
385
+ def push_callbacks(args, queue)
386
+ Batch::Callback::Worker.enqueue_all(args, queue)
387
+ end
388
+ end
389
+ end
390
+
391
+ ActiveJob::Base.include BatchAwareJob
392
+ end
393
+ end
394
+
395
+ # Automatically integrate with Sidekiq if it is present.
396
+ if defined?(::Sidekiq)
397
+ require_relative './sidekiq'
398
+ CanvasSync::JobBatches::Sidekiq.configure
399
+ end
@@ -0,0 +1,62 @@
1
+ module CanvasSync
2
+ module JobBatches
3
+ module BatchAwareJob
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ around_perform do |job, block|
8
+ if (@bid) # This _must_ be @bid - not just bid
9
+ prev_batch = Thread.current[:batch]
10
+ begin
11
+ Thread.current[:batch] = Batch.new(@bid)
12
+ block.call
13
+ batch&.save_context_changes
14
+ Batch.process_successful_job(@bid, job_id)
15
+ rescue
16
+ Batch.process_failed_job(@bid, job_id)
17
+ raise
18
+ ensure
19
+ Thread.current[:batch] = prev_batch
20
+ end
21
+ else
22
+ block.call
23
+ end
24
+ end
25
+
26
+ around_enqueue do |job, block|
27
+ if (batch = Thread.current[:batch])
28
+ batch.increment_job_queue(job_id) if (@bid = batch.bid)
29
+ end
30
+ block.call
31
+ end
32
+ end
33
+
34
+ def bid
35
+ @bid || Thread.current[:batch]&.bid
36
+ end
37
+
38
+ def batch
39
+ Thread.current[:batch]
40
+ end
41
+
42
+ def batch_context
43
+ batch&.context || {}
44
+ end
45
+
46
+ def valid_within_batch?
47
+ batch.valid?
48
+ end
49
+
50
+ def serialize
51
+ super.tap do |data|
52
+ data['batch_id'] = @bid # This _must_ be @bid - not just bid
53
+ end
54
+ end
55
+
56
+ def deserialize(data)
57
+ super
58
+ @bid = data['batch_id']
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,153 @@
1
+ module CanvasSync
2
+ module JobBatches
3
+ class Batch
4
+ module Callback
5
+
6
+ VALID_CALLBACKS = %w[success complete dead].freeze
7
+
8
+ module CallbackWorkerCommon
9
+ def perform(definition, event, opts, bid, parent_bid)
10
+ return unless VALID_CALLBACKS.include?(event)
11
+
12
+ method = nil
13
+ target = :instance
14
+ clazz = definition
15
+ if clazz.is_a?(String)
16
+ if clazz.include?('#')
17
+ clazz, method = clazz.split("#")
18
+ elsif clazz.include?('.')
19
+ clazz, method = clazz.split(".")
20
+ target = :class
21
+ end
22
+ end
23
+
24
+ method ||= "on_#{event}"
25
+ status = Batch::Status.new(bid)
26
+
27
+ if clazz && object = Object.const_get(clazz)
28
+ target = target == :instance ? object.new : object
29
+ if target.respond_to?(method)
30
+ target.send(method, status, opts)
31
+ else
32
+ Batch.logger.warn("Invalid callback method #{definition} - #{target.to_s} does not respond to #{method}")
33
+ end
34
+ else
35
+ Batch.logger.warn("Invalid callback method #{definition} - Class #{clazz} not found")
36
+ end
37
+ end
38
+ end
39
+
40
+ class ActiveJobCallbackWorker < ActiveJob::Base
41
+ include CallbackWorkerCommon
42
+
43
+ def self.enqueue_all(args, queue)
44
+ args.each do |arg_set|
45
+ set(queue: queue).perform_later(*arg_set)
46
+ end
47
+ end
48
+ end
49
+
50
+ if defined?(::Sidekiq)
51
+ class SidekiqCallbackWorker
52
+ include ::Sidekiq::Worker
53
+ include CallbackWorkerCommon
54
+
55
+ def self.enqueue_all(args, queue)
56
+ return if args.empty?
57
+
58
+ ::Sidekiq::Client.push_bulk(
59
+ 'class' => self,
60
+ 'args' => args,
61
+ 'queue' => queue
62
+ )
63
+ end
64
+ end
65
+ Worker = SidekiqCallbackWorker
66
+ else
67
+ Worker = ActiveJobCallbackWorker
68
+ end
69
+
70
+ class Finalize
71
+ def dispatch status, opts
72
+ bid = opts["bid"]
73
+ callback_bid = status.bid
74
+ event = opts["event"].to_sym
75
+ callback_batch = bid != callback_bid
76
+
77
+ Batch.logger.debug {"Finalize #{event} batch id: #{opts["bid"]}, callback batch id: #{callback_bid} callback_batch #{callback_batch}"}
78
+
79
+ batch_status = Status.new bid
80
+ send(event, bid, batch_status, batch_status.parent_bid)
81
+
82
+ # Different events are run in different callback batches
83
+ Batch.cleanup_redis callback_bid if callback_batch
84
+ Batch.cleanup_redis bid if event == :success
85
+ end
86
+
87
+ def success(bid, status, parent_bid)
88
+ return unless parent_bid
89
+
90
+ _, _, success, _, complete, pending, children, failure = Batch.redis do |r|
91
+ r.multi do
92
+ r.sadd("BID-#{parent_bid}-success", bid)
93
+ r.expire("BID-#{parent_bid}-success", Batch::BID_EXPIRE_TTL)
94
+ r.scard("BID-#{parent_bid}-success")
95
+ r.sadd("BID-#{parent_bid}-complete", bid)
96
+ r.scard("BID-#{parent_bid}-complete")
97
+ r.hincrby("BID-#{parent_bid}", "pending", 0)
98
+ r.hincrby("BID-#{parent_bid}", "children", 0)
99
+ r.scard("BID-#{parent_bid}-failed")
100
+ end
101
+ end
102
+ # if job finished successfully and parent batch completed call parent complete callback
103
+ # Success callback is called after complete callback
104
+ if complete == children && pending == failure
105
+ Batch.logger.debug {"Finalize parent complete bid: #{parent_bid}"}
106
+ Batch.enqueue_callbacks(:complete, parent_bid)
107
+ end
108
+ end
109
+
110
+ def complete(bid, status, parent_bid)
111
+ pending, children, success = Batch.redis do |r|
112
+ r.multi do
113
+ r.hincrby("BID-#{bid}", "pending", 0)
114
+ r.hincrby("BID-#{bid}", "children", 0)
115
+ r.scard("BID-#{bid}-success")
116
+ end
117
+ end
118
+
119
+ # if we batch was successful run success callback
120
+ if pending.to_i.zero? && children == success
121
+ Batch.enqueue_callbacks(:success, bid)
122
+
123
+ elsif parent_bid
124
+ # if batch was not successfull check and see if its parent is complete
125
+ # if the parent is complete we trigger the complete callback
126
+ # We don't want to run this if the batch was successfull because the success
127
+ # callback may add more jobs to the parent batch
128
+
129
+ Batch.logger.debug {"Finalize parent complete bid: #{parent_bid}"}
130
+ _, complete, pending, children, failure = Batch.redis do |r|
131
+ r.multi do
132
+ r.sadd("BID-#{parent_bid}-complete", bid)
133
+ r.scard("BID-#{parent_bid}-complete")
134
+ r.hincrby("BID-#{parent_bid}", "pending", 0)
135
+ r.hincrby("BID-#{parent_bid}", "children", 0)
136
+ r.scard("BID-#{parent_bid}-failed")
137
+ end
138
+ end
139
+ if complete == children && pending == failure
140
+ Batch.enqueue_callbacks(:complete, parent_bid)
141
+ end
142
+ end
143
+ end
144
+
145
+ def cleanup_redis bid, callback_bid=nil
146
+ Batch.cleanup_redis bid
147
+ Batch.cleanup_redis callback_bid if callback_bid
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end