canvas_sync 0.21.1.beta1 → 0.22.0.beta1

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