canvas_sync 0.17.1 → 0.17.4

Sign up to get free protection for your applications and to get access to all the features.
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