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.
- 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'
|