canvas_sync 0.17.0 → 0.17.3.beta3

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