canvas_sync 0.17.2 → 0.17.3.beta1

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: 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