joblin 0.1.11 → 0.2.0
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/app/models/joblin/background_task/api_access.rb +15 -2
- data/app/models/joblin/background_task/attachments.rb +2 -2
- data/app/models/joblin/background_task/retention_policy.rb +12 -4
- data/app/models/joblin/background_task.rb +8 -1
- data/db/migrate/20260709120000_add_indexes_to_joblin_background_tasks.rb +10 -0
- data/lib/joblin/batching/batch.rb +60 -33
- data/lib/joblin/batching/callback.rb +9 -1
- data/lib/joblin/batching/chain_builder.rb +3 -2
- data/lib/joblin/batching/compat/sidekiq/web/views/_batches_table.erb +2 -2
- data/lib/joblin/batching/compat/sidekiq/web/views/_jobs_table.erb +3 -3
- data/lib/joblin/batching/compat/sidekiq/web/views/batch.erb +5 -5
- data/lib/joblin/batching/compat/sidekiq/web/views/pool.erb +13 -13
- data/lib/joblin/batching/compat/sidekiq/web/views/pools.erb +3 -3
- data/lib/joblin/batching/compat/sidekiq/web.rb +17 -1
- data/lib/joblin/batching/context_hash.rb +1 -1
- data/lib/joblin/batching/jobs/managed_batch_job.rb +8 -2
- data/lib/joblin/batching/pool.rb +12 -3
- data/lib/joblin/batching/pool_refill.lua +2 -1
- data/lib/joblin/batching/schedule_callback.lua +6 -0
- data/lib/joblin/lazy_access.rb +108 -19
- data/lib/joblin/uniqueness/compat/active_job.rb +1 -1
- data/lib/joblin/uniqueness/job_uniqueness.rb +2 -0
- data/lib/joblin/uniqueness/lock_context.rb +4 -0
- data/lib/joblin/uniqueness/on_conflict/log.rb +1 -0
- data/lib/joblin/uniqueness/on_conflict/reject.rb +8 -1
- data/lib/joblin/uniqueness/strategy/until_expired.rb +1 -0
- data/lib/joblin/uniqueness/unique_job_common.rb +15 -7
- data/lib/joblin/version.rb +1 -1
- data/spec/internal/log/test.log +6619 -0
- data/spec/lazy_access_spec.rb +76 -0
- data/spec/models/api_access_spec.rb +57 -0
- data/spec/uniqueness/on_conflict/reject_spec.rb +42 -0
- data/spec/uniqueness/strategy/until_expired_spec.rb +7 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e87b736e32f7b502592a72a08da939d0a2ef95302e96b491f8d258bcdafcfab1
|
|
4
|
+
data.tar.gz: 97df4d3166bb653f6362d11b600c91a805c4cdc368091690265ae2c686c136d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 352592cc3733c55879d51ac118c242184140d174f7873b632a6695a7536a641493a3515377a5b838bc1d27e22c9cb5092517b8d4096098b96cfc124561ea558e
|
|
7
|
+
data.tar.gz: a818e8053990c09106eb47a21ca8bd70aeea51a21a3d21f8a14de54bd6f03e7aa4931913d145b90db8a8c3907eb1bb78ae0dd4f25cc3e07450d120bead87cde8
|
|
@@ -19,6 +19,7 @@ module Joblin
|
|
|
19
19
|
def allow_api_access!(&blk)
|
|
20
20
|
include Mixin unless self < Mixin
|
|
21
21
|
@api_access_allowed = true
|
|
22
|
+
BackgroundTask.api_accessible_types[name] = self if name
|
|
22
23
|
api_access_rules(&blk) if blk
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -27,6 +28,15 @@ module Joblin
|
|
|
27
28
|
@api_access_allowed
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
# Registry (name => class) of task types that opted into API access.
|
|
32
|
+
# build_from_api resolves params[:type] against this instead of
|
|
33
|
+
# constantizing arbitrary user input, so an unknown/hostile type can't
|
|
34
|
+
# autoload arbitrary constants.
|
|
35
|
+
def api_accessible_types
|
|
36
|
+
BackgroundTask.instance_variable_get(:@api_accessible_types) ||
|
|
37
|
+
BackgroundTask.instance_variable_set(:@api_accessible_types, {})
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
def find_for_api(id)
|
|
31
41
|
task = find(id)
|
|
32
42
|
raise ActiveRecord::RecordNotFound unless task.api_access_allowed?
|
|
@@ -41,7 +51,7 @@ module Joblin
|
|
|
41
51
|
params ||= controller_or_params
|
|
42
52
|
end
|
|
43
53
|
|
|
44
|
-
task_type = self == BackgroundTask ? params[:type].
|
|
54
|
+
task_type = self == BackgroundTask ? api_accessible_types[params[:type].to_s] : self
|
|
45
55
|
raise ActiveRecord::RecordNotFound unless task_type && task_type <= BackgroundTask
|
|
46
56
|
|
|
47
57
|
task = task_type.new
|
|
@@ -68,7 +78,10 @@ module Joblin
|
|
|
68
78
|
raise ActiveRecord::RecordInvalid.new(task) unless task.api_access_allowed?
|
|
69
79
|
|
|
70
80
|
val_errors = task.api_validate_options
|
|
71
|
-
|
|
81
|
+
unless val_errors.empty?
|
|
82
|
+
val_errors.each { |msg| task.errors.add(:base, msg) }
|
|
83
|
+
raise ActiveRecord::RecordInvalid.new(task)
|
|
84
|
+
end
|
|
72
85
|
|
|
73
86
|
task
|
|
74
87
|
end
|
|
@@ -38,9 +38,9 @@ module Joblin
|
|
|
38
38
|
save_as = save_as || send(key).filename.to_s || key.to_s
|
|
39
39
|
save_as = File.join(working_dir, save_as) unless Pathname.new(save_as).absolute?
|
|
40
40
|
|
|
41
|
-
File.open(save_as, '
|
|
41
|
+
File.open(save_as, 'wb') do |file|
|
|
42
42
|
send(key).download do |chunk|
|
|
43
|
-
file.write(chunk
|
|
43
|
+
file.write(chunk)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
save_as
|
|
@@ -16,12 +16,20 @@ module Joblin
|
|
|
16
16
|
class BackgroundTaskCleaner < ActiveJob::Base
|
|
17
17
|
def perform
|
|
18
18
|
types = BackgroundTask.distinct.pluck(:type)
|
|
19
|
-
types.each do |
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
types.each do |type_name|
|
|
20
|
+
klass = type_name ? type_name.safe_constantize : BackgroundTask
|
|
21
|
+
# Skip rows whose class has been renamed/removed rather than aborting
|
|
22
|
+
# the whole cleanup run.
|
|
23
|
+
next unless klass && klass <= BackgroundTask
|
|
24
|
+
|
|
25
|
+
rp = klass.record_retention
|
|
22
26
|
next unless rp
|
|
23
27
|
|
|
24
|
-
type
|
|
28
|
+
# Scope by the exact type column so a parent policy doesn't reap
|
|
29
|
+
# subclass rows that declared their own (longer) retention.
|
|
30
|
+
BackgroundTask.where(type: type_name)
|
|
31
|
+
.where("created_at < ?", rp.ago)
|
|
32
|
+
.destroy_all
|
|
25
33
|
end
|
|
26
34
|
end
|
|
27
35
|
end
|
|
@@ -22,12 +22,19 @@ module Joblin
|
|
|
22
22
|
joins("LEFT OUTER JOIN #{t} AS t2 ON #{t}.type = t2.type AND #{t}.created_at < t2.created_at")
|
|
23
23
|
.group(:id).order(:type, created_at: :desc).having('count(*) < ?', history_length)
|
|
24
24
|
else
|
|
25
|
+
# DISTINCT ON requires the distinct expression to lead the ORDER BY;
|
|
26
|
+
# created_at DESC then picks the most recent row per type.
|
|
25
27
|
select("DISTINCT ON (#{t}.type) #{t}.*")
|
|
28
|
+
.order(Arel.sql("#{t}.type, #{t}.created_at DESC"))
|
|
26
29
|
end
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
def enqueue!
|
|
30
|
-
|
|
33
|
+
# Persist the transition here so it isn't lost when enter_batch reuses an
|
|
34
|
+
# existing batch_id (and doesn't otherwise write the record) — e.g. a
|
|
35
|
+
# re-enqueue after failure must not stay `failed`.
|
|
36
|
+
self.workflow_state = 'scheduled'
|
|
37
|
+
save! if changed?
|
|
31
38
|
|
|
32
39
|
enter_batch do
|
|
33
40
|
self.class::ExecutorJob.perform_later()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AddIndexesToJoblinBackgroundTasks < Joblin::MiscHelper::MigrationClass
|
|
2
|
+
def change
|
|
3
|
+
# Retention cleanup filters on (type, created_at) and grouped_history orders
|
|
4
|
+
# by the same pair; a composite index keeps both off a sequential scan.
|
|
5
|
+
add_index :joblin_background_tasks, [:type, :created_at], if_not_exists: true
|
|
6
|
+
|
|
7
|
+
# Batch callbacks look tasks up by batch_id.
|
|
8
|
+
add_index :joblin_background_tasks, :batch_id, if_not_exists: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -221,7 +221,7 @@ module Joblin::Batching
|
|
|
221
221
|
unless defined?(@closed)
|
|
222
222
|
@closed = redis.hget(@bidkey, 'complete') == 'true'
|
|
223
223
|
end
|
|
224
|
-
raise "Cannot add jobs to Batch #{}
|
|
224
|
+
raise "Cannot add jobs to Batch #{bid} - it has already entered the callback-stage" if @closed
|
|
225
225
|
end
|
|
226
226
|
|
|
227
227
|
def persist!
|
|
@@ -236,6 +236,7 @@ module Joblin::Batching
|
|
|
236
236
|
r.hincrby("BID-#{parent_bid}", "children", 1)
|
|
237
237
|
r.expire("BID-#{parent_bid}", BID_EXPIRE_TTL)
|
|
238
238
|
r.zadd("BID-#{parent_bid}-bids", created_at, bid)
|
|
239
|
+
r.expire("BID-#{parent_bid}-bids", BID_EXPIRE_TTL)
|
|
239
240
|
else
|
|
240
241
|
r.zadd("BID-ROOT-bids", created_at, bid)
|
|
241
242
|
end
|
|
@@ -275,38 +276,64 @@ module Joblin::Batching
|
|
|
275
276
|
|
|
276
277
|
precount = 0
|
|
277
278
|
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
# WATCH the batch hash so this transaction (the caller's mutations *and*
|
|
280
|
+
# the state reads/expirations, some of which — e.g. hincrby "pending" —
|
|
281
|
+
# would otherwise recreate the hash) aborts if a concurrent cleanup_redis
|
|
282
|
+
# deletes the batch after we've checked it exists. An aborted EXEC applies
|
|
283
|
+
# nothing, so re-running the block on retry is safe (no double-apply).
|
|
284
|
+
# Concurrent mutations of the *same* bid can also abort us; we retry a
|
|
285
|
+
# bounded number of times and give up loudly rather than spin forever.
|
|
286
|
+
max_attempts = 50
|
|
287
|
+
all_results =
|
|
288
|
+
redis do |r|
|
|
289
|
+
attempts = 0
|
|
290
|
+
loop do
|
|
291
|
+
attempts += 1
|
|
292
|
+
result = r.watch("BID-#{bid}") do |rc|
|
|
293
|
+
if rc.exists?("BID-#{bid}")
|
|
294
|
+
rc.multi do |r|
|
|
295
|
+
blk.call(r)
|
|
296
|
+
futures = r.instance_variable_get(:@futures) || r.instance_variable_get(:@pipeline)&.futures || r.instance_variable_get(:@client)&.futures
|
|
297
|
+
precount = futures.size
|
|
298
|
+
|
|
299
|
+
# Misc
|
|
300
|
+
r.hget("BID-#{bid}", "parent_bid")
|
|
301
|
+
r.hget("BID-#{bid}", "keep_open")
|
|
302
|
+
r.scard("BID-#{bid}-holds")
|
|
303
|
+
|
|
304
|
+
# Jobs
|
|
305
|
+
r.hincrby("BID-#{bid}", "pending", 0)
|
|
306
|
+
r.scard("BID-#{bid}-failed")
|
|
307
|
+
r.scard("BID-#{bid}-dead")
|
|
308
|
+
|
|
309
|
+
# Batches
|
|
310
|
+
r.hincrby("BID-#{bid}", "children", 0)
|
|
311
|
+
r.scard("BID-#{bid}-batches-complete")
|
|
312
|
+
r.scard("BID-#{bid}-batches-success")
|
|
313
|
+
r.scard("BID-#{bid}-batches-failed")
|
|
314
|
+
r.scard("BID-#{bid}-batches-stagnated")
|
|
315
|
+
|
|
316
|
+
# Touch Expirations
|
|
317
|
+
r.expire("BID-#{bid}", BID_EXPIRE_TTL)
|
|
318
|
+
r.expire("BID-#{bid}-batches-success", BID_EXPIRE_TTL)
|
|
319
|
+
end
|
|
320
|
+
else
|
|
321
|
+
# Batch was cleaned up; nothing to process.
|
|
322
|
+
rc.unwatch
|
|
323
|
+
:missing
|
|
324
|
+
end
|
|
325
|
+
end
|
|
280
326
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
# Misc
|
|
289
|
-
r.hget("BID-#{bid}", "parent_bid")
|
|
290
|
-
r.hget("BID-#{bid}", "keep_open")
|
|
291
|
-
r.scard("BID-#{bid}-holds")
|
|
292
|
-
|
|
293
|
-
# Jobs
|
|
294
|
-
r.hincrby("BID-#{bid}", "pending", 0)
|
|
295
|
-
r.scard("BID-#{bid}-failed")
|
|
296
|
-
r.scard("BID-#{bid}-dead")
|
|
297
|
-
|
|
298
|
-
# Batches
|
|
299
|
-
r.hincrby("BID-#{bid}", "children", 0)
|
|
300
|
-
r.scard("BID-#{bid}-batches-complete")
|
|
301
|
-
r.scard("BID-#{bid}-batches-success")
|
|
302
|
-
r.scard("BID-#{bid}-batches-failed")
|
|
303
|
-
r.scard("BID-#{bid}-batches-stagnated")
|
|
304
|
-
|
|
305
|
-
# Touch Expirations
|
|
306
|
-
r.expire("BID-#{bid}", BID_EXPIRE_TTL)
|
|
307
|
-
r.expire("BID-#{bid}-batches-success", BID_EXPIRE_TTL)
|
|
327
|
+
# :missing => batch gone; Array => committed; nil => EXEC aborted (retry).
|
|
328
|
+
break result unless result.nil?
|
|
329
|
+
|
|
330
|
+
if attempts >= max_attempts
|
|
331
|
+
raise "with_callback_check: gave up after #{attempts} attempts due to contention on BID-#{bid}"
|
|
332
|
+
end
|
|
333
|
+
end
|
|
308
334
|
end
|
|
309
|
-
|
|
335
|
+
|
|
336
|
+
return if all_results == :missing
|
|
310
337
|
|
|
311
338
|
# Exclude return values from the passed block
|
|
312
339
|
actual_results = all_results[precount..-1]
|
|
@@ -407,7 +434,7 @@ module Joblin::Batching
|
|
|
407
434
|
|
|
408
435
|
# User-configured parameters/arguments to pass to the callback
|
|
409
436
|
callback_args = callbacks.reduce([]) do |memo, jcb|
|
|
410
|
-
cb = JSON.
|
|
437
|
+
cb = JSON.parse(jcb)
|
|
411
438
|
memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
|
|
412
439
|
end
|
|
413
440
|
|
|
@@ -487,7 +514,7 @@ module Joblin::Batching
|
|
|
487
514
|
# Internal method to cleanup a Redis Hash and related keys
|
|
488
515
|
def cleanup_redis_index_for(key, suffixes = [""])
|
|
489
516
|
redis do |r|
|
|
490
|
-
if r.hget(
|
|
517
|
+
if r.hget(key, "created_at").present?
|
|
491
518
|
r.multi do |r|
|
|
492
519
|
suffixes.each do |suffix|
|
|
493
520
|
r.expire(key + suffix, BID_EXPIRE_TTL)
|
|
@@ -26,7 +26,12 @@ module Joblin::Batching
|
|
|
26
26
|
method ||= "on_#{event}"
|
|
27
27
|
status = Batch::Status.new(bid)
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# safe_constantize returns nil (rather than raising NameError) for an
|
|
30
|
+
# unknown/typo'd class, so a bad callback name logs a warning instead
|
|
31
|
+
# of retrying forever and stranding the triggering batch.
|
|
32
|
+
object = clazz.is_a?(String) ? clazz.safe_constantize : clazz
|
|
33
|
+
|
|
34
|
+
if object
|
|
30
35
|
target = target == :instance ? object.new : object
|
|
31
36
|
if target.respond_to?(method, true)
|
|
32
37
|
target.send(method, status, opts.with_indifferent_access)
|
|
@@ -87,6 +92,7 @@ module Joblin::Batching
|
|
|
87
92
|
r.sadd("BID-#{parent_bid}-batches-success", bid)
|
|
88
93
|
r.srem("BID-#{parent_bid}-batches-failed", bid)
|
|
89
94
|
r.sadd("BID-#{parent_bid}-batches-complete", bid)
|
|
95
|
+
r.expire("BID-#{parent_bid}-batches-complete", BID_EXPIRE_TTL)
|
|
90
96
|
end
|
|
91
97
|
end
|
|
92
98
|
|
|
@@ -113,6 +119,8 @@ module Joblin::Batching
|
|
|
113
119
|
Batch.with_callback_check(parent_bid, except: [:success]) do |r|
|
|
114
120
|
r.sadd("BID-#{parent_bid}-batches-complete", bid)
|
|
115
121
|
r.sadd("BID-#{parent_bid}-batches-failed", bid)
|
|
122
|
+
r.expire("BID-#{parent_bid}-batches-complete", BID_EXPIRE_TTL)
|
|
123
|
+
r.expire("BID-#{parent_bid}-batches-failed", BID_EXPIRE_TTL)
|
|
116
124
|
end
|
|
117
125
|
end
|
|
118
126
|
end
|
|
@@ -79,11 +79,12 @@ module Joblin::Batching
|
|
|
79
79
|
|
|
80
80
|
chain = self.class.get_chain_parameter(parent_job)
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
# Compare by name: parent_job[:job] may be a Class or its String name.
|
|
83
|
+
if parent_job[:job].to_s != needed_parent_type.to_s
|
|
83
84
|
old_job = chain[sub_index]
|
|
84
85
|
parent_job = chain[sub_index] = {
|
|
85
86
|
job: needed_parent_type,
|
|
86
|
-
|
|
87
|
+
args: [],
|
|
87
88
|
}
|
|
88
89
|
sub_index = 0
|
|
89
90
|
chain = self.class.get_chain_parameter(parent_job)
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
<% status = Joblin::Batching::Batch::Status.new(batch) %>
|
|
27
27
|
<tr>
|
|
28
28
|
<td><%= safe_relative_time(batch.created_at.to_f) %></th>
|
|
29
|
-
<td><a href="<%= root_path %>batches/<%= batch.bid %>"><%= batch.bid %></a></td>
|
|
30
|
-
<td><%= batch.description %></th>
|
|
29
|
+
<td><a href="<%= root_path %>batches/<%= h(batch.bid) %>"><%= h(batch.bid) %></a></td>
|
|
30
|
+
<td><%= h(batch.description) %></th>
|
|
31
31
|
|
|
32
32
|
<td><%= status.pending %></th>
|
|
33
33
|
<td><%= status.failures %></th>
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
<% @jobs.each do |job_desc| %>
|
|
11
11
|
<tr>
|
|
12
|
-
<td><%= job_desc[:jid] %></td>
|
|
13
|
-
<td><%= job_desc['job'] %></td>
|
|
12
|
+
<td><%= h(job_desc[:jid]) %></td>
|
|
13
|
+
<td><%= h(job_desc['job']) %></td>
|
|
14
14
|
<td>
|
|
15
15
|
<code class="code-wrap">
|
|
16
|
-
<div class="args-extended"><%= job_desc['parameters'].to_json
|
|
16
|
+
<div class="args-extended"><%= h(job_desc['parameters'].to_json) %></div>
|
|
17
17
|
</code>
|
|
18
18
|
</td>
|
|
19
19
|
</tr>
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
<tbody>
|
|
9
9
|
<tr>
|
|
10
10
|
<th colspan="2" scope=row><%= t('Batch') %></td>
|
|
11
|
-
<td><%= @batch.bid %></td>
|
|
11
|
+
<td><%= h(@batch.bid) %></td>
|
|
12
12
|
</tr>
|
|
13
13
|
<tr>
|
|
14
14
|
<th colspan="2" scope=row><%= t('Parent') %></td>
|
|
15
15
|
<td>
|
|
16
16
|
<% if @batch.parent_bid.present? %>
|
|
17
|
-
<a href="<%= root_path %>batches/<%= @batch.parent_bid %>"><%= @batch.parent_bid %></a>
|
|
17
|
+
<a href="<%= root_path %>batches/<%= h(@batch.parent_bid) %>"><%= h(@batch.parent_bid) %></a>
|
|
18
18
|
<% else %>
|
|
19
19
|
ROOT
|
|
20
20
|
<% end %>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
</tr>
|
|
27
27
|
<tr>
|
|
28
28
|
<th colspan="2" scope=row><%= t('Description') %></td>
|
|
29
|
-
<td><%= @batch.description %></td>
|
|
29
|
+
<td><%= h(@batch.description) %></td>
|
|
30
30
|
</tr>
|
|
31
31
|
<tr>
|
|
32
32
|
<th colspan="2" scope=row><%= t('Context') %></td>
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
%>
|
|
64
64
|
<% if @jobs.any? && @total_size > @count.to_i %>
|
|
65
65
|
<div class="col-sm-4">
|
|
66
|
-
<%= erb get_template(:_pagination), locals: { url: "#{root_path}batches/#{@batch.bid}", page_key: :job_page } %>
|
|
66
|
+
<%= erb get_template(:_pagination), locals: { url: "#{root_path}batches/#{h(@batch.bid)}", page_key: :job_page } %>
|
|
67
67
|
</div>
|
|
68
68
|
<% end %>
|
|
69
69
|
</header>
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
</div>
|
|
75
75
|
<% end %>
|
|
76
76
|
|
|
77
|
-
<form class="form-horizontal" action="<%= root_path %>batches/<%= @batch.bid %>" method="post">
|
|
77
|
+
<form class="form-horizontal" action="<%= root_path %>batches/<%= h(@batch.bid) %>" method="post">
|
|
78
78
|
<%= csrf_tag %>
|
|
79
79
|
<a class="btn btn-default" href="<%= root_path %>batches"><%= t('GoBack') %></a>
|
|
80
80
|
<input class="btn btn-danger" type="submit" name="delete" value="<%= t('Delete') %>" data-confirm="<%= t('AreYouSure') %>" />
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
</tr>
|
|
14
14
|
<tr>
|
|
15
15
|
<th scope=row><%= t('Description') %></td>
|
|
16
|
-
<td><%= @pool.description %></td>
|
|
16
|
+
<td><%= h(@pool.description) %></td>
|
|
17
17
|
</tr>
|
|
18
18
|
<tr>
|
|
19
19
|
<th scope=row><%= t('Type') %></td>
|
|
20
|
-
<td><%= @pool.order.to_s.upcase %></td>
|
|
20
|
+
<td><%= h(@pool.order.to_s.upcase) %></td>
|
|
21
21
|
</tr>
|
|
22
22
|
<tr>
|
|
23
23
|
<th scope=row><%= t('Concurrency') %></td>
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
|
|
62
62
|
<% @active_tasks.each do |job_desc| %>
|
|
63
63
|
<tr>
|
|
64
|
-
<td><a href="<%= root_path %>batches/<%= job_desc['pool_wrapper_batch'] %>"><%= job_desc['pool_wrapper_batch'] %></a></td>
|
|
65
|
-
<td><%= job_desc['job'] %></td>
|
|
64
|
+
<td><a href="<%= root_path %>batches/<%= h(job_desc['pool_wrapper_batch']) %>"><%= h(job_desc['pool_wrapper_batch']) %></a></td>
|
|
65
|
+
<td><%= h(job_desc['job']) %></td>
|
|
66
66
|
<td>
|
|
67
67
|
<code class="code-wrap">
|
|
68
68
|
<div class="args-extended">
|
|
69
|
-
<%= job_desc['args']&.to_json %>
|
|
70
|
-
<%= job_desc['kwargs']&.to_json %>
|
|
69
|
+
<%= h(job_desc['args']&.to_json) %>
|
|
70
|
+
<%= h(job_desc['kwargs']&.to_json) %>
|
|
71
71
|
</div>
|
|
72
72
|
</code>
|
|
73
73
|
</td>
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
%>
|
|
91
91
|
<% if @jobs.any? && @total_size > @count.to_i %>
|
|
92
92
|
<div class="col-sm-4">
|
|
93
|
-
<%= erb get_template(:_pagination), locals: { url: "#{root_path}pools/#{@pool.pid}", page_key: :job_page } %>
|
|
93
|
+
<%= erb get_template(:_pagination), locals: { url: "#{root_path}pools/#{h(@pool.pid)}", page_key: :job_page } %>
|
|
94
94
|
</div>
|
|
95
95
|
<% end %>
|
|
96
96
|
</header>
|
|
@@ -111,18 +111,18 @@
|
|
|
111
111
|
|
|
112
112
|
<% @jobs.each do |job_desc| %>
|
|
113
113
|
<tr>
|
|
114
|
-
<td><%= job_desc['job'] %></td>
|
|
114
|
+
<td><%= h(job_desc['job']) %></td>
|
|
115
115
|
<td>
|
|
116
116
|
<code class="code-wrap">
|
|
117
117
|
<div class="args-extended">
|
|
118
|
-
<%= job_desc['args']&.to_json %>
|
|
119
|
-
<%= job_desc['kwargs']&.to_json %>
|
|
118
|
+
<%= h(job_desc['args']&.to_json) %>
|
|
119
|
+
<%= h(job_desc['kwargs']&.to_json) %>
|
|
120
120
|
</div>
|
|
121
121
|
</code>
|
|
122
122
|
</td>
|
|
123
|
-
<td><a href="<%= root_path %>batches/<%= job_desc['pool_wrapper_batch'] %>"><%= job_desc['pool_wrapper_batch'] %></a></td>
|
|
123
|
+
<td><a href="<%= root_path %>batches/<%= h(job_desc['pool_wrapper_batch']) %>"><%= h(job_desc['pool_wrapper_batch']) %></a></td>
|
|
124
124
|
<% if @pool.order == 'priority' %>
|
|
125
|
-
<td><%= job_desc['priority'] %></td>
|
|
125
|
+
<td><%= h(job_desc['priority']) %></td>
|
|
126
126
|
<% end %>
|
|
127
127
|
</tr>
|
|
128
128
|
<% end %>
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
</div>
|
|
131
131
|
<% end %>
|
|
132
132
|
|
|
133
|
-
<form class="form-horizontal" action="<%= root_path %>pools/<%= @pool.pid %>" method="post">
|
|
133
|
+
<form class="form-horizontal" action="<%= root_path %>pools/<%= h(@pool.pid) %>" method="post">
|
|
134
134
|
<%= csrf_tag %>
|
|
135
135
|
<a class="btn btn-default" href="<%= root_path %>pools"><%= t('GoBack') %></a>
|
|
136
136
|
<input class="btn btn-danger" type="submit" name="delete" value="<%= t('Delete') %>" data-confirm="<%= t('AreYouSure') %>" />
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
<% @pools.each do |pool| %>
|
|
30
30
|
<tr>
|
|
31
31
|
<td><%= safe_relative_time(pool.created_at.to_f) %></th>
|
|
32
|
-
<td><a href="<%= root_path %>pools/<%= pool.pid %>"><%= pool.pid %></a></td>
|
|
33
|
-
<td><%= pool.description %></th>
|
|
34
|
-
<td><%= pool.order.to_s.upcase %></th>
|
|
32
|
+
<td><a href="<%= root_path %>pools/<%= h(pool.pid) %>"><%= h(pool.pid) %></a></td>
|
|
33
|
+
<td><%= h(pool.description) %></th>
|
|
34
|
+
<td><%= h(pool.order.to_s.upcase) %></th>
|
|
35
35
|
<td><%= pool.concurrency %></th>
|
|
36
36
|
<td><%= pool.active_count %></th>
|
|
37
37
|
<td><%= pool.pending_count %></th>
|
|
@@ -39,6 +39,18 @@ module Joblin::Batching::Compat::Sidekiq
|
|
|
39
39
|
(env["rack.route_params"] || {})[key.to_sym]
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Read a query-string parameter such as `count` or `page`.
|
|
43
|
+
#
|
|
44
|
+
# Sidekiq 7+ defines `url_params(key)` on the web action and returns the
|
|
45
|
+
# query value only. Sidekiq 6.x has no `url_params`; there the action's
|
|
46
|
+
# `params` is an indifferent hash that already merges the query string,
|
|
47
|
+
# so we read from it. On Sidekiq 7/8 the action's own `url_params`
|
|
48
|
+
# shadows this fallback (a class's instance method wins over one from an
|
|
49
|
+
# included module), so this definition only takes effect on Sidekiq 6.x.
|
|
50
|
+
def url_params(key)
|
|
51
|
+
params[key]
|
|
52
|
+
end
|
|
53
|
+
|
|
42
54
|
def tree_data(root_bid, slice: nil)
|
|
43
55
|
tree_bids = Joblin::Batching::Batch.bid_hierarchy(root_bid, slice: slice)
|
|
44
56
|
|
|
@@ -98,7 +110,11 @@ module Joblin::Batching::Compat::Sidekiq
|
|
|
98
110
|
own_keys = batch.context.own.keys
|
|
99
111
|
batch.context.flatten.each do |k,v|
|
|
100
112
|
added = own_keys.include? k
|
|
101
|
-
|
|
113
|
+
# Both the key and the JSON-encoded value are app/user data; escape
|
|
114
|
+
# them so batch context can't inject markup into this page.
|
|
115
|
+
safe_key = Rack::Utils.escape_html(k.to_s)
|
|
116
|
+
safe_val = Rack::Utils.escape_html(v.to_json)
|
|
117
|
+
bits << " <span class=\"key #{added ? 'own' : 'inherited'}\">\"#{safe_key}\": #{safe_val},</span>"
|
|
102
118
|
end
|
|
103
119
|
bits = [
|
|
104
120
|
"{ // <span class=\"own\">Added</span> / <span class=\"inherited\">Inherited</span>",
|
|
@@ -2,6 +2,12 @@ require_relative './base_job'
|
|
|
2
2
|
|
|
3
3
|
module Joblin::Batching
|
|
4
4
|
class ManagedBatchJob < BaseJob
|
|
5
|
+
# preflight_check: an optional "Class.method" (or bare "method" on Object)
|
|
6
|
+
# string, invoked with the deserialized next job just before it is enqueued.
|
|
7
|
+
# Its return value controls the sequence:
|
|
8
|
+
# :abort -> abort the whole managed batch (cleanup and stop)
|
|
9
|
+
# falsy -> skip this job and advance to the next one
|
|
10
|
+
# truthy -> enqueue and run this job (the default lambda returns true)
|
|
5
11
|
def self.make_batch(sub_jobs, ordered: true, concurrency: nil, context: nil, preflight_check: nil, description: nil, desc_prefix: nil, &blk)
|
|
6
12
|
desc_prefix ||= ''
|
|
7
13
|
|
|
@@ -127,13 +133,13 @@ module Joblin::Batching
|
|
|
127
133
|
break unless next_job_json.present?
|
|
128
134
|
|
|
129
135
|
next_job = JSON.parse(next_job_json)
|
|
130
|
-
next_job = ::ActiveJob::Arguments.deserialize(next_job)[0]
|
|
136
|
+
next_job = ::ActiveJob::Arguments.deserialize(next_job)[0]&.symbolize_keys
|
|
131
137
|
|
|
132
138
|
preflight_result = preflight_check.call(next_job)
|
|
133
139
|
if preflight_result == :abort
|
|
134
140
|
cleanup_redis(nil, { "managed_batch_id" => man_batch_id })
|
|
135
141
|
break
|
|
136
|
-
elsif !
|
|
142
|
+
elsif !preflight_result
|
|
137
143
|
next
|
|
138
144
|
end
|
|
139
145
|
|
data/lib/joblin/batching/pool.rb
CHANGED
|
@@ -37,6 +37,13 @@ module Joblin::Batching
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def add_jobs(job_descs, skip_refill: false)
|
|
40
|
+
# Adding to a pool that has already been cleaned up (e.g. it drained to
|
|
41
|
+
# empty and auto-removed) would strand the job forever. Long-lived
|
|
42
|
+
# producers that add over time must hold the pool open with `keep_open!`.
|
|
43
|
+
unless redis.exists?(redis_key)
|
|
44
|
+
raise "Cannot add jobs to Pool #{pid}: it no longer exists. Wrap long-lived producers in `keep_open!`."
|
|
45
|
+
end
|
|
46
|
+
|
|
40
47
|
job_descs.each do |job_desc|
|
|
41
48
|
wrapper = Batch.new
|
|
42
49
|
wrapper.description = "Pool Job Wrapper (PID: #{pid})"
|
|
@@ -147,8 +154,10 @@ module Joblin::Batching
|
|
|
147
154
|
# Administrative/console method to cleanup expired pools from the WebUI
|
|
148
155
|
def self.cleanup_redis_index!
|
|
149
156
|
suffixes = ["", "-active", "-jobs"]
|
|
150
|
-
|
|
151
|
-
r.
|
|
157
|
+
redis do |r|
|
|
158
|
+
r.zrangebyscore("pools", "0", Batch::BID_EXPIRE_TTL.seconds.ago.to_i).each do |pid|
|
|
159
|
+
r.zrem("pools", pid) if Batch.cleanup_redis_index_for("POOLID-#{pid}", suffixes)
|
|
160
|
+
end
|
|
152
161
|
end
|
|
153
162
|
end
|
|
154
163
|
|
|
@@ -245,7 +254,7 @@ module Joblin::Batching
|
|
|
245
254
|
self.created_at = Time.now.utc.to_f
|
|
246
255
|
self.description = description
|
|
247
256
|
self.order = order
|
|
248
|
-
self.concurrency = concurrency
|
|
257
|
+
self.concurrency = concurrency || 1
|
|
249
258
|
self.clean_when_empty = clean_when_empty
|
|
250
259
|
self.on_failed_job = on_failed_job
|
|
251
260
|
flush_pending_attrs
|
|
@@ -15,7 +15,8 @@ if checkin_item ~= "" then
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
local pool_type = redis.call('HGET', poolkey, "order")
|
|
18
|
-
|
|
18
|
+
-- Treat an unset/blank concurrency as 1 rather than erroring on tonumber("") -> nil
|
|
19
|
+
local allotment = tonumber(redis.call("HGET", poolkey, "concurrency")) or 1
|
|
19
20
|
local active = redis.call("HLEN", activekey) + (redis.call("HGET", poolkey, "_active_count") or 0)
|
|
20
21
|
|
|
21
22
|
local pop_count = allotment - active
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
|
|
2
|
+
-- If the batch hash is already gone (cleaned up), don't resurrect it with a
|
|
3
|
+
-- fresh HSET. Report it as already-processed so no callback is (re-)scheduled.
|
|
4
|
+
if redis.call('EXISTS', KEYS[1]) == 0 then
|
|
5
|
+
return 'true'
|
|
6
|
+
end
|
|
7
|
+
|
|
2
8
|
local previously_scheduled = redis.call('HGET', KEYS[1], ARGV[1])
|
|
3
9
|
redis.call('HSET', KEYS[1], ARGV[1], 'true')
|
|
4
10
|
|