canvas_sync 0.17.0 → 0.17.3.beta3

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: d8629b92b654035d7c4480345bbf938d385b186c66acd5b4e30f8d76d6dcb109
4
- data.tar.gz: 0ef5bae856795745c2f8c8f845533148028076df62f76a395b315ae8b55679ff
3
+ metadata.gz: 54feabc8f9154b633a70a11d15bfeebaf54ab9d9eb24b9a636c27844728ca1a3
4
+ data.tar.gz: 0c8d2692a83ebb37a518f61d11fb2970b3204f3d68c990c721fa23a7365a91b6
5
5
  SHA512:
6
- metadata.gz: c08b8308faa3af77ba3828ef122d20e08d2dcf9f5a57e26f2274f000c23fa7d870638e9c2c37256139db6e63d9bc21e7b1497d5b257696c11c224ec9c77ae92e
7
- data.tar.gz: 63ddbbaf804dd4e547885c9817c7793646fae10c6f54d76bcf82643cc821d3f72c2a3b76a085e7720c1ec50360cd327f8498481c2d6d04cd829677d0d732259a
6
+ metadata.gz: 50efdad1e463d363463b72fa2fcc5581f877bdeb2c2440d688b10f451321996d95c7acd610d822fcc943d0220ed5520f4963de12341c1adb4238d131c5d8df04
7
+ data.tar.gz: 51d40bc898a58a0aab65a85d4c1820ca5da72bf22b4c11e0ee631ad2e815dc344dc4824a2ade24e7485ace7ad0d12692d71cc93054afcfc05269ac22a4f7b4c9
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)
@@ -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,52 +151,42 @@ 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
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
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)
195
166
  end
196
167
 
197
- def read_bid_attr(attribute)
198
- redis do |r|
199
- r.hget(@bidkey, attribute)
200
- end
168
+ protected
169
+
170
+ def redis_key
171
+ @bidkey
201
172
  end
202
173
 
203
- def flush_pending_attrs
204
- redis do |r|
205
- r.mapped_hmset(@bidkey, @pending_attrs)
174
+ private
175
+
176
+ def assert_batch_is_open
177
+ unless defined?(@closed)
178
+ redis do |r|
179
+ @closed = r.hget(@bidkey, 'success') == 'true'
180
+ end
206
181
  end
207
- @pending_attrs = {}
182
+ raise "Cannot add jobs to Batch #{} bid - it has already entered the callback-stage" if @closed
208
183
  end
209
184
 
210
- def append_jobs(jids, parent_bid = self.parent_bid)
185
+ def append_jobs(jids)
186
+ jids = jids.uniq
211
187
  redis do |r|
212
188
  r.multi do
213
- if parent_bid
214
- r.hincrby("BID-#{parent_bid}", "total", jids.size)
215
- end
216
-
217
189
  r.hincrby(@bidkey, "pending", jids.size)
218
- r.hincrby(@bidkey, "total", jids.size)
219
190
  r.expire(@bidkey, BID_EXPIRE_TTL)
220
191
 
221
192
  if jids.size > 0
@@ -242,17 +213,6 @@ module CanvasSync
242
213
  end
243
214
  end
244
215
 
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
216
  if pending.to_i == failed.to_i && children == complete
257
217
  enqueue_callbacks(:complete, bid)
258
218
  end
@@ -285,7 +245,7 @@ module CanvasSync
285
245
  end
286
246
 
287
247
  def process_successful_job(bid, jid)
288
- _, failed, pending, children, complete, success, total, parent_bid = redis do |r|
248
+ _, failed, pending, children, complete, success, parent_bid = redis do |r|
289
249
  r.multi do
290
250
  r.srem("BID-#{bid}-failed", jid)
291
251
 
@@ -294,14 +254,16 @@ module CanvasSync
294
254
  r.hincrby("BID-#{bid}", "children", 0)
295
255
  r.scard("BID-#{bid}-batches-complete")
296
256
  r.scard("BID-#{bid}-batches-success")
297
- r.hget("BID-#{bid}", "total")
298
257
  r.hget("BID-#{bid}", "parent_bid")
299
258
 
259
+ r.hincrby("BID-#{bid}", "successful-jobs", 1)
300
260
  r.srem("BID-#{bid}-jids", jid)
301
261
  r.expire("BID-#{bid}", BID_EXPIRE_TTL)
302
262
  end
303
263
  end
304
264
 
265
+ # TODO - There seems to be an issue where a :complete callback batch will occasionally run its job, but won't enqueue_callbacks()
266
+
305
267
  all_success = pending.to_i.zero? && children == success
306
268
  # if complete or successfull call complete callback (the complete callback may then call successful)
307
269
  if (pending.to_i == failed.to_i && children == complete) || all_success
@@ -313,14 +275,15 @@ module CanvasSync
313
275
  def enqueue_callbacks(event, bid)
314
276
  batch_key = "BID-#{bid}"
315
277
  callback_key = "#{batch_key}-callbacks-#{event}"
316
- already_processed, _, callbacks, queue, parent_bid, callback_batch = redis do |r|
278
+ already_processed, _, callbacks, queue, parent_bid, callback_params = redis do |r|
279
+ return unless r.exists?(batch_key)
317
280
  r.multi do
318
281
  r.hget(batch_key, event)
319
282
  r.hset(batch_key, event, true)
320
283
  r.smembers(callback_key)
321
284
  r.hget(batch_key, "callback_queue")
322
285
  r.hget(batch_key, "parent_bid")
323
- r.hget(batch_key, "callback_batch")
286
+ r.hget(batch_key, "callback_params")
324
287
  end
325
288
  end
326
289
 
@@ -328,6 +291,7 @@ module CanvasSync
328
291
 
329
292
  queue ||= "default"
330
293
  parent_bid = !parent_bid || parent_bid.empty? ? nil : parent_bid # Basically parent_bid.blank?
294
+ callback_params = JSON.parse(callback_params) if callback_params.present?
331
295
  callback_args = callbacks.reduce([]) do |memo, jcb|
332
296
  cb = JSON.load(jcb)
333
297
  memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
@@ -335,38 +299,32 @@ module CanvasSync
335
299
 
336
300
  opts = {"bid" => bid, "event" => event}
337
301
 
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
302
+ if callback_args.present? && !callback_params.present?
303
+ logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
343
304
 
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)
305
+ with_batch(parent_bid) do
306
+ cb_batch = self.new
307
+ cb_batch.callback_params = {
308
+ for_bid: bid,
309
+ event: event,
310
+ }
311
+ opts['callback_bid'] = cb_batch.bid
349
312
 
350
- return
313
+ logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
314
+ cb_batch.jobs do
315
+ push_callbacks callback_args, queue
316
+ end
317
+ end
351
318
  end
352
319
 
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
320
+ if callback_params.present?
321
+ opts['origin'] = callback_params
369
322
  end
323
+
324
+ logger.debug {"Run batch finalizer bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
325
+ finalizer = Batch::Callback::Finalize.new
326
+ status = Status.new bid
327
+ finalizer.dispatch(status, opts)
370
328
  end
371
329
 
372
330
  def cleanup_redis(bid)
@@ -381,6 +339,7 @@ module CanvasSync
381
339
  "BID-#{bid}-batches-success",
382
340
  "BID-#{bid}-batches-complete",
383
341
  "BID-#{bid}-batches-failed",
342
+ "BID-#{bid}-bids",
384
343
  "BID-#{bid}-jids",
385
344
  )
386
345
  end
@@ -397,7 +356,7 @@ module CanvasSync
397
356
  private
398
357
 
399
358
  def push_callbacks(args, queue)
400
- Batch::Callback::Worker.enqueue_all(args, queue)
359
+ Batch::Callback::worker_class.enqueue_all(args, queue)
401
360
  end
402
361
  end
403
362
  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,47 +48,34 @@ 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)
88
76
  return unless parent_bid
89
77
 
90
- _, _, success, _, _, complete, pending, children, failure = Batch.redis do |r|
78
+ _, _, success, _, _, complete, pending, children, success, failure = Batch.redis do |r|
91
79
  r.multi do
92
80
  r.sadd("BID-#{parent_bid}-batches-success", bid)
93
81
  r.expire("BID-#{parent_bid}-batches-success", Batch::BID_EXPIRE_TTL)
@@ -99,15 +87,21 @@ module CanvasSync
99
87
 
100
88
  r.hincrby("BID-#{parent_bid}", "pending", 0)
101
89
  r.hincrby("BID-#{parent_bid}", "children", 0)
90
+ r.scard("BID-#{parent_bid}-batches-success")
102
91
  r.scard("BID-#{parent_bid}-failed")
103
92
  end
104
93
  end
105
- # if job finished successfully and parent batch completed call parent complete callback
106
- # Success callback is called after complete callback
94
+
95
+ # If the job finished successfully and parent batch is completed, call parent :complete callback
96
+ # Parent :success callback will be called by its :complete callback
107
97
  if complete == children && pending == failure
108
98
  Batch.logger.debug {"Finalize parent complete bid: #{parent_bid}"}
109
99
  Batch.enqueue_callbacks(:complete, parent_bid)
110
100
  end
101
+ if pending.to_i.zero? && children == success
102
+ Batch.logger.debug {"Finalize parent success bid: #{parent_bid}"}
103
+ Batch.enqueue_callbacks(:success, parent_bid)
104
+ end
111
105
  end
112
106
 
113
107
  def complete(bid, status, parent_bid)
@@ -119,10 +113,12 @@ module CanvasSync
119
113
  end
120
114
  end
121
115
 
122
- # if we batch was successful run success callback
116
+ # If the batch was successful run :success callback, which will call the parent's :complete callback (if necessary)
117
+ # Also, only trigger the success callback if the :complete callback_batch was successful
123
118
  if pending.to_i.zero? && children == success
124
- Batch.enqueue_callbacks(:success, bid)
119
+ # Batch.enqueue_callbacks(:success, bid)
125
120
 
121
+ # otherwise check for a parent and call its :complete if needed
126
122
  elsif parent_bid
127
123
  # if batch was not successfull check and see if its parent is complete
128
124
  # if the parent is complete we trigger the complete callback