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.
- checksums.yaml +4 -4
- data/lib/canvas_sync/concerns/auto_relations.rb +11 -0
- data/lib/canvas_sync/config.rb +3 -5
- data/lib/canvas_sync/generators/templates/models/rubric.rb +2 -1
- data/lib/canvas_sync/job_batches/batch.rb +432 -402
- data/lib/canvas_sync/job_batches/callback.rb +100 -114
- data/lib/canvas_sync/job_batches/chain_builder.rb +194 -196
- data/lib/canvas_sync/job_batches/{active_job.rb → compat/active_job.rb} +2 -2
- data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/helpers.rb +1 -1
- data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web.rb +3 -3
- data/lib/canvas_sync/job_batches/{sidekiq.rb → compat/sidekiq.rb} +35 -22
- data/lib/canvas_sync/job_batches/compat.rb +20 -0
- data/lib/canvas_sync/job_batches/context_hash.rb +124 -126
- data/lib/canvas_sync/job_batches/jobs/base_job.rb +2 -4
- data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +14 -16
- data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +125 -127
- data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +14 -16
- data/lib/canvas_sync/job_batches/pool.rb +193 -195
- data/lib/canvas_sync/job_batches/redis_model.rb +50 -52
- data/lib/canvas_sync/job_batches/redis_script.rb +129 -131
- data/lib/canvas_sync/job_batches/status.rb +85 -87
- data/lib/canvas_sync/job_uniqueness/compat/active_job.rb +75 -0
- data/lib/canvas_sync/job_uniqueness/compat/sidekiq.rb +135 -0
- data/lib/canvas_sync/job_uniqueness/compat.rb +20 -0
- data/lib/canvas_sync/job_uniqueness/configuration.rb +25 -0
- data/lib/canvas_sync/job_uniqueness/job_uniqueness.rb +47 -0
- data/lib/canvas_sync/job_uniqueness/lock_context.rb +171 -0
- data/lib/canvas_sync/job_uniqueness/locksmith.rb +92 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/base.rb +32 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/log.rb +13 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/null_strategy.rb +9 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/raise.rb +11 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/reject.rb +21 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/reschedule.rb +20 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict.rb +41 -0
- data/lib/canvas_sync/job_uniqueness/strategy/base.rb +104 -0
- data/lib/canvas_sync/job_uniqueness/strategy/until_and_while_executing.rb +35 -0
- data/lib/canvas_sync/job_uniqueness/strategy/until_executed.rb +20 -0
- data/lib/canvas_sync/job_uniqueness/strategy/until_executing.rb +20 -0
- data/lib/canvas_sync/job_uniqueness/strategy/until_expired.rb +16 -0
- data/lib/canvas_sync/job_uniqueness/strategy/while_executing.rb +26 -0
- data/lib/canvas_sync/job_uniqueness/strategy.rb +27 -0
- data/lib/canvas_sync/job_uniqueness/unique_job_common.rb +79 -0
- data/lib/canvas_sync/misc_helper.rb +1 -1
- data/lib/canvas_sync/version.rb +1 -1
- data/lib/canvas_sync.rb +4 -3
- data/spec/dummy/app/models/rubric.rb +2 -1
- data/spec/dummy/config/environments/test.rb +1 -1
- data/spec/job_batching/batch_spec.rb +49 -7
- data/spec/job_batching/{active_job_spec.rb → compat/active_job_spec.rb} +2 -2
- data/spec/job_batching/{sidekiq_spec.rb → compat/sidekiq_spec.rb} +14 -12
- data/spec/job_batching/flow_spec.rb +1 -1
- data/spec/job_batching/integration_helper.rb +1 -1
- data/spec/job_batching/status_spec.rb +2 -2
- data/spec/job_uniqueness/compat/active_job_spec.rb +49 -0
- data/spec/job_uniqueness/compat/sidekiq_spec.rb +68 -0
- data/spec/job_uniqueness/lock_context_spec.rb +95 -0
- data/spec/job_uniqueness/on_conflict/log_spec.rb +11 -0
- data/spec/job_uniqueness/on_conflict/raise_spec.rb +10 -0
- data/spec/job_uniqueness/on_conflict/reschedule_spec.rb +24 -0
- data/spec/job_uniqueness/on_conflict_spec.rb +16 -0
- data/spec/job_uniqueness/spec_helper.rb +14 -0
- data/spec/job_uniqueness/strategy/base_spec.rb +100 -0
- data/spec/job_uniqueness/strategy/until_and_while_executing_spec.rb +48 -0
- data/spec/job_uniqueness/strategy/until_executed_spec.rb +23 -0
- data/spec/job_uniqueness/strategy/until_executing_spec.rb +23 -0
- data/spec/job_uniqueness/strategy/until_expired_spec.rb +23 -0
- data/spec/job_uniqueness/strategy/while_executing_spec.rb +33 -0
- data/spec/job_uniqueness/support/lock_strategy.rb +28 -0
- data/spec/job_uniqueness/support/on_conflict.rb +24 -0
- data/spec/job_uniqueness/support/test_worker.rb +19 -0
- data/spec/job_uniqueness/unique_job_common_spec.rb +45 -0
- data/spec/spec_helper.rb +1 -1
- metadata +278 -204
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/css/styles.less +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/js/batch_tree.js +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/js/util.js +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_batch_tree.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_batches_table.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_common.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_jobs_table.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_pagination.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/batch.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/batches.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/pool.erb +0 -0
- /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
|
-
|
21
|
-
CURRENT_BATCH_THREAD_KEY = :job_batches_batch
|
14
|
+
module CanvasSync::JobBatches
|
15
|
+
CURRENT_BATCH_THREAD_KEY = :job_batches_batch
|
22
16
|
|
23
|
-
|
24
|
-
|
17
|
+
class Batch
|
18
|
+
include RedisModel
|
25
19
|
|
26
|
-
|
20
|
+
class NoBlockGivenError < StandardError; end
|
27
21
|
|
28
|
-
|
22
|
+
delegate :redis, to: :class
|
29
23
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
29
|
+
attr_reader :bid
|
36
30
|
|
37
|
-
|
38
|
-
|
39
|
-
|
31
|
+
def self.current
|
32
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY]
|
33
|
+
end
|
40
34
|
|
41
|
-
|
42
|
-
|
43
|
-
|
35
|
+
def self.current_context
|
36
|
+
self.current&.context
|
37
|
+
end
|
44
38
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
60
|
-
|
53
|
+
def context
|
54
|
+
return @context if defined?(@context)
|
61
55
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
75
|
-
|
68
|
+
value = {} if value.nil?
|
69
|
+
value = value.local if value.is_a?(ContextHash)
|
76
70
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
71
|
+
@context ||= ContextHash.new(bid, {})
|
72
|
+
@context.set_local(value)
|
73
|
+
# persist_bid_attr('context', JSON.unparse(@context.local))
|
74
|
+
end
|
81
75
|
|
82
|
-
|
83
|
-
|
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.
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
99
|
-
|
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
|
-
|
102
|
-
|
133
|
+
nil
|
134
|
+
end
|
103
135
|
|
104
|
-
|
105
|
-
|
106
|
-
|
136
|
+
def increment_job_queue(jid)
|
137
|
+
assert_batch_is_open
|
138
|
+
append_jobs([jid])
|
139
|
+
end
|
107
140
|
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
118
|
-
|
145
|
+
def parent_bid
|
146
|
+
redis.hget(@bidkey, "parent_bid")
|
147
|
+
end
|
119
148
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
127
|
-
Thread.current[CURRENT_BATCH_THREAD_KEY] = self
|
163
|
+
keep_open!
|
128
164
|
yield
|
129
165
|
ensure
|
130
|
-
|
166
|
+
let_close!
|
131
167
|
end
|
132
|
-
|
133
|
-
|
168
|
+
else
|
169
|
+
redis.hset(@bidkey, 'keep_open', "true")
|
134
170
|
end
|
171
|
+
end
|
135
172
|
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
142
|
-
|
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
|
-
|
146
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
208
|
+
def redis_key
|
209
|
+
@bidkey
|
210
|
+
end
|
176
211
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
@bidkey
|
242
|
+
class << self
|
243
|
+
def current
|
244
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY]
|
210
245
|
end
|
211
246
|
|
212
|
-
def
|
213
|
-
|
214
|
-
redis.zadd("batches", created_at, bid) if INDEX_ALL_BATCHES
|
247
|
+
def current_context
|
248
|
+
current&.context || {}
|
215
249
|
end
|
216
250
|
|
217
|
-
|
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
|
-
|
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
|
-
|
227
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
r.
|
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
|
-
|
243
|
-
|
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
|
-
|
248
|
-
|
249
|
-
|
294
|
+
# "pending" = not successful (yet)
|
295
|
+
# "failed" = dead or retrying
|
296
|
+
# "complete" = successful or failed
|
250
297
|
|
251
|
-
|
252
|
-
|
253
|
-
|
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
|
-
|
256
|
-
r.sadd("BID-#{bid}-failed", jid)
|
303
|
+
pending_batches = child_batches - success_batches
|
257
304
|
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
265
|
-
|
266
|
-
|
309
|
+
Batch.logger.debug {"Finalize #{callback} bid: #{parent_bid}"}
|
310
|
+
enqueue_callbacks(callback, bid)
|
311
|
+
}
|
267
312
|
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
-
|
280
|
-
|
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
|
-
|
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
|
-
|
290
|
-
|
291
|
-
|
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
|
-
|
294
|
-
|
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
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
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
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
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
|
-
|
319
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
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
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
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
|
-
|
335
|
-
|
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
|
-
|
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
|
-
|
342
|
-
|
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
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
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
|
-
|
354
|
-
|
355
|
-
|
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
|
-
|
376
|
-
|
377
|
-
|
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
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
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
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
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
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
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
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
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
|
-
|
440
|
-
|
441
|
-
|
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
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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
|
-
|
450
|
-
|
451
|
-
|
490
|
+
cleanup_index.call("BID-ROOT-bids")
|
491
|
+
cleanup_index.call("batches")
|
492
|
+
end
|
452
493
|
|
453
|
-
|
454
|
-
|
494
|
+
def redis(&blk)
|
495
|
+
return RedisProxy.new unless block_given?
|
455
496
|
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|
-
|
469
|
-
|
470
|
-
|
509
|
+
def logger
|
510
|
+
::CanvasSync.logger
|
511
|
+
end
|
471
512
|
|
472
|
-
|
473
|
-
|
474
|
-
|
513
|
+
def push_callbacks(args, queue)
|
514
|
+
Batch::Callback::worker_class.enqueue_all(args, queue)
|
515
|
+
end
|
475
516
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
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
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
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
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
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
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
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
|
-
|
520
|
-
|
521
|
-
|
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
|
-
|
526
|
-
|
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
|
-
|
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'
|