canvas_sync 0.17.2 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7e920904fe92285bd67cbf48a88e8aa6c03761dbb543eb9ee477058f3086c2a
4
- data.tar.gz: d2221d37737fde81de62a0f9d6d304cf059c30cc78f3be814c01849365257680
3
+ metadata.gz: a67693c73d6b2bd34ffd9cf6ac17038c4d226eab30e1795ebbbd6a40d03e1bff
4
+ data.tar.gz: 2d42669d68de6ef9242e3d956ff15c82bb18fc07ee5d9e94c5eb1ac283928599
5
5
  SHA512:
6
- metadata.gz: 3a988e7678bc3f0e6e33325171f7665cc016cf30d07aa299c69268339587433e69d7c11036d797358c4f563d21956b74a48bb7bd5cc656d86ba01edde5d85a3f
7
- data.tar.gz: 74f9f9ba4a6a6a59e1a7dbb2dba351cee464d12d74ef526784fa63d81e319656163ccb52fbca1f71a7af664148f6712e77513bbf4346e432bc9394fede53ed3f
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
@@ -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
@@ -2,6 +2,7 @@ module CanvasSync
2
2
  module JobBatches
3
3
  class Batch
4
4
  module Callback
5
+ mattr_accessor :worker_class
5
6
 
6
7
  VALID_CALLBACKS = %w[success complete dead].freeze
7
8
 
@@ -47,41 +48,28 @@ module CanvasSync
47
48
  end
48
49
  end
49
50
 
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
51
+ worker_class = ActiveJobCallbackWorker
69
52
 
70
53
  class Finalize
71
- def dispatch status, opts
54
+ def dispatch(status, opts)
55
+ is_callback_batch = opts['origin'].present?
56
+ has_callback_batch = opts['callback_bid'].present?
57
+
72
58
  bid = opts["bid"]
73
- callback_bid = status.bid
74
59
  event = opts["event"].to_sym
75
- callback_batch = bid != callback_bid
76
60
 
77
- Batch.logger.debug {"Finalize #{event} batch id: #{opts["bid"]}, callback batch id: #{callback_bid} callback_batch #{callback_batch}"}
61
+ Batch.logger.debug {"Finalize #{event} batch id: #{opts["bid"]}, callback batch id: #{callback_bid} callback_batch #{is_callback_batch}"}
78
62
 
79
63
  batch_status = Status.new bid
80
64
  send(event, bid, batch_status, batch_status.parent_bid)
81
65
 
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
66
+ if event == :success && !has_callback_batch
67
+ Batch.cleanup_redis(bid)
68
+ end
69
+
70
+ if event == :success && is_callback_batch && opts['origin']['event'].to_sym == :success
71
+ Batch.cleanup_redis(opts['origin']['for_bid'])
72
+ end
85
73
  end
86
74
 
87
75
  def success(bid, status, parent_bid)
@@ -102,8 +90,8 @@ module CanvasSync
102
90
  r.scard("BID-#{parent_bid}-failed")
103
91
  end
104
92
  end
105
- # if job finished successfully and parent batch completed call parent complete callback
106
- # Success callback is called after complete callback
93
+ # If the job finished successfully and parent batch is completed, call parent :complete callback
94
+ # Parent :success callback will be called by its :complete callback
107
95
  if complete == children && pending == failure
108
96
  Batch.logger.debug {"Finalize parent complete bid: #{parent_bid}"}
109
97
  Batch.enqueue_callbacks(:complete, parent_bid)
@@ -119,10 +107,12 @@ module CanvasSync
119
107
  end
120
108
  end
121
109
 
122
- # if we batch was successful run success callback
110
+ # If the batch was successful run :success callback, which will call the parent's :complete callback (if necessary)
111
+ # Also, only trigger the success callback if the :complete callback_batch was successful
123
112
  if pending.to_i.zero? && children == success
124
113
  Batch.enqueue_callbacks(:success, bid)
125
114
 
115
+ # otherwise check for a parent and call its :complete if needed
126
116
  elsif parent_bid
127
117
  # if batch was not successfull check and see if its parent is complete
128
118
  # if the parent is complete we trigger the complete callback