joblin 0.1.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 +7 -0
- data/README.md +1 -0
- data/app/models/joblin/background_task/api_access.rb +148 -0
- data/app/models/joblin/background_task/attachments.rb +47 -0
- data/app/models/joblin/background_task/executor.rb +63 -0
- data/app/models/joblin/background_task/options.rb +75 -0
- data/app/models/joblin/background_task/retention_policy.rb +28 -0
- data/app/models/joblin/background_task.rb +72 -0
- data/app/models/joblin/concerns/job_working_dirs.rb +21 -0
- data/db/migrate/20250903184852_create_background_tasks.rb +12 -0
- data/joblin.gemspec +35 -0
- data/lib/joblin/batching/batch.rb +537 -0
- data/lib/joblin/batching/callback.rb +135 -0
- data/lib/joblin/batching/chain_builder.rb +247 -0
- data/lib/joblin/batching/compat/active_job.rb +108 -0
- data/lib/joblin/batching/compat/sidekiq/web/batches_assets/css/styles.less +182 -0
- data/lib/joblin/batching/compat/sidekiq/web/batches_assets/js/batch_tree.js +108 -0
- data/lib/joblin/batching/compat/sidekiq/web/batches_assets/js/util.js +2 -0
- data/lib/joblin/batching/compat/sidekiq/web/helpers.rb +41 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_batch_tree.erb +6 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_batches_table.erb +44 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_common.erb +13 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_jobs_table.erb +21 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_pagination.erb +26 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/batch.erb +81 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/batches.erb +23 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/pool.erb +137 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/pools.erb +47 -0
- data/lib/joblin/batching/compat/sidekiq/web.rb +218 -0
- data/lib/joblin/batching/compat/sidekiq.rb +149 -0
- data/lib/joblin/batching/compat.rb +20 -0
- data/lib/joblin/batching/context_hash.rb +157 -0
- data/lib/joblin/batching/hier_batch_ids.lua +25 -0
- data/lib/joblin/batching/jobs/base_job.rb +7 -0
- data/lib/joblin/batching/jobs/concurrent_batch_job.rb +20 -0
- data/lib/joblin/batching/jobs/managed_batch_job.rb +175 -0
- data/lib/joblin/batching/jobs/serial_batch_job.rb +20 -0
- data/lib/joblin/batching/pool.rb +254 -0
- data/lib/joblin/batching/pool_refill.lua +47 -0
- data/lib/joblin/batching/schedule_callback.lua +14 -0
- data/lib/joblin/batching/status.rb +89 -0
- data/lib/joblin/engine.rb +15 -0
- data/lib/joblin/lazy_access.rb +72 -0
- data/lib/joblin/uniqueness/compat/active_job.rb +75 -0
- data/lib/joblin/uniqueness/compat/sidekiq.rb +135 -0
- data/lib/joblin/uniqueness/compat.rb +20 -0
- data/lib/joblin/uniqueness/configuration.rb +25 -0
- data/lib/joblin/uniqueness/job_uniqueness.rb +49 -0
- data/lib/joblin/uniqueness/lock_context.rb +199 -0
- data/lib/joblin/uniqueness/locksmith.rb +92 -0
- data/lib/joblin/uniqueness/on_conflict/base.rb +32 -0
- data/lib/joblin/uniqueness/on_conflict/log.rb +13 -0
- data/lib/joblin/uniqueness/on_conflict/null_strategy.rb +9 -0
- data/lib/joblin/uniqueness/on_conflict/raise.rb +11 -0
- data/lib/joblin/uniqueness/on_conflict/reject.rb +21 -0
- data/lib/joblin/uniqueness/on_conflict/reschedule.rb +20 -0
- data/lib/joblin/uniqueness/on_conflict.rb +62 -0
- data/lib/joblin/uniqueness/strategy/base.rb +107 -0
- data/lib/joblin/uniqueness/strategy/until_and_while_executing.rb +35 -0
- data/lib/joblin/uniqueness/strategy/until_executed.rb +20 -0
- data/lib/joblin/uniqueness/strategy/until_executing.rb +20 -0
- data/lib/joblin/uniqueness/strategy/until_expired.rb +16 -0
- data/lib/joblin/uniqueness/strategy/while_executing.rb +26 -0
- data/lib/joblin/uniqueness/strategy.rb +27 -0
- data/lib/joblin/uniqueness/unique_job_common.rb +79 -0
- data/lib/joblin/version.rb +3 -0
- data/lib/joblin.rb +37 -0
- data/spec/batching/batch_spec.rb +493 -0
- data/spec/batching/callback_spec.rb +38 -0
- data/spec/batching/compat/active_job_spec.rb +107 -0
- data/spec/batching/compat/sidekiq_spec.rb +127 -0
- data/spec/batching/context_hash_spec.rb +54 -0
- data/spec/batching/flow_spec.rb +82 -0
- data/spec/batching/integration/fail_then_succeed.rb +42 -0
- data/spec/batching/integration/integration.rb +57 -0
- data/spec/batching/integration/nested.rb +88 -0
- data/spec/batching/integration/simple.rb +47 -0
- data/spec/batching/integration/workflow.rb +134 -0
- data/spec/batching/integration_helper.rb +50 -0
- data/spec/batching/pool_spec.rb +161 -0
- data/spec/batching/status_spec.rb +76 -0
- data/spec/batching/support/base_job.rb +19 -0
- data/spec/batching/support/sample_callback.rb +2 -0
- data/spec/internal/config/database.yml +5 -0
- data/spec/internal/config/routes.rb +5 -0
- data/spec/internal/config/storage.yml +3 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +6 -0
- data/spec/internal/log/test.log +48200 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/models/background_task_spec.rb +41 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/uniqueness/compat/active_job_spec.rb +49 -0
- data/spec/uniqueness/compat/sidekiq_spec.rb +68 -0
- data/spec/uniqueness/lock_context_spec.rb +106 -0
- data/spec/uniqueness/on_conflict/log_spec.rb +11 -0
- data/spec/uniqueness/on_conflict/raise_spec.rb +10 -0
- data/spec/uniqueness/on_conflict/reschedule_spec.rb +63 -0
- data/spec/uniqueness/on_conflict_spec.rb +16 -0
- data/spec/uniqueness/spec_helper.rb +19 -0
- data/spec/uniqueness/strategy/base_spec.rb +100 -0
- data/spec/uniqueness/strategy/until_and_while_executing_spec.rb +48 -0
- data/spec/uniqueness/strategy/until_executed_spec.rb +23 -0
- data/spec/uniqueness/strategy/until_executing_spec.rb +23 -0
- data/spec/uniqueness/strategy/until_expired_spec.rb +23 -0
- data/spec/uniqueness/strategy/while_executing_spec.rb +33 -0
- data/spec/uniqueness/support/lock_strategy.rb +28 -0
- data/spec/uniqueness/support/on_conflict.rb +24 -0
- data/spec/uniqueness/support/test_worker.rb +19 -0
- data/spec/uniqueness/unique_job_common_spec.rb +45 -0
- metadata +308 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
module Joblin::Batching
|
|
2
|
+
class ChainBuilder
|
|
3
|
+
VALID_PLACEMENT_PARAMETERS = %i[before after with].freeze
|
|
4
|
+
|
|
5
|
+
attr_reader :base_job
|
|
6
|
+
|
|
7
|
+
def initialize(base_type = SerialBatchJob)
|
|
8
|
+
if base_type.is_a?(Hash)
|
|
9
|
+
@base_job = base_type
|
|
10
|
+
@base_job[:args] ||= @base_job[:parameters] || []
|
|
11
|
+
@base_job[:kwargs] ||= {}
|
|
12
|
+
else
|
|
13
|
+
@base_job = build_job_hash(base_type)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
self.class.get_chain_parameter(base_job)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def process!
|
|
20
|
+
normalize!
|
|
21
|
+
self.class.enqueue_job(base_job)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def [](key)
|
|
25
|
+
if key.is_a?(Class)
|
|
26
|
+
get_sub_chain(key)
|
|
27
|
+
else
|
|
28
|
+
# Legacy Support
|
|
29
|
+
key = :args if key == :parameters
|
|
30
|
+
|
|
31
|
+
@base_job[key]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def args; return self[:args]; end
|
|
36
|
+
def kwargs; return self[:kwargs]; end
|
|
37
|
+
|
|
38
|
+
def <<(new_job)
|
|
39
|
+
insert_at(-1, new_job)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def insert_at(position, new_jobs, *args, **kwargs, &blk)
|
|
43
|
+
chain = self.class.get_chain_parameter(base_job)
|
|
44
|
+
if new_jobs.is_a?(Class) || new_jobs.is_a?(String)
|
|
45
|
+
new_jobs = build_job_hash(new_jobs, args: args, kwargs: kwargs, &blk)
|
|
46
|
+
elsif args.count > 0 || kwargs.count > 0
|
|
47
|
+
raise "Unexpected number of arguments"
|
|
48
|
+
end
|
|
49
|
+
new_jobs = [new_jobs] unless new_jobs.is_a?(Array)
|
|
50
|
+
chain.insert(position, *new_jobs)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def insert(new_jobs, *args, **kwargs, &blk)
|
|
54
|
+
if new_jobs.is_a?(Class) || new_jobs.is_a?(String)
|
|
55
|
+
job_kwargs = kwargs.except(*VALID_PLACEMENT_PARAMETERS)
|
|
56
|
+
new_jobs = build_job_hash(new_jobs, args: args, kwargs: job_kwargs, &blk)
|
|
57
|
+
kwargs = kwargs.slice(*VALID_PLACEMENT_PARAMETERS)
|
|
58
|
+
else
|
|
59
|
+
invalid_params = kwargs.keys - VALID_PLACEMENT_PARAMETERS
|
|
60
|
+
raise "Invalid placement parameters: #{invalid_params.map(&:to_s).join(', ')}" if invalid_params.present?
|
|
61
|
+
raise "At most one placement parameter may be provided" if kwargs.values.compact.length > 1
|
|
62
|
+
raise "Unexpected number of arguments" if args.length > 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
new_jobs = [new_jobs] unless new_jobs.is_a?(Array)
|
|
66
|
+
|
|
67
|
+
if !kwargs.present?
|
|
68
|
+
insert_at(-1, new_jobs)
|
|
69
|
+
else
|
|
70
|
+
placement = kwargs.keys[0]
|
|
71
|
+
relative_to = kwargs.values[0]
|
|
72
|
+
|
|
73
|
+
matching_jobs = find_matching_jobs(relative_to).to_a
|
|
74
|
+
raise "Could not find a \"#{relative_to}\" job in the chain" if matching_jobs.count == 0
|
|
75
|
+
raise "Found multiple \"#{relative_to}\" jobs in the chain" if matching_jobs.count > 1
|
|
76
|
+
|
|
77
|
+
relative_job, parent_job, sub_index = matching_jobs[0]
|
|
78
|
+
needed_parent_type = placement == :with ? ConcurrentBatchJob : SerialBatchJob
|
|
79
|
+
|
|
80
|
+
chain = self.class.get_chain_parameter(parent_job)
|
|
81
|
+
|
|
82
|
+
if parent_job[:job] != needed_parent_type
|
|
83
|
+
old_job = chain[sub_index]
|
|
84
|
+
parent_job = chain[sub_index] = {
|
|
85
|
+
job: needed_parent_type,
|
|
86
|
+
parameters: [],
|
|
87
|
+
}
|
|
88
|
+
sub_index = 0
|
|
89
|
+
chain = self.class.get_chain_parameter(parent_job)
|
|
90
|
+
chain << old_job
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if placement == :with
|
|
94
|
+
chain.insert(-1, *new_jobs)
|
|
95
|
+
else
|
|
96
|
+
sub_index += 1 if placement == :after
|
|
97
|
+
chain.insert(sub_index, *new_jobs)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def empty?
|
|
103
|
+
self.class.get_chain_parameter(self).empty?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def get_sub_chain(sub_type)
|
|
107
|
+
matching_jobs = find_matching_jobs(sub_type).to_a
|
|
108
|
+
raise "Found multiple \"#{sub_type}\" jobs in the chain" if matching_jobs.count > 1
|
|
109
|
+
return nil if matching_jobs.count == 0
|
|
110
|
+
|
|
111
|
+
job = matching_jobs[0][0]
|
|
112
|
+
job = self.class.new(job) unless job.is_a?(ChainBuilder)
|
|
113
|
+
job
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def normalize!(job_def = self.base_job)
|
|
117
|
+
if job_def.is_a?(ChainBuilder)
|
|
118
|
+
job_def.normalize!
|
|
119
|
+
else
|
|
120
|
+
job_def[:job] = job_def[:job].to_s
|
|
121
|
+
if (chain = self.class.get_chain_parameter(job_def, raise_error: false)).present?
|
|
122
|
+
chain.map! { |sub_job| normalize!(sub_job) }
|
|
123
|
+
end
|
|
124
|
+
job_def
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def apply_block(&blk)
|
|
129
|
+
return unless blk.present?
|
|
130
|
+
instance_exec(&blk)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def build_job_hash(job, args: [], kwargs: {}, &blk)
|
|
136
|
+
hsh = {
|
|
137
|
+
job: job,
|
|
138
|
+
args: args,
|
|
139
|
+
kwargs: kwargs,
|
|
140
|
+
}
|
|
141
|
+
self.class.new(hsh).apply_block(&blk) if blk.present?
|
|
142
|
+
hsh
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def find_matching_jobs(search_job, parent_job = self.base_job)
|
|
146
|
+
return to_enum(:find_matching_jobs, search_job, parent_job) unless block_given?
|
|
147
|
+
|
|
148
|
+
sub_jobs = self.class.get_chain_parameter(parent_job)
|
|
149
|
+
sub_jobs.each_with_index do |sub_job, i|
|
|
150
|
+
if sub_job[:job].to_s == search_job.to_s
|
|
151
|
+
yield [sub_job, parent_job, i]
|
|
152
|
+
elsif self.class._job_type_definitions[sub_job[:job].to_s]
|
|
153
|
+
find_matching_jobs(search_job, sub_job) { |item| yield item }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def find_parent_job(job_def)
|
|
159
|
+
iterate_job_tree do |job, path|
|
|
160
|
+
return path[-1] if job == job_def
|
|
161
|
+
end
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def iterate_job_tree(root: self.base_job, path: [], &blk)
|
|
166
|
+
blk.call(root, path)
|
|
167
|
+
|
|
168
|
+
if self.class._job_type_definitions[root[:job]]
|
|
169
|
+
sub_jobs = self.class.get_chain_parameter(root)
|
|
170
|
+
sub_jobs.each_with_index do |sub_job, i|
|
|
171
|
+
iterate_job_tree(root: sub_job, path: [*path, root], &blk)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class << self
|
|
177
|
+
# Support builder syntaxt/DSL
|
|
178
|
+
# Chain.build(ConcurrentBatchJob) do
|
|
179
|
+
# insert(SomeJob, arg1, kwarg: 1)
|
|
180
|
+
# insert(SerialBatchJob) do
|
|
181
|
+
# insert(SomeJob, arg1, kwarg: 1)
|
|
182
|
+
# end
|
|
183
|
+
# end
|
|
184
|
+
def build(job, *args, **kwargs, &blk)
|
|
185
|
+
new(job).tap do |ch|
|
|
186
|
+
ch.base_job[:args] = args
|
|
187
|
+
ch.base_job[:kwargs] = kwargs
|
|
188
|
+
ch.apply_block(&blk)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def _job_type_definitions
|
|
193
|
+
@job_type_definitions ||= {}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def register_chain_job(job_class, chain_parameter, **options)
|
|
197
|
+
_job_type_definitions[job_class.to_s] = {
|
|
198
|
+
**options,
|
|
199
|
+
chain_parameter: chain_parameter,
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def get_chain_parameter(job_def, raise_error: true)
|
|
204
|
+
unless _job_type_definitions[job_def[:job].to_s].present?
|
|
205
|
+
raise "Job Type #{job_def[:job].to_s} does not accept a sub-chain" if raise_error
|
|
206
|
+
return nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
key = _job_type_definitions[job_def[:job].to_s][:chain_parameter]
|
|
210
|
+
if key.is_a?(Numeric)
|
|
211
|
+
job_def[:args][key] ||= []
|
|
212
|
+
else
|
|
213
|
+
job_def[:kwargs][key] ||= []
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# TODO: Add a Chain progress web View
|
|
218
|
+
# Augment Batch tree-view with Chain data
|
|
219
|
+
# > [DONE] Tree view w/o Chain will only show Parent/Current batches and Job Counts
|
|
220
|
+
# > If augmented with Chain data, the above will be annotated with Chain-related info and will be able to show Jobs defined in the Chain
|
|
221
|
+
# > Chain-jobs will be supplied chain_id and chain_step_id metadata
|
|
222
|
+
# > Using server-middleware, if a Chain-job (has chain_id and chain_step_id) creates a Batch, tag the Batch w/ the chain_id and chain_step_id
|
|
223
|
+
# > UI will map Batches to Chain-steps using the chain_step_id. UI will add entries for any Chain-steps that were not tied to a Batch
|
|
224
|
+
# > [DONE] Use a Lua script to find child batch IDs. Support max_depth, items_per_depth, top_depth_slice parameters
|
|
225
|
+
def enqueue_job(job_def)
|
|
226
|
+
job_class = job_def[:job].constantize
|
|
227
|
+
job_args = job_def[:args] || job_def[:parameters] || []
|
|
228
|
+
job_kwargs = job_def[:kwargs] || {}
|
|
229
|
+
|
|
230
|
+
# Legacy Support
|
|
231
|
+
if job_def[:options]
|
|
232
|
+
job_args << {} unless job_args[-1].is_a?(Hash)
|
|
233
|
+
job_args[-1].merge!(job_def[:options])
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
if job_class.respond_to? :perform_async
|
|
237
|
+
job_class.perform_async(*job_args, **job_kwargs)
|
|
238
|
+
else
|
|
239
|
+
job_class.perform_later(*job_args, **job_kwargs)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
ChainBuilder.register_chain_job(ConcurrentBatchJob, 0)
|
|
246
|
+
ChainBuilder.register_chain_job(SerialBatchJob, 0)
|
|
247
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
module Joblin::Batching
|
|
3
|
+
module Compat
|
|
4
|
+
module ActiveJob
|
|
5
|
+
module BatchAwareJob
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
around_perform do |job, block|
|
|
10
|
+
if (@bid) # This _must_ be @bid - not just bid
|
|
11
|
+
prev_batch = Thread.current[CURRENT_BATCH_THREAD_KEY]
|
|
12
|
+
begin
|
|
13
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY] = Batch.new(@bid)
|
|
14
|
+
block.call
|
|
15
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY].save_context_changes
|
|
16
|
+
Batch.process_successful_job(@bid, job_id)
|
|
17
|
+
rescue
|
|
18
|
+
Batch.process_failed_job(@bid, job_id)
|
|
19
|
+
raise
|
|
20
|
+
ensure
|
|
21
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY] = prev_batch
|
|
22
|
+
end
|
|
23
|
+
else
|
|
24
|
+
block.call
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
around_enqueue do |job, block|
|
|
29
|
+
if (batch = Thread.current[CURRENT_BATCH_THREAD_KEY])
|
|
30
|
+
@bid = batch.bid
|
|
31
|
+
batch.append_jobs(job_id) if @bid
|
|
32
|
+
end
|
|
33
|
+
block.call
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def bid
|
|
38
|
+
@bid || Thread.current[CURRENT_BATCH_THREAD_KEY]&.bid
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def batch
|
|
42
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def batch_context
|
|
46
|
+
batch&.context || {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def valid_within_batch?
|
|
50
|
+
batch.valid?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def serialize
|
|
54
|
+
super.tap do |data|
|
|
55
|
+
data['batch_id'] = @bid # This _must_ be @bid - not just bid
|
|
56
|
+
data
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def deserialize(data)
|
|
61
|
+
super
|
|
62
|
+
@bid = data['batch_id']
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class ActiveJobCallbackWorker < ::ActiveJob::Base
|
|
67
|
+
include Batch::Callback::CallbackWorkerCommon
|
|
68
|
+
|
|
69
|
+
def self.enqueue_all(args, queue)
|
|
70
|
+
args.each do |arg_set|
|
|
71
|
+
set(queue: queue).perform_later(*arg_set)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.handle_job_death(job, error = nil)
|
|
77
|
+
if job.is_a?(Array)
|
|
78
|
+
event = ActiveSupport::Notifications::Event.new(*job)
|
|
79
|
+
payload = event.payload
|
|
80
|
+
job = payload[:job].serialize
|
|
81
|
+
error = payload[:error]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if job["job_id"].present? && job["batch_id"].present?
|
|
85
|
+
Joblin::Batching::Batch.process_dead_job(job['batch_id'], job['job_id'])
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.configure
|
|
90
|
+
::ActiveJob::Base.include BatchAwareJob
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
ActiveSupport::Notifications.subscribe "discard.active_job" do |*args|
|
|
94
|
+
handle_job_death(args)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
ActiveSupport::Notifications.subscribe "retry_stopped.active_job" do |*args|
|
|
98
|
+
handle_job_death(args)
|
|
99
|
+
end
|
|
100
|
+
rescue => err
|
|
101
|
+
Rails.logger.warn(err)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
Batch::Callback.worker_class ||= ActiveJobCallbackWorker
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
|
|
2
|
+
@color-green: #25c766;
|
|
3
|
+
@color-darkred: #8f0000;
|
|
4
|
+
@color-red: #e03963;
|
|
5
|
+
@color-yellow: #c4c725;
|
|
6
|
+
|
|
7
|
+
.code-wrap.batch-context .args-extended {
|
|
8
|
+
white-space: pre;
|
|
9
|
+
|
|
10
|
+
.key {
|
|
11
|
+
white-space: pre-wrap;
|
|
12
|
+
margin-left: 2em;
|
|
13
|
+
text-indent: -2em;
|
|
14
|
+
display: inline-block;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.own {
|
|
18
|
+
color: @color-green;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
.batch-tree {
|
|
24
|
+
.status-block {
|
|
25
|
+
.tree-stat {
|
|
26
|
+
margin: 0 4px;
|
|
27
|
+
|
|
28
|
+
&.pending {
|
|
29
|
+
color: @color-yellow;
|
|
30
|
+
}
|
|
31
|
+
&.failed {
|
|
32
|
+
color: @color-red;
|
|
33
|
+
}
|
|
34
|
+
&.dead {
|
|
35
|
+
color: @color-darkred;
|
|
36
|
+
}
|
|
37
|
+
&.success {
|
|
38
|
+
color: @color-green;
|
|
39
|
+
}
|
|
40
|
+
&.total {
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.text-inactive {
|
|
47
|
+
color: darken(#fff, 35%);
|
|
48
|
+
font-size: 80%;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.tree-header {
|
|
52
|
+
position: relative;
|
|
53
|
+
|
|
54
|
+
.status-block {
|
|
55
|
+
position: absolute;
|
|
56
|
+
bottom: 0;
|
|
57
|
+
width: 100%;
|
|
58
|
+
|
|
59
|
+
margin-right: 8px;
|
|
60
|
+
font-size: 90%;
|
|
61
|
+
text-align: right;
|
|
62
|
+
|
|
63
|
+
.tree-stat {
|
|
64
|
+
font-style: italic;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.tree-entry {
|
|
70
|
+
> .header {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
|
|
74
|
+
.header-inner {
|
|
75
|
+
padding: 4px 0;
|
|
76
|
+
border-bottom: 1px dashed white;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
flex: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&:hover {
|
|
83
|
+
background-color: rgba(0,0,0,0.20);
|
|
84
|
+
border-radius: 3px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.row-toggle {
|
|
88
|
+
width: 16px;
|
|
89
|
+
height: 16px;
|
|
90
|
+
text-align: center;
|
|
91
|
+
align-self: center;
|
|
92
|
+
border-radius: 50%;
|
|
93
|
+
border: 1px solid #999;
|
|
94
|
+
text-decoration: none;
|
|
95
|
+
margin: 0 4px;
|
|
96
|
+
font-size: 16px;
|
|
97
|
+
line-height: 15px;
|
|
98
|
+
|
|
99
|
+
&.not_applicable {
|
|
100
|
+
opacity: 0;
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.main {
|
|
106
|
+
flex: 1;
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: baseline;
|
|
109
|
+
|
|
110
|
+
.bid {
|
|
111
|
+
font-family: monospace;
|
|
112
|
+
padding: 3px 6px;
|
|
113
|
+
background: rgba(0,0,0,0.2);
|
|
114
|
+
border-radius: 3px;
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
margin: 0 12px 0 0;
|
|
117
|
+
|
|
118
|
+
&:hover {
|
|
119
|
+
.bid-goto {
|
|
120
|
+
display: inline-block;
|
|
121
|
+
padding: 0 0 0 4px;
|
|
122
|
+
font-size: 200%;
|
|
123
|
+
line-height: 10px;
|
|
124
|
+
vertical-align: sub;
|
|
125
|
+
text-decoration: dotted;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.bid-goto {
|
|
130
|
+
display: none;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.goto-link {
|
|
136
|
+
margin: 0 8px;
|
|
137
|
+
display: inline-block;
|
|
138
|
+
height: 16px;
|
|
139
|
+
font-size: 90%;
|
|
140
|
+
border-bottom: 1px dotted white;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.status-label {
|
|
144
|
+
font-family: monospace;
|
|
145
|
+
padding: 3px 6px;
|
|
146
|
+
background: rgba(0,0,0,0.2);
|
|
147
|
+
border-radius: 3px;
|
|
148
|
+
font-size: 12px;
|
|
149
|
+
margin: 0 12px 0 0;
|
|
150
|
+
|
|
151
|
+
&.deleted {
|
|
152
|
+
background: #99999933;
|
|
153
|
+
}
|
|
154
|
+
&.failed, &.complete {
|
|
155
|
+
background: #99000033;
|
|
156
|
+
}
|
|
157
|
+
&.success {
|
|
158
|
+
background: #00990033;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.status-block {
|
|
163
|
+
width: 12em;
|
|
164
|
+
text-align: center;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
> .subitems {
|
|
169
|
+
padding-left: 16px;
|
|
170
|
+
|
|
171
|
+
>.load-more {
|
|
172
|
+
padding: 4px 0;
|
|
173
|
+
text-align: center;
|
|
174
|
+
border-bottom: 1px dashed white;
|
|
175
|
+
a {
|
|
176
|
+
border-bottom: 1px dotted white;
|
|
177
|
+
text-decoration: none;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { h, Component, render } from 'https://esm.sh/preact';
|
|
2
|
+
import htm from 'https://esm.sh/htm';
|
|
3
|
+
import { root_url } from './util.js';
|
|
4
|
+
|
|
5
|
+
// Initialize htm with Preact
|
|
6
|
+
const html = htm.bind(h);
|
|
7
|
+
|
|
8
|
+
const StatusBlock = (props) => html`
|
|
9
|
+
<div class="status-block ${props.class || ''}">
|
|
10
|
+
${props.title && props.title + ':'}
|
|
11
|
+
<span class="tree-stat pending">${props.pending_count}</span>
|
|
12
|
+
|
|
|
13
|
+
<span class="tree-stat failed">${props.failed_count}</span>
|
|
14
|
+
|
|
|
15
|
+
<span class="tree-stat dead">${props.dead_count}</span>
|
|
16
|
+
|
|
|
17
|
+
<span class="tree-stat success">${props.successful_count}</span>
|
|
18
|
+
/
|
|
19
|
+
<span class="tree-stat total">${props.total_count}</span>
|
|
20
|
+
</div>
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
class TreeLevel extends Component {
|
|
24
|
+
get bid() {
|
|
25
|
+
return this.props.data.bid;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get batch() {
|
|
29
|
+
return this.props.data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
load_more = async (event) => {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
const l = this.batch.batches.items.length;
|
|
35
|
+
const resp = await fetch(`${root_url}batches/${this.bid}/tree?slice=${l}:${l + 5 - 1}`)
|
|
36
|
+
const result = await resp.json()
|
|
37
|
+
const newEntries = result.batches.items;
|
|
38
|
+
for (let ent of newEntries) {
|
|
39
|
+
this.batch.batches.items.push(ent)
|
|
40
|
+
}
|
|
41
|
+
this.forceUpdate()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
toggle = (event) => {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
this.setState({ collapsed: !this.state.collapsed })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
render() {
|
|
50
|
+
const { data: bd } = this.props;
|
|
51
|
+
|
|
52
|
+
let sub_entries = [];
|
|
53
|
+
let sub_batches = bd.batches.items;
|
|
54
|
+
for (let b of sub_batches) {
|
|
55
|
+
sub_entries.push(html`<${TreeLevel} data=${b} />`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let fully_loaded = !(sub_batches.length < bd.batches.total_count);
|
|
59
|
+
|
|
60
|
+
const load_more_elem = html`<div class="load-more">
|
|
61
|
+
${sub_entries.length} / ${bd.batches.total_count} Items Loaded - <a href="#" onClick=${this.load_more}>Load More</a>
|
|
62
|
+
</div>`
|
|
63
|
+
|
|
64
|
+
return html`<div class="tree-entry tree-batch">
|
|
65
|
+
<div class="header">
|
|
66
|
+
<a class="row-toggle ${!bd.batches.total_count && 'not_applicable'}" onClick=${this.toggle} href="#">
|
|
67
|
+
${this.state.collapsed ? '+' : '-'}
|
|
68
|
+
</a>
|
|
69
|
+
|
|
70
|
+
<div class="header-inner">
|
|
71
|
+
<div class="main">
|
|
72
|
+
<span class="bid">
|
|
73
|
+
${bd.bid}
|
|
74
|
+
<a class="bid-goto" href="${root_url}batches/${bd.bid}">⇢</a>
|
|
75
|
+
</span>
|
|
76
|
+
${bd.description || (bd.status == 'deleted' && html`<i class="text-inactive">Deleted</i>`) || html`<i class="text-inactive">No Description</i>`}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<span class="status-label ${bd.status}">${bd.status}</span>
|
|
80
|
+
|
|
81
|
+
${bd.status != 'deleted' && html`
|
|
82
|
+
<${StatusBlock} class="job-status" title="Jobs" ...${bd.jobs} />
|
|
83
|
+
<${StatusBlock} class="batch-status" title="Batches" ...${bd.batches} />
|
|
84
|
+
`}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="subitems ${this.state.collapsed ? 'hidden' : ''}">
|
|
89
|
+
${sub_entries}
|
|
90
|
+
${!fully_loaded && load_more_elem}
|
|
91
|
+
</div>
|
|
92
|
+
</div>`
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class TreeRoot extends Component {
|
|
97
|
+
render() {
|
|
98
|
+
const tree_data = JSON.parse(document.querySelector('#batch-tree #initial-data').innerHTML);
|
|
99
|
+
return html`
|
|
100
|
+
<div class="tree-header">
|
|
101
|
+
<${StatusBlock} pending_count="pending" failed_count="failed" dead_count="dead" successful_count="successful" total_count="total" />
|
|
102
|
+
</div>
|
|
103
|
+
<${TreeLevel} data=${tree_data} />
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render(html`<${TreeRoot} />`, document.querySelector('#batch-tree'));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Joblin::Batching::Compat::Sidekiq
|
|
4
|
+
module Web
|
|
5
|
+
module Helpers
|
|
6
|
+
VIEW_PATH = File.expand_path("../web/views", __dir__)
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def get_template(name)
|
|
11
|
+
File.open(File.join(VIEW_PATH, "#{name}.erb")).read
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def drain_zset(key)
|
|
15
|
+
items, _ = Joblin::Batching::Batch.redis do |r|
|
|
16
|
+
r.multi do |r|
|
|
17
|
+
r.zrange(key, 0, -1)
|
|
18
|
+
r.zremrangebyrank(key, 0, -1)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
yield items
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def safe_relative_time(time)
|
|
25
|
+
time = parse_time(time)
|
|
26
|
+
relative_time(time)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parse_time(time)
|
|
30
|
+
case time
|
|
31
|
+
when Time
|
|
32
|
+
time
|
|
33
|
+
when Integer, Float
|
|
34
|
+
Time.at(time)
|
|
35
|
+
else
|
|
36
|
+
Time.parse(time.to_s)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|