canvas_sync 0.21.1 → 0.22.0.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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/lib/canvas_sync/concerns/auto_relations.rb +11 -0
  3. data/lib/canvas_sync/config.rb +3 -5
  4. data/lib/canvas_sync/generators/templates/models/rubric.rb +2 -1
  5. data/lib/canvas_sync/job_batches/batch.rb +432 -402
  6. data/lib/canvas_sync/job_batches/callback.rb +100 -114
  7. data/lib/canvas_sync/job_batches/chain_builder.rb +194 -196
  8. data/lib/canvas_sync/job_batches/{active_job.rb → compat/active_job.rb} +2 -2
  9. data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/helpers.rb +1 -1
  10. data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web.rb +3 -3
  11. data/lib/canvas_sync/job_batches/{sidekiq.rb → compat/sidekiq.rb} +35 -22
  12. data/lib/canvas_sync/job_batches/compat.rb +20 -0
  13. data/lib/canvas_sync/job_batches/context_hash.rb +124 -126
  14. data/lib/canvas_sync/job_batches/jobs/base_job.rb +2 -4
  15. data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +14 -16
  16. data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +125 -127
  17. data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +14 -16
  18. data/lib/canvas_sync/job_batches/pool.rb +193 -195
  19. data/lib/canvas_sync/job_batches/redis_model.rb +50 -52
  20. data/lib/canvas_sync/job_batches/redis_script.rb +129 -131
  21. data/lib/canvas_sync/job_batches/status.rb +85 -87
  22. data/lib/canvas_sync/job_uniqueness/compat/active_job.rb +75 -0
  23. data/lib/canvas_sync/job_uniqueness/compat/sidekiq.rb +135 -0
  24. data/lib/canvas_sync/job_uniqueness/compat.rb +20 -0
  25. data/lib/canvas_sync/job_uniqueness/configuration.rb +25 -0
  26. data/lib/canvas_sync/job_uniqueness/job_uniqueness.rb +47 -0
  27. data/lib/canvas_sync/job_uniqueness/lock_context.rb +171 -0
  28. data/lib/canvas_sync/job_uniqueness/locksmith.rb +92 -0
  29. data/lib/canvas_sync/job_uniqueness/on_conflict/base.rb +32 -0
  30. data/lib/canvas_sync/job_uniqueness/on_conflict/log.rb +13 -0
  31. data/lib/canvas_sync/job_uniqueness/on_conflict/null_strategy.rb +9 -0
  32. data/lib/canvas_sync/job_uniqueness/on_conflict/raise.rb +11 -0
  33. data/lib/canvas_sync/job_uniqueness/on_conflict/reject.rb +21 -0
  34. data/lib/canvas_sync/job_uniqueness/on_conflict/reschedule.rb +20 -0
  35. data/lib/canvas_sync/job_uniqueness/on_conflict.rb +41 -0
  36. data/lib/canvas_sync/job_uniqueness/strategy/base.rb +104 -0
  37. data/lib/canvas_sync/job_uniqueness/strategy/until_and_while_executing.rb +35 -0
  38. data/lib/canvas_sync/job_uniqueness/strategy/until_executed.rb +20 -0
  39. data/lib/canvas_sync/job_uniqueness/strategy/until_executing.rb +20 -0
  40. data/lib/canvas_sync/job_uniqueness/strategy/until_expired.rb +16 -0
  41. data/lib/canvas_sync/job_uniqueness/strategy/while_executing.rb +26 -0
  42. data/lib/canvas_sync/job_uniqueness/strategy.rb +27 -0
  43. data/lib/canvas_sync/job_uniqueness/unique_job_common.rb +79 -0
  44. data/lib/canvas_sync/misc_helper.rb +1 -1
  45. data/lib/canvas_sync/version.rb +1 -1
  46. data/lib/canvas_sync.rb +4 -3
  47. data/spec/dummy/app/models/rubric.rb +2 -1
  48. data/spec/dummy/config/environments/test.rb +1 -1
  49. data/spec/job_batching/batch_spec.rb +49 -7
  50. data/spec/job_batching/{active_job_spec.rb → compat/active_job_spec.rb} +2 -2
  51. data/spec/job_batching/{sidekiq_spec.rb → compat/sidekiq_spec.rb} +14 -12
  52. data/spec/job_batching/flow_spec.rb +1 -1
  53. data/spec/job_batching/integration_helper.rb +1 -1
  54. data/spec/job_batching/status_spec.rb +2 -2
  55. data/spec/job_uniqueness/compat/active_job_spec.rb +49 -0
  56. data/spec/job_uniqueness/compat/sidekiq_spec.rb +68 -0
  57. data/spec/job_uniqueness/lock_context_spec.rb +95 -0
  58. data/spec/job_uniqueness/on_conflict/log_spec.rb +11 -0
  59. data/spec/job_uniqueness/on_conflict/raise_spec.rb +10 -0
  60. data/spec/job_uniqueness/on_conflict/reschedule_spec.rb +24 -0
  61. data/spec/job_uniqueness/on_conflict_spec.rb +16 -0
  62. data/spec/job_uniqueness/spec_helper.rb +14 -0
  63. data/spec/job_uniqueness/strategy/base_spec.rb +100 -0
  64. data/spec/job_uniqueness/strategy/until_and_while_executing_spec.rb +48 -0
  65. data/spec/job_uniqueness/strategy/until_executed_spec.rb +23 -0
  66. data/spec/job_uniqueness/strategy/until_executing_spec.rb +23 -0
  67. data/spec/job_uniqueness/strategy/until_expired_spec.rb +23 -0
  68. data/spec/job_uniqueness/strategy/while_executing_spec.rb +33 -0
  69. data/spec/job_uniqueness/support/lock_strategy.rb +28 -0
  70. data/spec/job_uniqueness/support/on_conflict.rb +24 -0
  71. data/spec/job_uniqueness/support/test_worker.rb +19 -0
  72. data/spec/job_uniqueness/unique_job_common_spec.rb +45 -0
  73. data/spec/spec_helper.rb +1 -1
  74. metadata +278 -204
  75. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/css/styles.less +0 -0
  76. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/js/batch_tree.js +0 -0
  77. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/js/util.js +0 -0
  78. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_batch_tree.erb +0 -0
  79. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_batches_table.erb +0 -0
  80. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_common.erb +0 -0
  81. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_jobs_table.erb +0 -0
  82. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_pagination.erb +0 -0
  83. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/batch.erb +0 -0
  84. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/batches.erb +0 -0
  85. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/pool.erb +0 -0
  86. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/pools.erb +0 -0
@@ -1,9 +1,4 @@
1
1
 
2
- begin
3
- require 'sidekiq'
4
- rescue LoadError
5
- end
6
-
7
2
  require_relative './redis_model'
8
3
  require_relative './redis_script'
9
4
  require_relative "./callback"
@@ -16,528 +11,563 @@ require_relative "./chain_builder"
16
11
  # Implement Job Batching similar to Sidekiq::Batch. Supports ActiveJob and Sidekiq, or a mix thereof.
17
12
  # Much of this code is modifed/extended from https://github.com/breamware/sidekiq-batch
18
13
 
19
- module CanvasSync
20
- module JobBatches
21
- CURRENT_BATCH_THREAD_KEY = :job_batches_batch
14
+ module CanvasSync::JobBatches
15
+ CURRENT_BATCH_THREAD_KEY = :job_batches_batch
22
16
 
23
- class Batch
24
- include RedisModel
17
+ class Batch
18
+ include RedisModel
25
19
 
26
- class NoBlockGivenError < StandardError; end
20
+ class NoBlockGivenError < StandardError; end
27
21
 
28
- delegate :redis, to: :class
22
+ delegate :redis, to: :class
29
23
 
30
- BID_EXPIRE_TTL = 90.days.to_i
31
- INDEX_ALL_BATCHES = false
32
- SCHEDULE_CALLBACK = RedisScript.new(Pathname.new(__FILE__) + "../schedule_callback.lua")
33
- BID_HIERARCHY = RedisScript.new(Pathname.new(__FILE__) + "../hier_batch_ids.lua")
24
+ BID_EXPIRE_TTL = 90.days.to_i
25
+ INDEX_ALL_BATCHES = false
26
+ SCHEDULE_CALLBACK = RedisScript.new(Pathname.new(__FILE__) + "../schedule_callback.lua")
27
+ BID_HIERARCHY = RedisScript.new(Pathname.new(__FILE__) + "../hier_batch_ids.lua")
34
28
 
35
- attr_reader :bid
29
+ attr_reader :bid
36
30
 
37
- def self.current
38
- Thread.current[CURRENT_BATCH_THREAD_KEY]
39
- end
31
+ def self.current
32
+ Thread.current[CURRENT_BATCH_THREAD_KEY]
33
+ end
40
34
 
41
- def self.current_context
42
- self.current&.context
43
- end
35
+ def self.current_context
36
+ self.current&.context
37
+ end
44
38
 
45
- def initialize(existing_bid = nil)
46
- @bid = existing_bid || SecureRandom.urlsafe_base64(10)
47
- @existing = !(!existing_bid || existing_bid.empty?) # Basically existing_bid.present?
48
- @initialized = false
49
- @bidkey = "BID-" + @bid.to_s
50
- self.created_at = Time.now.utc.to_f unless @existing
51
- end
39
+ def initialize(existing_bid = nil)
40
+ @bid = existing_bid || SecureRandom.urlsafe_base64(10)
41
+ @existing = !(!existing_bid || existing_bid.empty?) # Basically existing_bid.present?
42
+ @initialized = false
43
+ @bidkey = "BID-" + @bid.to_s
44
+ self.created_at = Time.now.utc.to_f unless @existing
45
+ end
52
46
 
53
- redis_attr :description
54
- redis_attr :created_at
55
- redis_attr :callback_queue, read_only: false
56
- redis_attr :callback_params, :json
57
- redis_attr :allow_context_changes, :bool
47
+ redis_attr :description
48
+ redis_attr :created_at
49
+ redis_attr :callback_queue, read_only: false
50
+ redis_attr :callback_params, :json
51
+ redis_attr :allow_context_changes, :bool
58
52
 
59
- def context
60
- return @context if defined?(@context)
53
+ def context
54
+ return @context if defined?(@context)
61
55
 
62
- if (@initialized || @existing)
63
- @context = ContextHash.new(bid)
64
- else
65
- @context = ContextHash.new(bid, {})
66
- end
56
+ if (@initialized || @existing)
57
+ @context = ContextHash.new(bid)
58
+ else
59
+ @context = ContextHash.new(bid, {})
67
60
  end
61
+ end
68
62
 
69
- def context=(value)
70
- raise "context is read-only once the batch has been started" if (@initialized || @existing) # && !allow_context_changes
71
- raise "context must be a Hash" unless value.is_a?(Hash) || value.nil?
72
- return nil if value.nil? && @context.nil?
63
+ def context=(value)
64
+ raise "context is read-only once the batch has been started" if (@initialized || @existing) # && !allow_context_changes
65
+ raise "context must be a Hash" unless value.is_a?(Hash) || value.nil?
66
+ return nil if value.nil? && @context.nil?
73
67
 
74
- value = {} if value.nil?
75
- value = value.local if value.is_a?(ContextHash)
68
+ value = {} if value.nil?
69
+ value = value.local if value.is_a?(ContextHash)
76
70
 
77
- @context ||= ContextHash.new(bid, {})
78
- @context.set_local(value)
79
- # persist_bid_attr('context', JSON.unparse(@context.local))
80
- end
71
+ @context ||= ContextHash.new(bid, {})
72
+ @context.set_local(value)
73
+ # persist_bid_attr('context', JSON.unparse(@context.local))
74
+ end
81
75
 
82
- def save_context_changes
83
- @context&.save!
76
+ def save_context_changes
77
+ @context&.save!
78
+ end
79
+
80
+ # Events:
81
+ # :complete - triggered once all jobs have been executed, regardless of success or failure.
82
+ # :success - triggered once all jobs haves successfully executed.
83
+ # :death - triggered after any job enters the dead state.
84
+ # :stagnated - triggered when a job dies and no other jobs/batches are active.
85
+ def on(event, callback, options = {})
86
+ return unless Callback::VALID_CALLBACKS.include?(event.to_sym)
87
+
88
+ callback_key = "#{@bidkey}-callbacks-#{event}"
89
+ redis.multi do |r|
90
+ r.sadd(callback_key, JSON.unparse({
91
+ callback: callback,
92
+ opts: options
93
+ }))
94
+ r.expire(callback_key, BID_EXPIRE_TTL)
84
95
  end
96
+ end
97
+
98
+ def jobs
99
+ raise NoBlockGivenError unless block_given?
100
+
101
+ if !@existing && !@initialized
102
+ parent_bid = Thread.current[CURRENT_BATCH_THREAD_KEY]&.bid
85
103
 
86
- def on(event, callback, options = {})
87
- return unless Callback::VALID_CALLBACKS.include?(event.to_s)
88
- callback_key = "#{@bidkey}-callbacks-#{event}"
89
104
  redis.multi do |r|
90
- r.sadd(callback_key, JSON.unparse({
91
- callback: callback,
92
- opts: options
93
- }))
94
- r.expire(callback_key, BID_EXPIRE_TTL)
105
+ r.hset(@bidkey, "parent_bid", parent_bid.to_s) if parent_bid
106
+ r.expire(@bidkey, BID_EXPIRE_TTL)
107
+
108
+ if parent_bid
109
+ r.hincrby("BID-#{parent_bid}", "children", 1)
110
+ r.expire("BID-#{parent_bid}", BID_EXPIRE_TTL)
111
+ r.zadd("BID-#{parent_bid}-bids", created_at, bid)
112
+ else
113
+ r.zadd("BID-ROOT-bids", created_at, bid)
114
+ end
95
115
  end
116
+
117
+ flush_pending_attrs
118
+ @context&.save!
119
+
120
+ @initialized = true
121
+ else
122
+ assert_batch_is_open
96
123
  end
97
124
 
98
- def jobs
99
- raise NoBlockGivenError unless block_given?
125
+ begin
126
+ parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
127
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = self
128
+ yield
129
+ ensure
130
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
131
+ end
100
132
 
101
- if !@existing && !@initialized
102
- parent_bid = Thread.current[CURRENT_BATCH_THREAD_KEY]&.bid
133
+ nil
134
+ end
103
135
 
104
- redis.multi do |r|
105
- r.hset(@bidkey, "parent_bid", parent_bid.to_s) if parent_bid
106
- r.expire(@bidkey, BID_EXPIRE_TTL)
136
+ def increment_job_queue(jid)
137
+ assert_batch_is_open
138
+ append_jobs([jid])
139
+ end
107
140
 
108
- if parent_bid
109
- r.hincrby("BID-#{parent_bid}", "children", 1)
110
- r.expire("BID-#{parent_bid}", BID_EXPIRE_TTL)
111
- r.zadd("BID-#{parent_bid}-bids", created_at, bid)
112
- else
113
- r.zadd("BID-ROOT-bids", created_at, bid)
114
- end
115
- end
141
+ def invalidate_all
142
+ redis.setex("invalidated-bid-#{bid}", BID_EXPIRE_TTL, 1)
143
+ end
116
144
 
117
- flush_pending_attrs
118
- @context&.save!
145
+ def parent_bid
146
+ redis.hget(@bidkey, "parent_bid")
147
+ end
119
148
 
120
- @initialized = true
121
- else
122
- assert_batch_is_open
123
- end
149
+ def parent
150
+ if parent_bid
151
+ Batch.new(parent_bid)
152
+ end
153
+ end
154
+
155
+ def valid?(batch = self)
156
+ valid = !redis.exists?("invalidated-bid-#{batch.bid}")
157
+ batch.parent ? valid && valid?(batch.parent) : valid
158
+ end
124
159
 
160
+ def keep_open!
161
+ if block_given?
125
162
  begin
126
- parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
127
- Thread.current[CURRENT_BATCH_THREAD_KEY] = self
163
+ keep_open!
128
164
  yield
129
165
  ensure
130
- Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
166
+ let_close!
131
167
  end
132
-
133
- nil
168
+ else
169
+ redis.hset(@bidkey, 'keep_open', "true")
134
170
  end
171
+ end
135
172
 
136
- def increment_job_queue(jid)
137
- assert_batch_is_open
138
- append_jobs([jid])
139
- end
173
+ def let_close!
174
+ _, failed, pending, children, complete, success = redis.multi do |r|
175
+ r.hset(@bidkey, 'keep_open', "false")
140
176
 
141
- def invalidate_all
142
- redis.setex("invalidated-bid-#{bid}", BID_EXPIRE_TTL, 1)
177
+ r.scard("BID-#{bid}-failed")
178
+ r.hincrby("BID-#{bid}", "pending", 0)
179
+ r.hincrby("BID-#{bid}", "children", 0)
180
+ r.scard("BID-#{bid}-batches-complete")
181
+ r.scard("BID-#{bid}-batches-success")
143
182
  end
144
183
 
145
- def parent_bid
146
- redis.hget(@bidkey, "parent_bid")
184
+ all_success = pending.to_i.zero? && children == success
185
+ # if complete or successfull call complete callback (the complete callback may then call successful)
186
+ if (pending.to_i == failed.to_i && children == complete) || all_success
187
+ self.class.enqueue_callbacks(:complete, bid)
188
+ self.class.enqueue_callbacks(:success, bid) if all_success
147
189
  end
190
+ end
148
191
 
149
- def parent
150
- if parent_bid
151
- Batch.new(parent_bid)
152
- end
153
- end
192
+ def self.with_batch(batch)
193
+ batch = self.new(batch) if batch.is_a?(String)
194
+ parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
195
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = batch
196
+ yield
197
+ ensure
198
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
199
+ end
154
200
 
155
- def valid?(batch = self)
156
- valid = !redis.exists?("invalidated-bid-#{batch.bid}")
157
- batch.parent ? valid && valid?(batch.parent) : valid
158
- end
201
+ # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
202
+ def self.without_batch(&blk)
203
+ with_batch(nil, &blk)
204
+ end
159
205
 
160
- def keep_open!
161
- if block_given?
162
- begin
163
- keep_open!
164
- yield
165
- ensure
166
- let_close!
167
- end
168
- else
169
- redis.hset(@bidkey, 'keep_open', "true")
170
- end
171
- end
206
+ protected
172
207
 
173
- def let_close!
174
- _, failed, pending, children, complete, success = redis.multi do |r|
175
- r.hset(@bidkey, 'keep_open', "false")
208
+ def redis_key
209
+ @bidkey
210
+ end
176
211
 
177
- r.scard("BID-#{bid}-failed")
178
- r.hincrby("BID-#{bid}", "pending", 0)
179
- r.hincrby("BID-#{bid}", "children", 0)
180
- r.scard("BID-#{bid}-batches-complete")
181
- r.scard("BID-#{bid}-batches-success")
182
- end
212
+ def flush_pending_attrs
213
+ super
214
+ redis.zadd("batches", created_at, bid) if INDEX_ALL_BATCHES
215
+ end
183
216
 
184
- all_success = pending.to_i.zero? && children == success
185
- # if complete or successfull call complete callback (the complete callback may then call successful)
186
- if (pending.to_i == failed.to_i && children == complete) || all_success
187
- self.class.enqueue_callbacks(:complete, bid)
188
- self.class.enqueue_callbacks(:success, bid) if all_success
189
- end
190
- end
217
+ private
191
218
 
192
- def self.with_batch(batch)
193
- batch = self.new(batch) if batch.is_a?(String)
194
- parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
195
- Thread.current[CURRENT_BATCH_THREAD_KEY] = batch
196
- yield
197
- ensure
198
- Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
219
+ def assert_batch_is_open
220
+ unless defined?(@closed)
221
+ @closed = redis.hget(@bidkey, 'success') == 'true'
199
222
  end
223
+ raise "Cannot add jobs to Batch #{} bid - it has already entered the callback-stage" if @closed
224
+ end
200
225
 
201
- # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
202
- def self.without_batch(&blk)
203
- with_batch(nil, &blk)
226
+ def append_jobs(jids)
227
+ jids = jids.uniq
228
+ return unless jids.size > 0
229
+
230
+ redis do |r|
231
+ tme = Time.now.utc.to_f
232
+ added = r.zadd(@bidkey + "-jids", jids.map{|jid| [tme, jid] }, nx: true)
233
+ r.multi do |r|
234
+ r.hincrby(@bidkey, "pending", added)
235
+ r.hincrby(@bidkey, "job_count", added)
236
+ r.expire(@bidkey, BID_EXPIRE_TTL)
237
+ r.expire(@bidkey + "-jids", BID_EXPIRE_TTL)
238
+ end
204
239
  end
240
+ end
205
241
 
206
- protected
207
-
208
- def redis_key
209
- @bidkey
242
+ class << self
243
+ def current
244
+ Thread.current[CURRENT_BATCH_THREAD_KEY]
210
245
  end
211
246
 
212
- def flush_pending_attrs
213
- super
214
- redis.zadd("batches", created_at, bid) if INDEX_ALL_BATCHES
247
+ def current_context
248
+ current&.context || {}
215
249
  end
216
250
 
217
- private
251
+ # Perform a success/failure/complete/etc transaction against Redis, checking
252
+ # if any callbacks should be triggered
253
+ def with_callback_check(bid, except: [], only: Callback::VALID_CALLBACKS, &blk)
254
+ except = Array(except).map(&:to_sym)
255
+ only = Array(only).map(&:to_sym)
218
256
 
219
- def assert_batch_is_open
220
- unless defined?(@closed)
221
- @closed = redis.hget(@bidkey, 'success') == 'true'
222
- end
223
- raise "Cannot add jobs to Batch #{} bid - it has already entered the callback-stage" if @closed
224
- end
257
+ precount = 0
225
258
 
226
- def append_jobs(jids)
227
- jids = jids.uniq
228
- return unless jids.size > 0
259
+ all_results = redis do |r|
260
+ return unless r.exists?("BID-#{bid}")
229
261
 
230
- redis do |r|
231
- tme = Time.now.utc.to_f
232
- added = r.zadd(@bidkey + "-jids", jids.map{|jid| [tme, jid] }, nx: true)
233
262
  r.multi do |r|
234
- r.hincrby(@bidkey, "pending", added)
235
- r.hincrby(@bidkey, "job_count", added)
236
- r.expire(@bidkey, BID_EXPIRE_TTL)
237
- r.expire(@bidkey + "-jids", BID_EXPIRE_TTL)
263
+ # The block probably doesn't _have_ to be part of the same transaction, but it was easier than
264
+ # re-assessing possible race-conditions and is technically more performant
265
+ blk.call(r)
266
+ futures = r.instance_variable_get(:@futures) || r.instance_variable_get(:@pipeline)&.futures || r.instance_variable_get(:@client)&.futures
267
+ precount = futures.size
268
+
269
+ # Misc
270
+ r.hget("BID-#{bid}", "parent_bid")
271
+ r.hget("BID-#{bid}", "keep_open")
272
+
273
+ # Jobs
274
+ r.hincrby("BID-#{bid}", "pending", 0)
275
+ r.scard("BID-#{bid}-failed")
276
+ r.scard("BID-#{bid}-dead")
277
+
278
+ # Batches
279
+ r.hincrby("BID-#{bid}", "children", 0)
280
+ r.scard("BID-#{bid}-batches-complete")
281
+ r.scard("BID-#{bid}-batches-success")
282
+ r.scard("BID-#{bid}-batches-failed")
283
+ r.scard("BID-#{bid}-batches-stagnated")
284
+
285
+ # Touch Expirations
286
+ r.expire("BID-#{bid}", BID_EXPIRE_TTL)
287
+ r.expire("BID-#{bid}-batches-success", BID_EXPIRE_TTL)
238
288
  end
239
289
  end
240
- end
241
290
 
242
- class << self
243
- def current
244
- Thread.current[CURRENT_BATCH_THREAD_KEY]
245
- end
291
+ # Exclude return values from the passed block
292
+ actual_results = all_results[precount..-1]
246
293
 
247
- def current_context
248
- current&.context || {}
249
- end
294
+ # "pending" = not successful (yet)
295
+ # "failed" = dead or retrying
296
+ # "complete" = successful or failed
250
297
 
251
- def process_failed_job(bid, jid)
252
- _, pending, failed, children, complete, parent_bid = redis do |r|
253
- return unless r.exists?("BID-#{bid}")
298
+ parent_bid, keep_open, \
299
+ pending_jobs, failed_jobs, dead_jobs, \
300
+ child_batches, complete_batches, success_batches, failed_batches, stagnated_batches \
301
+ = actual_results
254
302
 
255
- r.multi do |r|
256
- r.sadd("BID-#{bid}-failed", jid)
303
+ pending_batches = child_batches - success_batches
257
304
 
258
- r.hincrby("BID-#{bid}", "pending", 0)
259
- r.scard("BID-#{bid}-failed")
260
- r.hincrby("BID-#{bid}", "children", 0)
261
- r.scard("BID-#{bid}-batches-complete")
262
- r.hget("BID-#{bid}", "parent_bid")
305
+ trigger_callback = ->(callback) {
306
+ next if except.include?(callback.to_sym)
307
+ next unless only.include?(callback.to_sym)
263
308
 
264
- r.expire("BID-#{bid}-failed", BID_EXPIRE_TTL)
265
- end
266
- end
309
+ Batch.logger.debug {"Finalize #{callback} bid: #{parent_bid}"}
310
+ enqueue_callbacks(callback, bid)
311
+ }
267
312
 
268
- if pending.to_i == failed.to_i && children == complete
269
- enqueue_callbacks(:complete, bid)
270
- end
313
+ # Handling all of these cases in one method may be a little less performant than in more specialized methods, but
314
+ # I was dealing with duplicate Redis queries and checks being made in up to 4 places and it was getting out of
315
+ # hand - this combined method should be easier to maintain and reason about
316
+
317
+ all_successful = pending_jobs.zero? && child_batches == success_batches
318
+
319
+ if all_successful || (pending_jobs == failed_jobs && child_batches == complete_batches) # All Complete
320
+ trigger_callback.call(:complete)
271
321
  end
272
322
 
273
- # Dead jobs are a Sidekiq feature.
274
- # If this is called for a job, process_failed_job was also called
275
- def process_dead_job(bid, jid)
276
- _, dead_count = redis do |r|
277
- return unless r.exists?("BID-#{bid}")
323
+ if all_successful # All Successfull
324
+ trigger_callback.call(:success)
325
+ elsif pending_jobs == dead_jobs && pending_batches == stagnated_batches # Stagnated
326
+ trigger_callback.call(:stagnated)
327
+ end
278
328
 
279
- r.multi do |r|
280
- r.sadd("BID-#{bid}-dead", jid)
281
- r.scard("BID-#{bid}-dead")
282
- r.expire("BID-#{bid}-dead", BID_EXPIRE_TTL)
283
- end
284
- end
329
+ all_results[0...precount]
330
+ end
285
331
 
286
- enqueue_callbacks(:death, bid)
332
+ def process_failed_job(bid, jid)
333
+ with_callback_check(bid, except: [:success]) do |r|
334
+ r.sadd("BID-#{bid}-failed", jid)
335
+ r.expire("BID-#{bid}-failed", BID_EXPIRE_TTL)
287
336
  end
337
+ end
288
338
 
289
- def process_successful_job(bid, jid)
290
- _, failed, pending, children, complete, success, parent_bid, keep_open = redis do |r|
291
- return unless r.exists?("BID-#{bid}")
339
+ # Dead jobs are a Sidekiq feature.
340
+ # If this is called for a job, process_failed_job was also called
341
+ def process_dead_job(bid, jid)
342
+ enqueue_callbacks(:death, bid)
292
343
 
293
- r.multi do |r|
294
- r.srem("BID-#{bid}-failed", jid)
344
+ with_callback_check(bid) do |r|
345
+ r.sadd("BID-#{bid}-dead", jid)
346
+ r.expire("BID-#{bid}-dead", BID_EXPIRE_TTL)
347
+ end
348
+ end
295
349
 
296
- r.scard("BID-#{bid}-failed")
297
- r.hincrby("BID-#{bid}", "pending", -1)
298
- r.hincrby("BID-#{bid}", "children", 0)
299
- r.scard("BID-#{bid}-batches-complete")
300
- r.scard("BID-#{bid}-batches-success")
301
- r.hget("BID-#{bid}", "parent_bid")
302
- r.hget("BID-#{bid}", "keep_open")
350
+ def process_successful_job(bid, jid)
351
+ with_callback_check(bid) do |r|
352
+ r.srem("BID-#{bid}-failed", jid)
353
+ r.hincrby("BID-#{bid}", "pending", -1)
354
+ r.hincrby("BID-#{bid}", "successful-jobs", 1)
355
+ r.zrem("BID-#{bid}-jids", jid)
356
+ end
357
+ end
303
358
 
304
- r.hincrby("BID-#{bid}", "successful-jobs", 1)
305
- r.zrem("BID-#{bid}-jids", jid)
306
- r.expire("BID-#{bid}", BID_EXPIRE_TTL)
307
- end
308
- end
359
+ def enqueue_callbacks(event, bid)
360
+ batch_key = "BID-#{bid}"
361
+ callback_key = "#{batch_key}-callbacks-#{event}"
362
+
363
+ callbacks, queue, parent_bid, callback_params = redis do |r|
364
+ return unless r.exists?(batch_key)
365
+ return if r.hget(batch_key, 'keep_open') == 'true'
309
366
 
310
- all_success = pending.to_i.zero? && children == success
311
- # if complete or successfull call complete callback (the complete callback may then call successful)
312
- if (pending.to_i == failed.to_i && children == complete) || all_success
313
- enqueue_callbacks(:complete, bid)
314
- enqueue_callbacks(:success, bid) if all_success
367
+ r.multi do |r|
368
+ r.smembers(callback_key)
369
+ r.hget(batch_key, "callback_queue")
370
+ r.hget(batch_key, "parent_bid")
371
+ r.hget(batch_key, "callback_params")
315
372
  end
316
373
  end
317
374
 
318
- def enqueue_callbacks(event, bid)
319
- batch_key = "BID-#{bid}"
320
- callback_key = "#{batch_key}-callbacks-#{event}"
375
+ queue ||= "default"
376
+ parent_bid = !parent_bid || parent_bid.empty? ? nil : parent_bid # Basically parent_bid.blank?
321
377
 
322
- callbacks, queue, parent_bid, callback_params = redis do |r|
323
- return unless r.exists?(batch_key)
324
- return if r.hget(batch_key, 'keep_open') == 'true'
378
+ # Internal callback params. If this is present, we're trying to enqueue callbacks for a callback, which is a special case that
379
+ # indicates that the callback completed and we need to close the triggering batch (which is in a done-but-not-cleaned state)
380
+ callback_params = JSON.parse(callback_params) if callback_params.present?
325
381
 
326
- r.multi do |r|
327
- r.smembers(callback_key)
328
- r.hget(batch_key, "callback_queue")
329
- r.hget(batch_key, "parent_bid")
330
- r.hget(batch_key, "callback_params")
331
- end
332
- end
382
+ # User-configured parameters/arguments to pass to the callback
383
+ callback_args = callbacks.reduce([]) do |memo, jcb|
384
+ cb = JSON.load(jcb)
385
+ memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
386
+ end
333
387
 
334
- queue ||= "default"
335
- parent_bid = !parent_bid || parent_bid.empty? ? nil : parent_bid # Basically parent_bid.blank?
388
+ opts = {"bid" => bid, "event" => event}
389
+ should_schedule_batch = callback_args.present? && !callback_params.present?
390
+ already_processed = redis do |r|
391
+ SCHEDULE_CALLBACK.call(r, [batch_key], [event.to_s, should_schedule_batch.to_s, BID_EXPIRE_TTL])
392
+ end
336
393
 
337
- # Internal callback params. If this is present, we're trying to enqueue callbacks for a callback, which is a special case that
338
- # indicates that the callback completed and we need to close the triggering batch (which is in a done-but-not-cleaned state)
339
- callback_params = JSON.parse(callback_params) if callback_params.present?
394
+ return if already_processed == 'true'
340
395
 
341
- # User-configured parameters/arguments to pass to the callback
342
- callback_args = callbacks.reduce([]) do |memo, jcb|
343
- cb = JSON.load(jcb)
344
- memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
345
- end
396
+ if should_schedule_batch
397
+ logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
346
398
 
347
- opts = {"bid" => bid, "event" => event}
348
- should_schedule_batch = callback_args.present? && !callback_params.present?
349
- already_processed = redis do |r|
350
- SCHEDULE_CALLBACK.call(r, [batch_key], [event.to_s, should_schedule_batch.to_s, BID_EXPIRE_TTL])
351
- end
399
+ # Create a new Batch to handle the callbacks and add it to the _parent_ batch
400
+ # (this ensures that the parent's lifecycle status can't change until the child's callbacks are done)
401
+ with_batch(parent_bid) do
402
+ cb_batch = self.new
403
+ cb_batch.callback_params = {
404
+ for_bid: bid,
405
+ event: event,
406
+ }
407
+ opts['callback_bid'] = cb_batch.bid
352
408
 
353
- return if already_processed == 'true'
354
-
355
- if should_schedule_batch
356
- logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
357
-
358
- # Create a new Batch to handle the callbacks and add it to the _parent_ batch
359
- # (this ensures that the parent's lifecycle status can't change until the child's callbacks are done)
360
- with_batch(parent_bid) do
361
- cb_batch = self.new
362
- cb_batch.callback_params = {
363
- for_bid: bid,
364
- event: event,
365
- }
366
- opts['callback_bid'] = cb_batch.bid
367
-
368
- logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
369
- cb_batch.jobs do
370
- push_callbacks(callback_args, queue)
371
- end
409
+ logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
410
+ cb_batch.jobs do
411
+ push_callbacks(callback_args, queue)
372
412
  end
373
413
  end
414
+ end
374
415
 
375
- if callback_params.present?
376
- # This is a callback for a callback. Passing `origin` to the Finalizer allows it to also cleanup the original/callback-triggering batch
377
- opts['origin'] = callback_params
378
- end
379
-
380
- # The Finalizer marks this batch as complete, bumps any necessary counters, cleans up this Batch _if_ no callbacks were scheduled,
381
- # and enqueues parent-Batch callbacks if needed.
382
- logger.debug {"Run batch finalizer bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
383
- finalizer = Batch::Callback::Finalize.new
384
- status = Status.new bid
385
- finalizer.dispatch(status, opts)
416
+ if callback_params.present?
417
+ # This is a callback for a callback. Passing `origin` to the Finalizer allows it to also cleanup the original/callback-triggering batch
418
+ opts['origin'] = callback_params
386
419
  end
387
420
 
388
- def cleanup_redis(bid)
389
- logger.debug {"Cleaning redis of batch #{bid}"}
390
- redis do |r|
391
- r.zrem("batches", bid)
392
- r.zrem("BID-ROOT-bids", bid)
393
- r.unlink(
394
- "BID-#{bid}",
395
- "BID-#{bid}-callbacks-complete",
396
- "BID-#{bid}-callbacks-success",
397
- "BID-#{bid}-failed",
398
- "BID-#{bid}-dead",
399
-
400
- "BID-#{bid}-batches-success",
401
- "BID-#{bid}-batches-complete",
402
- "BID-#{bid}-batches-failed",
403
- "BID-#{bid}-bids",
404
- "BID-#{bid}-jids",
405
- "BID-#{bid}-pending_callbacks",
406
- )
407
- end
421
+ # The Finalizer marks this batch as complete, bumps any necessary counters, cleans up this Batch _if_ no callbacks were scheduled,
422
+ # and enqueues parent-Batch callbacks if needed.
423
+ logger.debug {"Run batch finalizer bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
424
+ finalizer = Batch::Callback::Finalize.new
425
+ status = Status.new bid
426
+ finalizer.dispatch(status, opts)
427
+ end
428
+
429
+ def cleanup_redis(bid)
430
+ logger.debug {"Cleaning redis of batch #{bid}"}
431
+ redis do |r|
432
+ r.zrem("batches", bid)
433
+ r.zrem("BID-ROOT-bids", bid)
434
+ r.unlink(
435
+ "BID-#{bid}",
436
+ "BID-#{bid}-callbacks-complete",
437
+ "BID-#{bid}-callbacks-success",
438
+ "BID-#{bid}-failed",
439
+ "BID-#{bid}-dead",
440
+
441
+ "BID-#{bid}-batches-success",
442
+ "BID-#{bid}-batches-complete",
443
+ "BID-#{bid}-batches-failed",
444
+ "BID-#{bid}-bids",
445
+ "BID-#{bid}-jids",
446
+ "BID-#{bid}-pending_callbacks",
447
+ )
408
448
  end
449
+ end
409
450
 
410
- def delete_prematurely!(bid)
411
- child_bids = redis do |r|
412
- r.zrange("BID-#{bid}-bids", 0, -1)
413
- end
414
- child_bids.each do |cbid|
415
- delete_prematurely!(cbid)
416
- end
417
- cleanup_redis(bid)
451
+ def delete_prematurely!(bid)
452
+ child_bids = redis do |r|
453
+ r.zrange("BID-#{bid}-bids", 0, -1)
454
+ end
455
+ child_bids.each do |cbid|
456
+ delete_prematurely!(cbid)
418
457
  end
458
+ cleanup_redis(bid)
459
+ end
419
460
 
420
- # Internal method to cleanup a Redis Hash and related keys
421
- def cleanup_redis_index_for(key, suffixes = [""])
422
- if r.hget(k, "created_at").present?
423
- r.multi do |r|
424
- suffixes.each do |suffix|
425
- r.expire(key + suffix, BID_EXPIRE_TTL)
426
- end
461
+ # Internal method to cleanup a Redis Hash and related keys
462
+ def cleanup_redis_index_for(key, suffixes = [""])
463
+ if r.hget(k, "created_at").present?
464
+ r.multi do |r|
465
+ suffixes.each do |suffix|
466
+ r.expire(key + suffix, BID_EXPIRE_TTL)
427
467
  end
428
- false
429
- else
430
- r.multi do |r|
431
- suffixes.each do |suffix|
432
- r.unlink(key + suffix)
433
- end
468
+ end
469
+ false
470
+ else
471
+ r.multi do |r|
472
+ suffixes.each do |suffix|
473
+ r.unlink(key + suffix)
434
474
  end
435
- true
436
475
  end
476
+ true
437
477
  end
478
+ end
438
479
 
439
- # Administrative/console method to cleanup expired batches from the WebUI
440
- def cleanup_redis_index!
441
- suffixes = ["", "-callbacks-complete", "-callbacks-success", "-failed", "-dead", "-batches-success", "-batches-complete", "-batches-failed", "-bids", "-jids", "-pending_callbacks"]
480
+ # Administrative/console method to cleanup expired batches from the WebUI
481
+ def cleanup_redis_index!
482
+ suffixes = ["", "-callbacks-complete", "-callbacks-success", "-failed", "-dead", "-batches-success", "-batches-complete", "-batches-failed", "-bids", "-jids", "-pending_callbacks"]
442
483
 
443
- cleanup_index = ->(index) {
444
- r.zrangebyscore(index, "0", BID_EXPIRE_TTL.seconds.ago.to_i).each do |bid|
445
- r.zrem(index, bid) if cleanup_redis_index_for("BID-#{bid}", suffixes)
446
- end
447
- }
484
+ cleanup_index = ->(index) {
485
+ r.zrangebyscore(index, "0", BID_EXPIRE_TTL.seconds.ago.to_i).each do |bid|
486
+ r.zrem(index, bid) if cleanup_redis_index_for("BID-#{bid}", suffixes)
487
+ end
488
+ }
448
489
 
449
- cleanup_index.call("BID-ROOT-bids")
450
- cleanup_index.call("batches")
451
- end
490
+ cleanup_index.call("BID-ROOT-bids")
491
+ cleanup_index.call("batches")
492
+ end
452
493
 
453
- def redis(&blk)
454
- return RedisProxy.new unless block_given?
494
+ def redis(&blk)
495
+ return RedisProxy.new unless block_given?
455
496
 
456
- if Thread.current[:job_batches_redis]
457
- yield Thread.current[:job_batches_redis]
458
- else
459
- ::Bearcat.redis do |r|
460
- Thread.current[:job_batches_redis] = r
461
- yield r
462
- ensure
463
- Thread.current[:job_batches_redis] = nil
464
- end
497
+ if Thread.current[:job_batches_redis]
498
+ yield Thread.current[:job_batches_redis]
499
+ else
500
+ ::Bearcat.redis do |r|
501
+ Thread.current[:job_batches_redis] = r
502
+ yield r
503
+ ensure
504
+ Thread.current[:job_batches_redis] = nil
465
505
  end
466
506
  end
507
+ end
467
508
 
468
- def logger
469
- ::CanvasSync.logger
470
- end
509
+ def logger
510
+ ::CanvasSync.logger
511
+ end
471
512
 
472
- def push_callbacks(args, queue)
473
- Batch::Callback::worker_class.enqueue_all(args, queue)
474
- end
513
+ def push_callbacks(args, queue)
514
+ Batch::Callback::worker_class.enqueue_all(args, queue)
515
+ end
475
516
 
476
- def bid_hierarchy(bid, depth: 4, per_depth: 5, slice: nil)
477
- args = [bid, depth, per_depth]
478
- args << slice if slice
479
- redis do |r|
480
- BID_HIERARCHY.call(r, [], args)
481
- end
517
+ def bid_hierarchy(bid, depth: 4, per_depth: 5, slice: nil)
518
+ args = [bid, depth, per_depth]
519
+ args << slice if slice
520
+ redis do |r|
521
+ BID_HIERARCHY.call(r, [], args)
482
522
  end
483
523
  end
524
+ end
484
525
 
485
- class RedisProxy
486
- def multi(*args, &block)
487
- Batch.redis do |r|
488
- r.multi(*args) do |r|
489
- block.call(r)
490
- end
526
+ class RedisProxy
527
+ def multi(*args, &block)
528
+ Batch.redis do |r|
529
+ r.multi(*args) do |r|
530
+ block.call(r)
491
531
  end
492
532
  end
533
+ end
493
534
 
494
- def pipelined(*args, &block)
495
- Batch.redis do |r|
496
- r.pipelined(*args) do |r2|
497
- block.call(r2 || r)
498
- end
535
+ def pipelined(*args, &block)
536
+ Batch.redis do |r|
537
+ r.pipelined(*args) do |r2|
538
+ block.call(r2 || r)
499
539
  end
500
540
  end
541
+ end
501
542
 
502
- def uget(key)
503
- Batch.redis do |r|
504
- case r.type(key)
505
- when 'string'
506
- r.get(key)
507
- when 'list'
508
- r.lrange(key, 0, -1)
509
- when 'hash'
510
- r.hgetall(key)
511
- when 'set'
512
- r.smembers(key)
513
- when 'zset'
514
- r.zrange(key, 0, -1)
515
- end
543
+ def uget(key)
544
+ Batch.redis do |r|
545
+ case r.type(key)
546
+ when 'string'
547
+ r.get(key)
548
+ when 'list'
549
+ r.lrange(key, 0, -1)
550
+ when 'hash'
551
+ r.hgetall(key)
552
+ when 'set'
553
+ r.smembers(key)
554
+ when 'zset'
555
+ r.zrange(key, 0, -1)
516
556
  end
517
557
  end
558
+ end
518
559
 
519
- def method_missing(method_name, *arguments, &block)
520
- Batch.redis do |r|
521
- r.send(method_name, *arguments, &block)
522
- end
560
+ def method_missing(method_name, *arguments, &block)
561
+ Batch.redis do |r|
562
+ r.send(method_name, *arguments, &block)
523
563
  end
564
+ end
524
565
 
525
- def respond_to_missing?(method_name, include_private = false)
526
- super || Redis.method_defined?(method_name)
527
- end
566
+ def respond_to_missing?(method_name, include_private = false)
567
+ super || Redis.method_defined?(method_name)
528
568
  end
529
569
  end
530
570
  end
531
571
  end
532
572
 
533
- # Automatically integrate with Sidekiq if it is present.
534
- if defined?(::Sidekiq)
535
- require_relative './sidekiq'
536
- CanvasSync::JobBatches::Sidekiq.configure
537
- end
538
-
539
- # Automatically integrate with ActiveJob if it is present.
540
- if defined?(::ActiveJob)
541
- require_relative './active_job'
542
- CanvasSync::JobBatches::ActiveJob.configure
543
- end
573
+ require_relative './compat'