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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 35d34b0f27040ddb94b3cfcd15d01c6fcdc12b18895a6b525032ec1d2d0aa41f
|
|
4
|
+
data.tar.gz: 6cbc121c07c836a85e77c294c92baf82d3338fac4625f38d697973fdf5a97577
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aa4cad19edc21301edd5ff875bbfbff1fec1eed88a10814112380b14ce3f59f7efa3bd7d82741ed86674df1f48dc765d4df62495bb9c14d0aeb444375893755a
|
|
7
|
+
data.tar.gz: a59dbee1edea55c4f68ff30607aa20c70862462db1c123a0585ba8f757bfd41440a827b6226b7613d197ab224db71e1a05080cfa43f7d18c7fe15eb6b700cf1c
|
data/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Joblin
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
module Joblin
|
|
2
|
+
module BackgroundTask::ApiAccess
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def api_access_rules(&blk)
|
|
7
|
+
@api_access_rules ||= []
|
|
8
|
+
if blk
|
|
9
|
+
@api_access_rules << ApiAccessRules.new.tap do |aar|
|
|
10
|
+
aar.instance_exec(&blk)
|
|
11
|
+
end
|
|
12
|
+
nil
|
|
13
|
+
else
|
|
14
|
+
[*superclass.try(:api_access_rules), *@api_access_rules].compact.freeze
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def allow_api_access!(&blk)
|
|
19
|
+
include ApiAccess::Mixin unless self < ApiAccess::Mixin
|
|
20
|
+
@api_access_allowed = true
|
|
21
|
+
api_access_rules(&blk) if blk
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def api_access_allowed?
|
|
25
|
+
# This is intentionally not inherited by subclasses
|
|
26
|
+
@api_access_allowed
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find_for_api(id)
|
|
30
|
+
task = find(id)
|
|
31
|
+
raise ActiveRecord::RecordNotFound unless task.class.api_access_allowed?
|
|
32
|
+
task
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_from_api(controller_or_params, options = {}, params: nil, exec_context: nil)
|
|
36
|
+
if controller_or_params.respond_to?(:params)
|
|
37
|
+
exec_context ||= controller_or_params
|
|
38
|
+
params ||= controller_or_params.params.require(:background_task)
|
|
39
|
+
else
|
|
40
|
+
params ||= controller_or_params
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
task_type = self == BackgroundTask ? params[:type].safe_constantize : self
|
|
44
|
+
raise ActiveRecord::RecordNotFound unless task_type && task_type <= BackgroundTask
|
|
45
|
+
raise ActiveRecord::RecordNotFound unless task_type.api_access_allowed?
|
|
46
|
+
|
|
47
|
+
task = task_type.new
|
|
48
|
+
rule_sets = task_type.api_access_rules
|
|
49
|
+
|
|
50
|
+
# Apply permitted options
|
|
51
|
+
if params[:options].present?
|
|
52
|
+
permitted_options = rule_sets.map(&:permitted_options).flatten
|
|
53
|
+
task.options.merge!(params[:options].permit(permitted_options).to_h)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Apply default options
|
|
57
|
+
rule_sets.reverse.each do |aar|
|
|
58
|
+
aar.default_options.each do |k, v|
|
|
59
|
+
next if task.options.key?(k)
|
|
60
|
+
task.options[k] = v.is_a?(Proc) ? exec_context.instance_exec(&v) : v
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Apply any additional/hard-coded options
|
|
65
|
+
task.options.merge!(options)
|
|
66
|
+
|
|
67
|
+
val_errors = task.api_validate_options
|
|
68
|
+
raise ActiveRecord::ValidationError.new(val_errors) unless val_errors.empty?
|
|
69
|
+
|
|
70
|
+
task
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
included do
|
|
75
|
+
api_access_rules do
|
|
76
|
+
default_option(:creator) { try(:current_user) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
module Mixin
|
|
81
|
+
extend ActiveSupport::Concern
|
|
82
|
+
|
|
83
|
+
included do
|
|
84
|
+
if defined?(BackgroundTaskChannel)
|
|
85
|
+
after_commit do
|
|
86
|
+
BackgroundTaskChannel.broadcast_to(self, api_serialize)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def api_serialize
|
|
92
|
+
builder = Jbuilder.new do |json|
|
|
93
|
+
json.id id
|
|
94
|
+
json.type type
|
|
95
|
+
json.workflow_state workflow_state
|
|
96
|
+
|
|
97
|
+
rule_sets = self.class.api_access_rules
|
|
98
|
+
rule_sets.each do |aar|
|
|
99
|
+
instance_exec(json, &aar.serializer) if aar.serializer
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
builder.attributes!
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def api_validate_options
|
|
107
|
+
errors = []
|
|
108
|
+
rule_sets = self.class.api_access_rules
|
|
109
|
+
rule_sets.each do |aar|
|
|
110
|
+
aar.validators.each do |validator|
|
|
111
|
+
errors << instance_exec(&validator)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
errors.flatten.compact.uniq
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class ApiAccessRules
|
|
119
|
+
attr_accessor :serializer
|
|
120
|
+
attr_reader :validators
|
|
121
|
+
attr_reader :default_options
|
|
122
|
+
attr_reader :permitted_options
|
|
123
|
+
|
|
124
|
+
def initialize
|
|
125
|
+
@serializer = nil
|
|
126
|
+
@validators = []
|
|
127
|
+
@permitted_options = []
|
|
128
|
+
@default_options = {}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def default_option(key, value = nil, &blk)
|
|
132
|
+
@default_options[key] = blk || value
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def permit_options(*args)
|
|
136
|
+
@permitted_options.concat(args)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate(&blk)
|
|
140
|
+
@validators << blk
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def serialize(&blk)
|
|
144
|
+
@serializer = blk
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Joblin
|
|
2
|
+
module BackgroundTask::Attachments
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
def attachment_path(key, expires_in: true)
|
|
6
|
+
if !expires_in || Rails.env.development? || Rails.env.test?
|
|
7
|
+
return Rails.application.routes.url_helpers.rails_blob_path(
|
|
8
|
+
send(key),
|
|
9
|
+
only_path: true,
|
|
10
|
+
disposition: 'attachment',
|
|
11
|
+
# organization_id: current_organization&.id,
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if expires_in == true
|
|
16
|
+
send(key).url
|
|
17
|
+
else expires_in
|
|
18
|
+
send(key).url(expires_in:)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def attach_file(key, path, as: nil)
|
|
23
|
+
path = File.join(working_dir, path) unless Pathname.new(path).absolute?
|
|
24
|
+
|
|
25
|
+
self.send(key).attach(
|
|
26
|
+
io: File.open(path),
|
|
27
|
+
filename: as || File.basename(path)
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def load_attachment(key, save_as: nil)
|
|
32
|
+
@loaded_attachments ||= {}
|
|
33
|
+
|
|
34
|
+
@loaded_attachments[key] ||= begin
|
|
35
|
+
save_as = save_as || send(key).filename.to_s || key.to_s
|
|
36
|
+
save_as = File.join(working_dir, save_as) unless Pathname.new(save_as).absolute?
|
|
37
|
+
|
|
38
|
+
File.open(save_as, 'w') do |file|
|
|
39
|
+
send(key).download do |chunk|
|
|
40
|
+
file.write(chunk.force_encoding("UTF-8"))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
save_as
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Joblin
|
|
2
|
+
module BackgroundTask::Executor
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def inherited(subclass)
|
|
7
|
+
subclass.const_set(:ExecutorJob, Class.new(self::ExecutorJob))
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def job_executor_class
|
|
12
|
+
self::ExecutorJob
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def executor_eval(&blk)
|
|
16
|
+
job_executor_class.class_eval(&blk)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
delegate :queue_as, :sidekiq_options, to: :job_executor_class
|
|
20
|
+
|
|
21
|
+
%i[before around after].each do |hook_type|
|
|
22
|
+
define_method(:"#{hook_type}_perform") do |*args, &blk|
|
|
23
|
+
blk ||= args[0].to_proc
|
|
24
|
+
job_executor_class.send(:"#{hook_type}_perform") do |*args|
|
|
25
|
+
the_task.instance_exec(&blk)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# delegate :set, to: :job_executor_class
|
|
31
|
+
|
|
32
|
+
# delegate :before_perform, :around_perform, :after_perform, to: :job_executor_class
|
|
33
|
+
# delegate :before_enqueue, :around_enqueue, :after_enqueue, to: :job_executor_class
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
included do
|
|
37
|
+
delegate_missing_to :_current_executor
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
protected
|
|
41
|
+
|
|
42
|
+
def _current_executor
|
|
43
|
+
@executor
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class ExecutorJob < ActiveJob::Base
|
|
47
|
+
# include JobWorkingDirs
|
|
48
|
+
|
|
49
|
+
def perform
|
|
50
|
+
the_task.update(workflow_state: "started") if the_task.workflow_state == "scheduled"
|
|
51
|
+
|
|
52
|
+
the_task.perform
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def the_task
|
|
56
|
+
@the_task ||= BackgroundTask.find(batch_context[:background_task_id]).tap do |task|
|
|
57
|
+
task.instance_variable_set(:@executor, self)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Joblin
|
|
2
|
+
module BackgroundTask::Options
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def bt_passthrough_values
|
|
7
|
+
@bt_passthrough_values ||= (instance_methods - ActiveRecord::Base.instance_methods - %i[options= batch_id= id_value= type=]).map(&:to_s).select { |m| m.end_with?('=') }.map { |m| m[0..-2] }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
serialize :extra_options, coder: LazyAccess
|
|
13
|
+
|
|
14
|
+
after_initialize do
|
|
15
|
+
self.extra_options ||= HashWithIndifferentAccess.new
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def options
|
|
20
|
+
@task_options_view ||= TaskOptionsView.new(self)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class TaskOptionsView
|
|
24
|
+
def initialize(context)
|
|
25
|
+
@context = context
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](key)
|
|
29
|
+
if @context.class.bt_passthrough_values.include?(key.to_s)
|
|
30
|
+
@context.public_send(key.to_sym)
|
|
31
|
+
else
|
|
32
|
+
inner_hash[key]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def []=(key, value)
|
|
37
|
+
if @context.class.bt_passthrough_values.include?(key.to_s)
|
|
38
|
+
@context.public_send("#{key.to_sym}=", value)
|
|
39
|
+
else
|
|
40
|
+
inner_hash[key] = value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def merge(other); to_h.merge!(other); end
|
|
45
|
+
|
|
46
|
+
def merge!(other)
|
|
47
|
+
other.each do |k, v|
|
|
48
|
+
self[k] = v
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def keys
|
|
53
|
+
inner_hash.keys + @context.class.bt_passthrough_values
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def key?(key)
|
|
57
|
+
if @context.class.bt_passthrough_values.include?(key.to_s)
|
|
58
|
+
@context.public_send(key.to_sym).present?
|
|
59
|
+
else
|
|
60
|
+
inner_hash.key?(key)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# def to_h
|
|
65
|
+
# inner_hash.merge(@context.class.bt_passthrough_values.index_with {|k| @context.public_send(k.to_sym) })
|
|
66
|
+
# end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def inner_hash
|
|
71
|
+
@context.extra_options
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Joblin
|
|
2
|
+
module BackgroundTask::RetentionPolicy
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def record_retention(policy = nil)
|
|
7
|
+
if policy.nil?
|
|
8
|
+
@record_retention || superclass.try(:record_retention)
|
|
9
|
+
else
|
|
10
|
+
@record_retention = policy
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class BackgroundTaskCleaner < ActiveJob::Base
|
|
16
|
+
def perform
|
|
17
|
+
types = BackgroundTask.distinct.pluck(:type)
|
|
18
|
+
types.each do |type|
|
|
19
|
+
type = type.constantize
|
|
20
|
+
rp = type.record_retention
|
|
21
|
+
next unless rp
|
|
22
|
+
|
|
23
|
+
type.where("created_at < ?", rp.ago).destroy_all
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Joblin
|
|
2
|
+
class BackgroundTask < ActiveRecord::Base
|
|
3
|
+
include Options
|
|
4
|
+
include Executor
|
|
5
|
+
include RetentionPolicy
|
|
6
|
+
include Attachments
|
|
7
|
+
include ApiAccess
|
|
8
|
+
|
|
9
|
+
belongs_to :creator, polymorphic: true, optional: true
|
|
10
|
+
|
|
11
|
+
after_initialize do
|
|
12
|
+
self.workflow_state ||= 'unscheduled'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
scope :grouped_history, lambda { |history_length|
|
|
16
|
+
t = quoted_table_name
|
|
17
|
+
if history_length > 1
|
|
18
|
+
joins("LEFT OUTER JOIN #{t} AS t2 ON #{t}.type = t2.type AND #{t}.created_at < t2.created_at")
|
|
19
|
+
.group(:id).order(:type, created_at: :desc).having('count(*) < ?', history_length)
|
|
20
|
+
else
|
|
21
|
+
select("DISTINCT ON (#{t}.type) #{t}.*")
|
|
22
|
+
end
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def enqueue!
|
|
26
|
+
self.workflow_state = 'scheduled' if self.workflow_state == 'unscheduled'
|
|
27
|
+
|
|
28
|
+
enter_batch do
|
|
29
|
+
self.class::ExecutorJob.perform_later()
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cancel!
|
|
34
|
+
update!(workflow_state: 'cancelled')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
def perform
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def enter_batch(&blk)
|
|
44
|
+
b = batch_id.present? ? Joblin::Batching::Batch.new(batch_id) : Joblin::Batching::Batch.new.tap do |b|
|
|
45
|
+
update!(batch_id: b.bid)
|
|
46
|
+
b.description = "BackgroundTask #{id}"
|
|
47
|
+
b.on(:complete, BackgroundTaskCallbacks, id: id)
|
|
48
|
+
b.on(:success, BackgroundTaskCallbacks, id: id)
|
|
49
|
+
b.context[:background_task_id] = id
|
|
50
|
+
end
|
|
51
|
+
b.jobs(&blk)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class BackgroundTaskCallbacks
|
|
55
|
+
def on_complete(status, options)
|
|
56
|
+
tracker = BackgroundTask.find(options['id'])
|
|
57
|
+
return if tracker.workflow_state == 'cancelled'
|
|
58
|
+
|
|
59
|
+
tracker.workflow_state = status.success? ? 'completed' : 'failed'
|
|
60
|
+
tracker.save! if tracker.changed?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def on_success(status, options)
|
|
64
|
+
tracker = BackgroundTask.find(options['id'])
|
|
65
|
+
return if tracker.workflow_state == 'cancelled'
|
|
66
|
+
|
|
67
|
+
tracker.workflow_state = 'completed'
|
|
68
|
+
tracker.save! if tracker.changed?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Joblin::Concerns
|
|
2
|
+
module JobWorkingDirs
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
around_perform :cleanup_working_dir
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def working_dir
|
|
10
|
+
@working_dir ||= Dir.mktmpdir
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def cleanup_working_dir
|
|
16
|
+
yield if block_given?
|
|
17
|
+
ensure
|
|
18
|
+
FileUtils.remove_entry @working_dir if @working_dir
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateBackgroundTasks < ActiveRecord::Migration[5.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :joblin_background_tasks do |t|
|
|
4
|
+
t.references :creator, polymorphic: true
|
|
5
|
+
t.jsonb :extra_options
|
|
6
|
+
t.string :batch_id
|
|
7
|
+
t.string :type, null: false
|
|
8
|
+
t.string :workflow_state, null: false, default: 'unscheduled'
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/joblin.gemspec
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "joblin/version"
|
|
7
|
+
version = Joblin::VERSION
|
|
8
|
+
rescue LoadError
|
|
9
|
+
version = "0.0.0.docker"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Gem::Specification.new do |spec|
|
|
13
|
+
spec.name = "joblin"
|
|
14
|
+
spec.version = version
|
|
15
|
+
spec.authors = ["Ethan Knapp"]
|
|
16
|
+
spec.email = ["eknapp@instructure.com"]
|
|
17
|
+
|
|
18
|
+
spec.summary = "Gem for ActiveJob extensions"
|
|
19
|
+
spec.homepage = "https://instructure.com"
|
|
20
|
+
|
|
21
|
+
spec.files = Dir["{app,config,db,lib}/**/*", "README.md", "*.gemspec"]
|
|
22
|
+
spec.test_files = Dir["spec/**/*"]
|
|
23
|
+
spec.require_paths = ['lib']
|
|
24
|
+
|
|
25
|
+
spec.add_dependency 'rediconn', '~> 0.1.0'
|
|
26
|
+
spec.add_dependency 'rails', '>= 5', '< 9.0'
|
|
27
|
+
|
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3'
|
|
29
|
+
spec.add_development_dependency 'redis'
|
|
30
|
+
spec.add_development_dependency 'rspec-rails'
|
|
31
|
+
spec.add_development_dependency 'pg'
|
|
32
|
+
|
|
33
|
+
spec.add_development_dependency "appraisal"
|
|
34
|
+
spec.add_development_dependency 'combustion', '~> 1.3'
|
|
35
|
+
end
|