canvas_sync 0.17.2 → 0.17.3.beta1
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 +4 -4
- data/README.md +58 -0
- data/lib/canvas_sync/job_batches/batch.rb +75 -95
- data/lib/canvas_sync/job_batches/callback.rb +19 -29
- data/lib/canvas_sync/job_batches/context_hash.rb +8 -5
- data/lib/canvas_sync/job_batches/hincr_max.lua +5 -0
- data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +99 -0
- data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +6 -65
- data/lib/canvas_sync/job_batches/pool.rb +209 -0
- data/lib/canvas_sync/job_batches/redis_model.rb +67 -0
- data/lib/canvas_sync/job_batches/redis_script.rb +163 -0
- data/lib/canvas_sync/job_batches/sidekiq.rb +22 -1
- data/lib/canvas_sync/job_batches/status.rb +0 -5
- data/lib/canvas_sync/version.rb +1 -1
- data/spec/dummy/log/test.log +67741 -0
- data/spec/job_batching/batch_aware_job_spec.rb +1 -0
- data/spec/job_batching/batch_spec.rb +72 -15
- data/spec/job_batching/callback_spec.rb +1 -1
- data/spec/job_batching/flow_spec.rb +0 -1
- data/spec/job_batching/integration/fail_then_succeed.rb +42 -0
- data/spec/job_batching/integration_helper.rb +6 -4
- data/spec/job_batching/sidekiq_spec.rb +1 -0
- data/spec/job_batching/status_spec.rb +1 -17
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a67693c73d6b2bd34ffd9cf6ac17038c4d226eab30e1795ebbbd6a40d03e1bff
|
4
|
+
data.tar.gz: 2d42669d68de6ef9242e3d956ff15c82bb18fc07ee5d9e94c5eb1ac283928599
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
174
|
-
|
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] =
|
166
|
+
Thread.current[:batch] = batch
|
177
167
|
yield
|
178
168
|
ensure
|
179
169
|
Thread.current[:batch] = parent
|
180
170
|
end
|
181
171
|
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
177
|
+
protected
|
178
|
+
|
179
|
+
def redis_key
|
180
|
+
@bidkey
|
201
181
|
end
|
202
182
|
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
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,
|
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,
|
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, "
|
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
|
-
|
339
|
-
|
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
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
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
|
-
|
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
|
-
|
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::
|
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
|
-
|
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
|
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 #{
|
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
|
-
|
83
|
-
|
84
|
-
|
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
|
-
#
|
106
|
-
#
|
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
|
-
#
|
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
|