canvas_sync 0.18.12 → 0.19.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +12 -9
- data/lib/canvas_sync/concerns/ability_helper.rb +19 -11
- data/lib/canvas_sync/concerns/account/base.rb +15 -0
- data/lib/canvas_sync/job_batches/batch.rb +46 -2
- data/lib/canvas_sync/job_batches/callback.rb +7 -4
- data/lib/canvas_sync/job_batches/chain_builder.rb +38 -74
- data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +5 -5
- data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +54 -22
- data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +5 -5
- data/lib/canvas_sync/job_batches/pool.rb +83 -54
- data/lib/canvas_sync/job_batches/pool_refill.lua +40 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_jobs_table.erb +2 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/pool.erb +10 -1
- data/lib/canvas_sync/job_batches/sidekiq/web.rb +3 -1
- data/lib/canvas_sync/jobs/canvas_process_waiter.rb +3 -35
- data/lib/canvas_sync/misc_helper.rb +48 -0
- data/lib/canvas_sync/version.rb +1 -1
- data/lib/canvas_sync.rb +2 -9
- data/spec/canvas_sync/canvas_sync_spec.rb +201 -115
- data/spec/canvas_sync/jobs/canvas_process_waiter_spec.rb +0 -48
- data/spec/canvas_sync/misc_helper_spec.rb +58 -0
- data/spec/dummy/log/test.log +69092 -0
- data/spec/job_batching/pool_spec.rb +161 -0
- data/spec/job_batching/support/base_job.rb +1 -1
- metadata +10 -5
- data/lib/canvas_sync/job_batches/hincr_max.lua +0 -5
@@ -3,7 +3,7 @@ module CanvasSync
|
|
3
3
|
class Pool
|
4
4
|
include RedisModel
|
5
5
|
|
6
|
-
|
6
|
+
POOL_REFILL = RedisScript.new(Pathname.new(__FILE__) + "../pool_refill.lua")
|
7
7
|
|
8
8
|
attr_reader :pid
|
9
9
|
redis_attr :description
|
@@ -36,7 +36,7 @@ module CanvasSync
|
|
36
36
|
add_jobs([job_desc])
|
37
37
|
end
|
38
38
|
|
39
|
-
def add_jobs(job_descs)
|
39
|
+
def add_jobs(job_descs, skip_refill: false)
|
40
40
|
job_descs.each do |job_desc|
|
41
41
|
wrapper = Batch.new
|
42
42
|
wrapper.description = "Pool Job Wrapper (PID: #{pid})"
|
@@ -52,7 +52,7 @@ module CanvasSync
|
|
52
52
|
|
53
53
|
push_job_to_pool(job_desc)
|
54
54
|
end
|
55
|
-
refill_allotment
|
55
|
+
refill_allotment unless skip_refill
|
56
56
|
end
|
57
57
|
|
58
58
|
def keep_open!
|
@@ -64,19 +64,13 @@ module CanvasSync
|
|
64
64
|
let_close!
|
65
65
|
end
|
66
66
|
else
|
67
|
-
redis.hset(redis_key, 'keep_open', true)
|
67
|
+
redis.hset(redis_key, 'keep_open', 'true')
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
71
|
def let_close!
|
72
|
-
|
73
|
-
|
74
|
-
r.hincrby(redis_key, "active_count", 0)
|
75
|
-
end
|
76
|
-
|
77
|
-
if active_count == 0 && pending_count == 0
|
78
|
-
cleanup_redis if clean_when_empty
|
79
|
-
end
|
72
|
+
redis.hset(redis_key, 'keep_open', 'false')
|
73
|
+
cleanup_if_empty
|
80
74
|
end
|
81
75
|
|
82
76
|
def cleanup_redis
|
@@ -90,8 +84,26 @@ module CanvasSync
|
|
90
84
|
end
|
91
85
|
end
|
92
86
|
|
93
|
-
def
|
94
|
-
|
87
|
+
def cleanup_if_empty
|
88
|
+
self.order
|
89
|
+
|
90
|
+
activec, pactivec, pendingc, clean_when_empty, keep_open = redis.multi do |r|
|
91
|
+
r.scard("#{redis_key}-active")
|
92
|
+
r.hincrby(redis_key, "_active_count", 0)
|
93
|
+
pending_count(r)
|
94
|
+
r.hget(redis_key, 'clean_when_empty')
|
95
|
+
r.hget(redis_key, 'keep_open')
|
96
|
+
end
|
97
|
+
|
98
|
+
return if keep_open == 'true' || clean_when_empty == 'false'
|
99
|
+
|
100
|
+
if activec <= 0 && pactivec <= 0 && pendingc <= 0
|
101
|
+
cleanup_redis
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def active_count(r = redis)
|
106
|
+
r.scard("#{redis_key}-active") + r.hincrby(redis_key, "_active_count", 0)
|
95
107
|
end
|
96
108
|
|
97
109
|
def pending_count(r = redis)
|
@@ -108,24 +120,9 @@ module CanvasSync
|
|
108
120
|
end
|
109
121
|
|
110
122
|
def job_checked_in(status, options)
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
# Make sure this is loaded outside of the pipeline
|
115
|
-
self.order
|
116
|
-
|
117
|
-
redis.multi do |r|
|
118
|
-
r.hincrby(redis_key, "active_count", -1)
|
119
|
-
self.pending_count(r)
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
added_count = refill_allotment
|
124
|
-
if active_count == 0 && added_count == 0 && pending_count == 0
|
125
|
-
if clean_when_empty && redis.hget(redis_key, 'keep_open') != 'true'
|
126
|
-
cleanup_redis
|
127
|
-
end
|
128
|
-
end
|
123
|
+
redis.srem("#{redis_key}-active", status.bid)
|
124
|
+
active_count = refill_allotment
|
125
|
+
cleanup_if_empty unless active_count > 0
|
129
126
|
end
|
130
127
|
|
131
128
|
def self.job_checked_in(status, options)
|
@@ -133,6 +130,14 @@ module CanvasSync
|
|
133
130
|
from_pid(pid).job_checked_in(status, options)
|
134
131
|
end
|
135
132
|
|
133
|
+
# Administrative/console method to cleanup expired pools from the WebUI
|
134
|
+
def self.cleanup_redis_index!
|
135
|
+
suffixes = ["", "-active", "-jobs"]
|
136
|
+
r.zrangebyscore("pools", "0", Batch::BID_EXPIRE_TTL.seconds.ago.to_i).each do |pid|
|
137
|
+
r.zrem("pools", pid) if Batch.cleanup_redis_index_for("POOLID-#{pid}", suffixes)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
136
141
|
protected
|
137
142
|
|
138
143
|
def redis_key
|
@@ -140,31 +145,52 @@ module CanvasSync
|
|
140
145
|
end
|
141
146
|
|
142
147
|
def refill_allotment
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
break
|
148
|
+
active_count, job_descs = POOL_REFILL.call(redis, [redis_key, "#{redis_key}-jobs", "#{redis_key}-active"], [])
|
149
|
+
return active_count if active_count < 0
|
150
|
+
|
151
|
+
pending_job_descs = job_descs.dup
|
152
|
+
|
153
|
+
added_jobs = []
|
154
|
+
failed_to_add_jobs = []
|
155
|
+
add_exception = nil
|
156
|
+
|
157
|
+
while pending_job_descs.count > 0
|
158
|
+
begin
|
159
|
+
job_json = pending_job_descs.shift
|
160
|
+
job_desc = ::ActiveJob::Arguments.deserialize(JSON.parse(job_json))[0]&.symbolize_keys
|
161
|
+
|
162
|
+
wbid = job_desc[:pool_wrapper_batch]
|
163
|
+
|
164
|
+
Batch.new(wbid).jobs do
|
165
|
+
ChainBuilder.enqueue_job(job_desc)
|
162
166
|
end
|
167
|
+
|
168
|
+
added_jobs << wbid
|
169
|
+
rescue => ex
|
170
|
+
failed_to_add_jobs << job_json
|
171
|
+
add_exception = ex
|
163
172
|
end
|
173
|
+
end
|
174
|
+
|
175
|
+
redis.multi do |r|
|
176
|
+
r.sadd("#{redis_key}-active", added_jobs) if added_jobs.count > 0
|
177
|
+
# Release reserved slots now that we've added the jobs to `-active`
|
178
|
+
r.hincrby(redis_key, "_active_count", -job_descs.count)
|
179
|
+
|
164
180
|
r.expire(redis_key, Batch::BID_EXPIRE_TTL)
|
181
|
+
r.expire("#{redis_key}-active", Batch::BID_EXPIRE_TTL)
|
165
182
|
r.expire("#{redis_key}-jobs", Batch::BID_EXPIRE_TTL)
|
166
183
|
end
|
167
|
-
|
184
|
+
|
185
|
+
# If this happens, we end up in a bad state (as we don't try to re-add items to the pool or refill_allotment again), but
|
186
|
+
# this should be a _really_ rare case that should only occur if we've lost connection to Redis or something, so we're
|
187
|
+
# operating on the assumption that if we get here, any recovery logic will fail too
|
188
|
+
if add_exception.present?
|
189
|
+
Batch.logger.error {"Error popping jobs from Pool #{pid}: #{add_exception}"}
|
190
|
+
raise add_exception
|
191
|
+
end
|
192
|
+
|
193
|
+
active_count + added_jobs.count
|
168
194
|
end
|
169
195
|
|
170
196
|
def push_job_to_pool(job_desc)
|
@@ -183,13 +209,15 @@ module CanvasSync
|
|
183
209
|
when :priority
|
184
210
|
r.zadd(jobs_key, job_desc[:priority] || 0, job_json)
|
185
211
|
end
|
212
|
+
r.expire(redis_key, Batch::BID_EXPIRE_TTL)
|
186
213
|
r.expire(jobs_key, Batch::BID_EXPIRE_TTL)
|
187
214
|
end
|
188
215
|
end
|
189
216
|
|
217
|
+
# @deprecated
|
190
218
|
def pop_job_from_pool
|
191
219
|
jobs_key = "#{redis_key}-jobs"
|
192
|
-
order = self.order
|
220
|
+
order = self.order || 'fifo'
|
193
221
|
|
194
222
|
job_json = case order.to_sym
|
195
223
|
when :fifo
|
@@ -199,7 +227,7 @@ module CanvasSync
|
|
199
227
|
when :random
|
200
228
|
redis.spop(jobs_key)
|
201
229
|
when :priority
|
202
|
-
redis.zpopmax(jobs_key)
|
230
|
+
redis.zpopmax(jobs_key)&.[](0)
|
203
231
|
end
|
204
232
|
|
205
233
|
return nil unless job_json.present?
|
@@ -214,6 +242,7 @@ module CanvasSync
|
|
214
242
|
|
215
243
|
def flush_pending_attrs
|
216
244
|
super
|
245
|
+
redis.expire(redis_key, Batch::BID_EXPIRE_TTL)
|
217
246
|
redis.zadd("pools", created_at, pid)
|
218
247
|
end
|
219
248
|
|
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
local poolkey = KEYS[1]
|
3
|
+
local qkey = KEYS[2]
|
4
|
+
local activekey = KEYS[3]
|
5
|
+
|
6
|
+
if redis.call('EXISTS', poolkey) == 0 then
|
7
|
+
return { -1, {} } -- pool doesn't exist
|
8
|
+
end
|
9
|
+
|
10
|
+
local pool_type = redis.call('HGET', poolkey, "order")
|
11
|
+
local allotment = tonumber(redis.call("HGET", poolkey, "concurrency"))
|
12
|
+
local active = redis.call("SCARD", activekey) + (redis.call("HGET", poolkey, "_active_count") or 0)
|
13
|
+
|
14
|
+
local pop_count = allotment - active
|
15
|
+
|
16
|
+
local popped_items = {}
|
17
|
+
|
18
|
+
if pop_count > 0 then
|
19
|
+
if pool_type == "fifo" then
|
20
|
+
popped_items = redis.call("LPOP", qkey, pop_count) or {}
|
21
|
+
elseif pool_type == "lifo" then
|
22
|
+
popped_items = redis.call("RPOP", qkey, pop_count) or {}
|
23
|
+
elseif pool_type == "random" then
|
24
|
+
popped_items = redis.call("SPOP", qkey, pop_count) or {}
|
25
|
+
elseif pool_type == "priority" then
|
26
|
+
local temp_items = redis.call("ZPOPMAX", qkey, pop_count) or {}
|
27
|
+
for i,v in ipairs(temp_items) do
|
28
|
+
if i % 2 == 1 then
|
29
|
+
table.insert(popped_items, v)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
-- Reserve slots for these jobs while we return to Ruby and deserialize them
|
36
|
+
-- This could also be inlined by just storing a key in the queue and storing parameters
|
37
|
+
-- in a Hash, but this seems more efficient.
|
38
|
+
redis.call('HINCRBY', poolkey, "_active_count", #popped_items)
|
39
|
+
|
40
|
+
return { active, popped_items }
|
@@ -1,6 +1,7 @@
|
|
1
1
|
<table class="table table-striped table-bordered table-hover">
|
2
2
|
<thead>
|
3
3
|
<tr>
|
4
|
+
<th><%= t('JID') %></th>
|
4
5
|
<th><%= t('Job Class') %></th>
|
5
6
|
<th><%= t('Parameters') %></th>
|
6
7
|
</tr>
|
@@ -8,6 +9,7 @@
|
|
8
9
|
|
9
10
|
<% @jobs.each do |job_desc| %>
|
10
11
|
<tr>
|
12
|
+
<td><%= job_desc[:jid] %></td>
|
11
13
|
<td><%= job_desc['job'] %></td>
|
12
14
|
<td>
|
13
15
|
<code class="code-wrap">
|
@@ -60,6 +60,9 @@
|
|
60
60
|
<th><%= t('Job Class') %></th>
|
61
61
|
<th><%= t('Parameters') %></th>
|
62
62
|
<th><%= t('Wrapper Batch BID') %></th>
|
63
|
+
<% if @pool.order == 'priority' %>
|
64
|
+
<th><%= t('Priority') %></th>
|
65
|
+
<% end %>
|
63
66
|
</tr>
|
64
67
|
</thead>
|
65
68
|
|
@@ -68,10 +71,16 @@
|
|
68
71
|
<td><%= job_desc['job'] %></td>
|
69
72
|
<td>
|
70
73
|
<code class="code-wrap">
|
71
|
-
<div class="args-extended"
|
74
|
+
<div class="args-extended">
|
75
|
+
<%= job_desc['args']&.to_json %>
|
76
|
+
<%= job_desc['kwargs']&.to_json %>
|
77
|
+
</div>
|
72
78
|
</code>
|
73
79
|
</td>
|
74
80
|
<td><a href="<%= root_path %>batches/<%= job_desc['pool_wrapper_batch'] %>"><%= job_desc['pool_wrapper_batch'] %></a></td>
|
81
|
+
<% if @pool.order == 'priority' %>
|
82
|
+
<td><%= job_desc['priority'] %></td>
|
83
|
+
<% end %>
|
75
84
|
</tr>
|
76
85
|
<% end %>
|
77
86
|
</table>
|
@@ -45,7 +45,9 @@ module CanvasSync::JobBatches::Sidekiq
|
|
45
45
|
@sub_batches = @sub_batches.map {|b, score| CanvasSync::JobBatches::Batch.new(b) }
|
46
46
|
|
47
47
|
@current_jobs_page, @total_jobs_size, @jobs = page("BID-#{@batch.bid}-jids", params['job_page'], @count)
|
48
|
-
@jobs = @jobs.map
|
48
|
+
@jobs = @jobs.map do |jid, score|
|
49
|
+
{ jid: jid, }
|
50
|
+
end
|
49
51
|
|
50
52
|
erb(get_template(:batch))
|
51
53
|
end
|
@@ -7,9 +7,9 @@ module CanvasSync::Jobs
|
|
7
7
|
response = canvas_sync_client.get(progress_url)
|
8
8
|
status = kwargs[:status_key].present? ? response[kwargs[:status_key]] : response['workflow_state'] || response['status']
|
9
9
|
|
10
|
-
if %w[completed complete].include? status
|
10
|
+
if %w[completed complete imported imported_with_messages].include? status
|
11
11
|
InvokeCallbackWorker.perform_later(build_next_job(next_job, kwargs, response)) if next_job
|
12
|
-
elsif %w[failed error].include? status
|
12
|
+
elsif %w[failed error failed_with_messages].include? status
|
13
13
|
if kwargs[:on_failure].is_a?(Hash)
|
14
14
|
InvokeCallbackWorker.perform_later(build_next_job(kwargs[:on_failure], kwargs, response))
|
15
15
|
else
|
@@ -33,40 +33,8 @@ module CanvasSync::Jobs
|
|
33
33
|
|
34
34
|
# This is a separate job so that, if it fails and a retry is triggered, it doesn't query the API needlessly
|
35
35
|
class InvokeCallbackWorker < ActiveJob::Base
|
36
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
37
36
|
def perform(job)
|
38
|
-
job
|
39
|
-
|
40
|
-
params = job[:args] || []
|
41
|
-
params << job[:kwargs].symbolize_keys if job[:kwargs]
|
42
|
-
# params[-1] = params[-1].symbolize_keys if params[-1].is_a?(Hash)
|
43
|
-
|
44
|
-
if job[:model]
|
45
|
-
model_class = load_constant(job[:model])
|
46
|
-
find_by = job[:find_by]
|
47
|
-
target = find_by.is_a?(Hash) ? model_class.find_by(find_by) : model_class.find_by(id: find_by)
|
48
|
-
target.send(job[:method], *params)
|
49
|
-
elsif job[:class]
|
50
|
-
target = load_constant(job[:class])
|
51
|
-
target.send(job[:method], *params)
|
52
|
-
elsif job[:instance_of]
|
53
|
-
target = load_constant(job[:instance_of]).new
|
54
|
-
target.send(job[:method], *params)
|
55
|
-
elsif job[:job]
|
56
|
-
job_class = load_constant(job[:job])
|
57
|
-
job_class = job_class.set(job[:options]) if job[:options].present?
|
58
|
-
if job_class < ActiveJob::Base
|
59
|
-
job_class.perform_later(*params)
|
60
|
-
else
|
61
|
-
job_class.perform_async(*params)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
66
|
-
|
67
|
-
def load_constant(const)
|
68
|
-
const = const.constantize if const.is_a?(String)
|
69
|
-
const
|
37
|
+
CanvasSync::MiscHelper.invoke_task(job)
|
70
38
|
end
|
71
39
|
end
|
72
40
|
end
|
@@ -11,5 +11,53 @@ module CanvasSync
|
|
11
11
|
ActiveRecord::Type::Boolean.new.deserialize(v)
|
12
12
|
end
|
13
13
|
end
|
14
|
+
|
15
|
+
def self.invoke_task(job)
|
16
|
+
job = job.symbolize_keys
|
17
|
+
|
18
|
+
job_args = job[:args] || job[:parameters] || []
|
19
|
+
job_kwargs = job[:kwargs] || {}
|
20
|
+
|
21
|
+
if (mthd = job[:method]) && !(job[:class] || job[:instance_of] || job[:model])
|
22
|
+
if mthd.include?('#')
|
23
|
+
clazz, method = clazz.split("#")
|
24
|
+
job[:instance_of] = clazz
|
25
|
+
job[:method] = method
|
26
|
+
elsif mthd.include?('.')
|
27
|
+
clazz, method = mthd.split(".")
|
28
|
+
job[:class] = clazz
|
29
|
+
job[:method] = method
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if job[:model]
|
34
|
+
# TODO Support globalid
|
35
|
+
model_class = load_constant(job[:model])
|
36
|
+
find_by = job[:find_by]
|
37
|
+
target = find_by.is_a?(Hash) ? model_class.find_by(find_by) : model_class.find_by(id: find_by)
|
38
|
+
target.send(job[:method], *job_args, **job_kwargs)
|
39
|
+
elsif job[:class]
|
40
|
+
target = load_constant(job[:class])
|
41
|
+
target.send(job[:method], *job_args, **job_kwargs)
|
42
|
+
elsif job[:instance_of]
|
43
|
+
target = load_constant(job[:instance_of]).new
|
44
|
+
target.send(job[:method], *job_args, **job_kwargs)
|
45
|
+
elsif job[:job]
|
46
|
+
job_class = load_constant(job[:job])
|
47
|
+
job_class = job_class.set(job[:options]) if job[:options].present?
|
48
|
+
if job_class < ActiveJob::Base
|
49
|
+
job_class.perform_later(*job_args, **job_kwargs)
|
50
|
+
else
|
51
|
+
job_args << job_kwargs.symbolize_keys if job_kwargs
|
52
|
+
# job_args[-1] = job_args[-1].symbolize_keys if job_args[-1].is_a?(Hash)
|
53
|
+
job_class.perform_async(*job_args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.load_constant(const)
|
59
|
+
const = const.constantize if const.is_a?(String)
|
60
|
+
const
|
61
|
+
end
|
14
62
|
end
|
15
63
|
end
|
data/lib/canvas_sync/version.rb
CHANGED
data/lib/canvas_sync.rb
CHANGED
@@ -193,8 +193,7 @@ module CanvasSync
|
|
193
193
|
|
194
194
|
term_parent_chain = current_chain
|
195
195
|
|
196
|
-
per_term_chain = JobBatches::ChainBuilder.
|
197
|
-
per_term_chain.params[:term_scope] = term_scope
|
196
|
+
per_term_chain = JobBatches::ChainBuilder.build(model_job_map[:terms], term_scope: term_scope)
|
198
197
|
current_chain = per_term_chain
|
199
198
|
|
200
199
|
term_scoped_models.each do |mdl|
|
@@ -226,8 +225,6 @@ module CanvasSync
|
|
226
225
|
globals: {},
|
227
226
|
&blk
|
228
227
|
)
|
229
|
-
root_chain = JobBatches::ChainBuilder.new(CanvasSync::Jobs::BeginSyncChainJob)
|
230
|
-
|
231
228
|
global_options = {
|
232
229
|
legacy_support: legacy_support,
|
233
230
|
updated_after: updated_after,
|
@@ -237,11 +234,7 @@ module CanvasSync
|
|
237
234
|
global_options[:account_id] = account_id if account_id.present?
|
238
235
|
global_options.merge!(globals) if globals
|
239
236
|
|
240
|
-
|
241
|
-
|
242
|
-
root_chain.apply_block(&blk)
|
243
|
-
|
244
|
-
root_chain
|
237
|
+
JobBatches::ChainBuilder.build(CanvasSync::Jobs::BeginSyncChainJob, [], global_options, &blk)
|
245
238
|
end
|
246
239
|
|
247
240
|
def group_by_job_options(model_list, options_hash, only_split: nil, default_key: :provisioning)
|