canvas_sync 0.17.1 → 0.17.4

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/job_batches/batch.rb +101 -115
  4. data/lib/canvas_sync/job_batches/callback.rb +29 -34
  5. data/lib/canvas_sync/job_batches/context_hash.rb +13 -5
  6. data/lib/canvas_sync/job_batches/hincr_max.lua +5 -0
  7. data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +99 -0
  8. data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +6 -65
  9. data/lib/canvas_sync/job_batches/pool.rb +213 -0
  10. data/lib/canvas_sync/job_batches/redis_model.rb +69 -0
  11. data/lib/canvas_sync/job_batches/redis_script.rb +163 -0
  12. data/lib/canvas_sync/job_batches/sidekiq.rb +24 -1
  13. data/lib/canvas_sync/job_batches/sidekiq/web.rb +114 -0
  14. data/lib/canvas_sync/job_batches/sidekiq/web/helpers.rb +41 -0
  15. data/lib/canvas_sync/job_batches/sidekiq/web/views/_batches_table.erb +42 -0
  16. data/lib/canvas_sync/job_batches/sidekiq/web/views/_pagination.erb +26 -0
  17. data/lib/canvas_sync/job_batches/sidekiq/web/views/batch.erb +138 -0
  18. data/lib/canvas_sync/job_batches/sidekiq/web/views/batches.erb +23 -0
  19. data/lib/canvas_sync/job_batches/sidekiq/web/views/pool.erb +85 -0
  20. data/lib/canvas_sync/job_batches/sidekiq/web/views/pools.erb +47 -0
  21. data/lib/canvas_sync/job_batches/status.rb +9 -5
  22. data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +3 -1
  23. data/lib/canvas_sync/version.rb +1 -1
  24. data/spec/dummy/log/test.log +140455 -0
  25. data/spec/job_batching/batch_aware_job_spec.rb +1 -0
  26. data/spec/job_batching/batch_spec.rb +72 -16
  27. data/spec/job_batching/callback_spec.rb +1 -1
  28. data/spec/job_batching/context_hash_spec.rb +54 -0
  29. data/spec/job_batching/flow_spec.rb +5 -11
  30. data/spec/job_batching/integration/fail_then_succeed.rb +42 -0
  31. data/spec/job_batching/integration_helper.rb +6 -4
  32. data/spec/job_batching/sidekiq_spec.rb +1 -0
  33. data/spec/job_batching/status_spec.rb +4 -20
  34. data/spec/spec_helper.rb +3 -7
  35. metadata +19 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e870dc1c517c8a91279f114c7f3c46b108555a350a76749ceca14e517b96e723
4
- data.tar.gz: 9b13b29d052e41e917c91afca7fcf693c038e537639be1f66b1c7d0eed635705
3
+ metadata.gz: 999dfad29e32b30e204cc26e6f36700752f3f3efc06314293c9d86df1b52b774
4
+ data.tar.gz: ef2c1b967f0451a3f2029d2aaae5dbe698e93a5a0136a52eeefe184dc1009f36
5
5
  SHA512:
6
- metadata.gz: cff6b6a167ce9eac91487cf9ec546c36544d0156e12b3273c57015cdbf18a279be3018f5221dcc55d90df8c6003a78c5150afe489bd2efec834df4e0700ed327
7
- data.tar.gz: 2162d8b3a32d82624ddaacee645ad81e5a0c70afb2a40354e284a9091c225cb92a06f83e291aeab3fbba06a85b99d3ea98621c78f4f1b63f43bba1d9258aec64
6
+ metadata.gz: cd96c186dced2cb00a77fe8e729094ce8c9a7878f74c854c6e15fe1e143b773682b747065c231cf9babf777ebe752f9bdb24f130b7b2485dab9efae7e4ba1be9
7
+ data.tar.gz: c7d3f33ccbc78a5f4fc22ab1c834ebf21c2be0863e7ff6cbabb5af5ebb5f9181783d8a18f2a1a015d56193d0a67289e1f77f12dcd5942d0b42e3ef6e96b82317
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,14 @@ 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
- @ready_to_queue = nil
52
38
  self.created_at = Time.now.utc.to_f unless @existing
53
39
  end
54
40
 
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
41
+ redis_attr :description
42
+ redis_attr :created_at
43
+ redis_attr :callback_queue, read_only: false
44
+ redis_attr :callback_params, :json
45
+ redis_attr :allow_context_changes
60
46
 
61
47
  def context
62
48
  return @context if defined?(@context)
@@ -113,7 +99,7 @@ module CanvasSync
113
99
  if parent_bid
114
100
  r.hincrby("BID-#{parent_bid}", "children", 1)
115
101
  r.expire("BID-#{parent_bid}", BID_EXPIRE_TTL)
116
- r.sadd("BID-#{parent_bid}-bids", bid)
102
+ r.zadd("BID-#{parent_bid}-bids", created_at, bid)
117
103
  end
118
104
  end
119
105
  end
@@ -122,29 +108,24 @@ module CanvasSync
122
108
  @context&.save!
123
109
 
124
110
  @initialized = true
111
+ else
112
+ assert_batch_is_open
125
113
  end
126
114
 
127
- job_queue = @ready_to_queue = []
128
-
129
115
  begin
130
116
  parent = Thread.current[:batch]
131
117
  Thread.current[:batch] = self
132
118
  yield
133
119
  ensure
134
- @ready_to_queue = nil
135
- append_jobs(job_queue, parent_bid)
136
120
  Thread.current[:batch] = parent
137
121
  end
138
122
 
139
- job_queue
123
+ nil
140
124
  end
141
125
 
142
126
  def increment_job_queue(jid)
143
- if @ready_to_queue
144
- @ready_to_queue << jid
145
- else
146
- append_jobs([jid])
147
- end
127
+ assert_batch_is_open
128
+ append_jobs([jid])
148
129
  end
149
130
 
150
131
  def invalidate_all
@@ -170,58 +151,56 @@ module CanvasSync
170
151
  batch.parent ? valid && valid?(batch.parent) : valid
171
152
  end
172
153
 
173
- # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
174
- def self.without_batch
154
+ def self.with_batch(batch)
155
+ batch = self.new(batch) if batch.is_a?(String)
175
156
  parent = Thread.current[:batch]
176
- Thread.current[:batch] = nil
157
+ Thread.current[:batch] = batch
177
158
  yield
178
159
  ensure
179
160
  Thread.current[:batch] = parent
180
161
  end
181
162
 
182
- private
163
+ # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
164
+ def self.without_batch(&blk)
165
+ with_batch(nil, &blk)
166
+ end
183
167
 
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
168
+ protected
169
+
170
+ def redis_key
171
+ @bidkey
195
172
  end
196
173
 
197
- def read_bid_attr(attribute)
174
+ def flush_pending_attrs
175
+ super
198
176
  redis do |r|
199
- r.hget(@bidkey, attribute)
177
+ r.zadd("batches", created_at, bid)
200
178
  end
201
179
  end
202
180
 
203
- def flush_pending_attrs
204
- redis do |r|
205
- r.mapped_hmset(@bidkey, @pending_attrs)
181
+ private
182
+
183
+ def assert_batch_is_open
184
+ unless defined?(@closed)
185
+ redis do |r|
186
+ @closed = r.hget(@bidkey, 'success') == 'true'
187
+ end
206
188
  end
207
- @pending_attrs = {}
189
+ raise "Cannot add jobs to Batch #{} bid - it has already entered the callback-stage" if @closed
208
190
  end
209
191
 
210
- def append_jobs(jids, parent_bid = self.parent_bid)
192
+ def append_jobs(jids)
193
+ jids = jids.uniq
194
+ return unless jids.size > 0
195
+
211
196
  redis do |r|
197
+ tme = Time.now.utc.to_f
198
+ added = r.zadd(@bidkey + "-jids", jids.map{|jid| [tme, jid] }, nx: true)
212
199
  r.multi do
213
- if parent_bid
214
- r.hincrby("BID-#{parent_bid}", "total", jids.size)
215
- end
216
-
217
- r.hincrby(@bidkey, "pending", jids.size)
218
- r.hincrby(@bidkey, "total", jids.size)
200
+ r.hincrby(@bidkey, "pending", added)
201
+ r.hincrby(@bidkey, "job_count", added)
219
202
  r.expire(@bidkey, BID_EXPIRE_TTL)
220
-
221
- if jids.size > 0
222
- r.sadd(@bidkey + "-jids", jids)
223
- r.expire(@bidkey + "-jids", BID_EXPIRE_TTL)
224
- end
203
+ r.expire(@bidkey + "-jids", BID_EXPIRE_TTL)
225
204
  end
226
205
  end
227
206
  end
@@ -229,6 +208,8 @@ module CanvasSync
229
208
  class << self
230
209
  def process_failed_job(bid, jid)
231
210
  _, pending, failed, children, complete, parent_bid = redis do |r|
211
+ return unless r.exists?("BID-#{bid}")
212
+
232
213
  r.multi do
233
214
  r.sadd("BID-#{bid}-failed", jid)
234
215
 
@@ -242,17 +223,6 @@ module CanvasSync
242
223
  end
243
224
  end
244
225
 
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
226
  if pending.to_i == failed.to_i && children == complete
257
227
  enqueue_callbacks(:complete, bid)
258
228
  end
@@ -260,6 +230,8 @@ module CanvasSync
260
230
 
261
231
  def process_dead_job(bid, jid)
262
232
  _, failed, children, complete, parent_bid = redis do |r|
233
+ return unless r.exists?("BID-#{bid}")
234
+
263
235
  r.multi do
264
236
  r.sadd("BID-#{bid}-dead", jid)
265
237
 
@@ -285,7 +257,9 @@ module CanvasSync
285
257
  end
286
258
 
287
259
  def process_successful_job(bid, jid)
288
- _, failed, pending, children, complete, success, total, parent_bid = redis do |r|
260
+ _, failed, pending, children, complete, success, parent_bid = redis do |r|
261
+ return unless r.exists?("BID-#{bid}")
262
+
289
263
  r.multi do
290
264
  r.srem("BID-#{bid}-failed", jid)
291
265
 
@@ -294,10 +268,10 @@ module CanvasSync
294
268
  r.hincrby("BID-#{bid}", "children", 0)
295
269
  r.scard("BID-#{bid}-batches-complete")
296
270
  r.scard("BID-#{bid}-batches-success")
297
- r.hget("BID-#{bid}", "total")
298
271
  r.hget("BID-#{bid}", "parent_bid")
299
272
 
300
- r.srem("BID-#{bid}-jids", jid)
273
+ r.hincrby("BID-#{bid}", "successful-jobs", 1)
274
+ r.zrem("BID-#{bid}-jids", jid)
301
275
  r.expire("BID-#{bid}", BID_EXPIRE_TTL)
302
276
  end
303
277
  end
@@ -313,14 +287,15 @@ module CanvasSync
313
287
  def enqueue_callbacks(event, bid)
314
288
  batch_key = "BID-#{bid}"
315
289
  callback_key = "#{batch_key}-callbacks-#{event}"
316
- already_processed, _, callbacks, queue, parent_bid, callback_batch = redis do |r|
290
+ already_processed, _, callbacks, queue, parent_bid, callback_params = redis do |r|
291
+ return unless r.exists?(batch_key)
317
292
  r.multi do
318
293
  r.hget(batch_key, event)
319
294
  r.hset(batch_key, event, true)
320
295
  r.smembers(callback_key)
321
296
  r.hget(batch_key, "callback_queue")
322
297
  r.hget(batch_key, "parent_bid")
323
- r.hget(batch_key, "callback_batch")
298
+ r.hget(batch_key, "callback_params")
324
299
  end
325
300
  end
326
301
 
@@ -328,6 +303,7 @@ module CanvasSync
328
303
 
329
304
  queue ||= "default"
330
305
  parent_bid = !parent_bid || parent_bid.empty? ? nil : parent_bid # Basically parent_bid.blank?
306
+ callback_params = JSON.parse(callback_params) if callback_params.present?
331
307
  callback_args = callbacks.reduce([]) do |memo, jcb|
332
308
  cb = JSON.load(jcb)
333
309
  memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
@@ -335,44 +311,44 @@ module CanvasSync
335
311
 
336
312
  opts = {"bid" => bid, "event" => event}
337
313
 
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
314
+ if callback_args.present? && !callback_params.present?
315
+ logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
343
316
 
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)
317
+ redis do |r|
318
+ r.sadd("#{batch_key}-pending_callbacks", event)
319
+ r.expire("#{batch_key}-pending_callbacks", BID_EXPIRE_TTL)
320
+ end
349
321
 
350
- return
322
+ with_batch(parent_bid) do
323
+ cb_batch = self.new
324
+ cb_batch.callback_params = {
325
+ for_bid: bid,
326
+ event: event,
327
+ }
328
+ opts['callback_bid'] = cb_batch.bid
329
+
330
+ logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
331
+ cb_batch.jobs do
332
+ push_callbacks callback_args, queue
333
+ end
334
+ end
351
335
  end
352
336
 
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
337
+ if callback_params.present?
338
+ opts['origin'] = callback_params
369
339
  end
340
+
341
+ logger.debug {"Run batch finalizer bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
342
+ finalizer = Batch::Callback::Finalize.new
343
+ status = Status.new bid
344
+ finalizer.dispatch(status, opts)
370
345
  end
371
346
 
372
347
  def cleanup_redis(bid)
373
348
  logger.debug {"Cleaning redis of batch #{bid}"}
374
349
  redis do |r|
375
- r.del(
350
+ r.zrem("batches", bid)
351
+ r.unlink(
376
352
  "BID-#{bid}",
377
353
  "BID-#{bid}-callbacks-complete",
378
354
  "BID-#{bid}-callbacks-success",
@@ -381,11 +357,23 @@ module CanvasSync
381
357
  "BID-#{bid}-batches-success",
382
358
  "BID-#{bid}-batches-complete",
383
359
  "BID-#{bid}-batches-failed",
360
+ "BID-#{bid}-bids",
384
361
  "BID-#{bid}-jids",
362
+ "BID-#{bid}-pending_callbacks",
385
363
  )
386
364
  end
387
365
  end
388
366
 
367
+ def delete_prematurely!(bid)
368
+ child_bids = redis do |r|
369
+ r.zrange("BID-#{bid}-bids", 0, -1)
370
+ end
371
+ child_bids.each do |cbid|
372
+ delete_prematurely!(cbid)
373
+ end
374
+ cleanup_redis(bid)
375
+ end
376
+
389
377
  def redis(*args, &blk)
390
378
  defined?(::Sidekiq) ? ::Sidekiq.redis(*args, &blk) : nil # TODO
391
379
  end
@@ -394,10 +382,8 @@ module CanvasSync
394
382
  defined?(::Sidekiq) ? ::Sidekiq.logger : Rails.logger
395
383
  end
396
384
 
397
- private
398
-
399
385
  def push_callbacks(args, queue)
400
- Batch::Callback::Worker.enqueue_all(args, queue)
386
+ Batch::Callback::worker_class.enqueue_all(args, queue)
401
387
  end
402
388
  end
403
389
  end