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,149 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'sidekiq/batch'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
module Joblin::Batching
|
|
7
|
+
module Compat
|
|
8
|
+
module Sidekiq
|
|
9
|
+
module WorkerExtension
|
|
10
|
+
def bid
|
|
11
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY].bid
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def batch
|
|
15
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def batch_context
|
|
19
|
+
batch&.context || {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def valid_within_batch?
|
|
23
|
+
batch.valid?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class SidekiqCallbackWorker
|
|
28
|
+
include ::Sidekiq::Worker
|
|
29
|
+
include WorkerExtension
|
|
30
|
+
include Batch::Callback::CallbackWorkerCommon
|
|
31
|
+
|
|
32
|
+
def self.enqueue_all(args, queue)
|
|
33
|
+
return if args.empty?
|
|
34
|
+
|
|
35
|
+
::Sidekiq::Client.push_bulk(
|
|
36
|
+
'class' => self,
|
|
37
|
+
'args' => args,
|
|
38
|
+
'queue' => queue
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class ClientMiddleware
|
|
44
|
+
include ::Sidekiq::ClientMiddleware if defined? ::Sidekiq::ClientMiddleware
|
|
45
|
+
|
|
46
|
+
def call(_worker, msg, _queue, _redis_pool = nil)
|
|
47
|
+
if (batch = Thread.current[CURRENT_BATCH_THREAD_KEY]) && should_handle_batch?(msg)
|
|
48
|
+
batch.append_jobs(msg['jid']) if (msg['bid'] = batch.bid)
|
|
49
|
+
end
|
|
50
|
+
yield
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def should_handle_batch?(msg)
|
|
54
|
+
return false if Joblin::Batching::Compat::Sidekiq.is_activejob_job?(msg)
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class ServerMiddleware
|
|
60
|
+
include ::Sidekiq::ServerMiddleware if defined? ::Sidekiq::ServerMiddleware
|
|
61
|
+
|
|
62
|
+
def call(_worker, msg, _queue)
|
|
63
|
+
if (bid = msg['bid'])
|
|
64
|
+
prev_batch = Thread.current[CURRENT_BATCH_THREAD_KEY]
|
|
65
|
+
begin
|
|
66
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY] = Batch.new(bid)
|
|
67
|
+
yield
|
|
68
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY].save_context_changes
|
|
69
|
+
Batch.process_successful_job(bid, msg['jid'])
|
|
70
|
+
rescue
|
|
71
|
+
Batch.process_failed_job(bid, msg['jid'])
|
|
72
|
+
raise
|
|
73
|
+
ensure
|
|
74
|
+
Thread.current[CURRENT_BATCH_THREAD_KEY] = prev_batch
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
yield
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.is_activejob_job?(msg)
|
|
83
|
+
return false unless defined?(::ActiveJob)
|
|
84
|
+
|
|
85
|
+
msg['class'] == 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper' && (msg['wrapped'].to_s).constantize < Joblin::Batching::Compat::ActiveJob::BatchAwareJob
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.switch_tenant(job)
|
|
89
|
+
if defined?(::Apartment)
|
|
90
|
+
::Apartment::Tenant.switch(job['apartment'] || 'public') do
|
|
91
|
+
yield
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
yield
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.sidekiq_middleware(placement, &blk)
|
|
99
|
+
install_middleware = ->(config) do
|
|
100
|
+
config.send("#{placement}_middleware") do |chain|
|
|
101
|
+
blk.call(chain)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
::Sidekiq.configure_client(&install_middleware) if placement == :client
|
|
106
|
+
::Sidekiq.configure_server(&install_middleware)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.configure
|
|
110
|
+
return if @already_configured
|
|
111
|
+
@already_configured = true
|
|
112
|
+
|
|
113
|
+
if defined?(::Sidekiq::Batch) && ::Sidekiq::Batch != Joblin::Batching::Batch
|
|
114
|
+
print "WARNING: Detected Sidekiq Pro or sidekiq-batch. Joblin JobBatches may not be fully compatible!"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
sidekiq_middleware(:client) do |chain|
|
|
118
|
+
chain.remove ::Sidekiq::Batch::Middleware::ClientMiddleware if defined?(::Sidekiq::Batch::Middleware::ClientMiddleware)
|
|
119
|
+
chain.add Joblin::Batching::Compat::Sidekiq::ClientMiddleware
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
sidekiq_middleware(:server) do |chain|
|
|
123
|
+
chain.remove ::Sidekiq::Batch::Middleware::ServerMiddleware if defined?(::Sidekiq::Batch::Middleware::ServerMiddleware)
|
|
124
|
+
chain.add Joblin::Batching::Compat::Sidekiq::ServerMiddleware
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
::Sidekiq.configure_server do |config|
|
|
128
|
+
config.death_handlers << ->(job, ex) do
|
|
129
|
+
switch_tenant(job) do
|
|
130
|
+
if is_activejob_job?(job)
|
|
131
|
+
Joblin::Batching::Compat::ActiveJob.handle_job_death(job["args"][0], ex)
|
|
132
|
+
elsif job['bid'].present?
|
|
133
|
+
::Sidekiq::Batch.process_dead_job(job['bid'], job['jid'])
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
::Sidekiq.const_set(:Batch, Joblin::Batching::Batch)
|
|
140
|
+
# This alias helps apartment-sidekiq set itself up correctly
|
|
141
|
+
::Sidekiq::Batch.const_set(:Server, Joblin::Batching::Compat::Sidekiq::ServerMiddleware)
|
|
142
|
+
::Sidekiq::Worker.send(:include, Joblin::Batching::Compat::Sidekiq::WorkerExtension)
|
|
143
|
+
Batch::Callback.worker_class = SidekiqCallbackWorker
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
require_relative 'sidekiq/web'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
module Joblin::Batching
|
|
3
|
+
module Compat
|
|
4
|
+
def self.load_compat(name)
|
|
5
|
+
name = name.to_s
|
|
6
|
+
begin
|
|
7
|
+
require name
|
|
8
|
+
rescue LoadError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
if name.classify.safe_constantize
|
|
12
|
+
require_relative "./compat/#{name}"
|
|
13
|
+
"Joblin::Batching::Compat::#{name.classify}".constantize.configure
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
load_compat(:active_job)
|
|
18
|
+
load_compat(:sidekiq)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module Joblin::Batching
|
|
2
|
+
class ContextHash
|
|
3
|
+
delegate_missing_to :flatten
|
|
4
|
+
|
|
5
|
+
def initialize(bid, hash = nil)
|
|
6
|
+
@bid_stack = [bid]
|
|
7
|
+
@hash_map = {}
|
|
8
|
+
@dirty = false
|
|
9
|
+
@flattened = nil
|
|
10
|
+
@hash_map[bid] = hash.with_indifferent_access if hash
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Local is "the nearest batch with a context value"
|
|
14
|
+
# This allows for, for example, SerialBatchJob to have a modifiable context stored on it's main Batch
|
|
15
|
+
# that can be accessed transparently from one of it's internal, context-less Batches
|
|
16
|
+
def local_bid
|
|
17
|
+
bid = @bid_stack[-1]
|
|
18
|
+
while bid.present?
|
|
19
|
+
bhash = resolve_hash(bid)
|
|
20
|
+
return bid if bhash
|
|
21
|
+
bid = get_parent_bid(bid)
|
|
22
|
+
end
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def local
|
|
27
|
+
@hash_map[local_bid]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def own
|
|
31
|
+
resolve_hash(@bid_stack[-1]) || {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def set_local(new_hash)
|
|
35
|
+
@dirty = true
|
|
36
|
+
local.clear.merge!(new_hash)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clear
|
|
40
|
+
local.clear
|
|
41
|
+
@flattened = nil
|
|
42
|
+
@dirty = true
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def []=(key, value)
|
|
47
|
+
@flattened = nil
|
|
48
|
+
@dirty = true
|
|
49
|
+
local[key] = value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def [](key)
|
|
53
|
+
bid = @bid_stack[-1]
|
|
54
|
+
while bid.present?
|
|
55
|
+
bhash = resolve_hash(bid)
|
|
56
|
+
return bhash[key] if bhash&.key?(key)
|
|
57
|
+
bid = get_parent_bid(bid)
|
|
58
|
+
end
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reload!
|
|
63
|
+
@dirty = false
|
|
64
|
+
@hash_map = {}
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def save!(force: false)
|
|
69
|
+
return unless dirty? || force
|
|
70
|
+
Batch.redis do |r|
|
|
71
|
+
r.hset("BID-#{local_bid}", 'context', JSON.unparse(local))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def dirty?
|
|
76
|
+
@dirty
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def is_a?(arg)
|
|
80
|
+
return true if Hash <= arg
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def flatten
|
|
85
|
+
return @flattened if @flattened
|
|
86
|
+
|
|
87
|
+
load_all
|
|
88
|
+
flattened = {}
|
|
89
|
+
@bid_stack.compact.each do |bid|
|
|
90
|
+
flattened.merge!(@hash_map[bid]) if @hash_map[bid]
|
|
91
|
+
end
|
|
92
|
+
flattened.freeze
|
|
93
|
+
|
|
94
|
+
@flattened = flattened.with_indifferent_access
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def to_h
|
|
98
|
+
flatten
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def get_parent_hash(bid)
|
|
104
|
+
resolve_hash(get_parent_bid(bid)).freeze
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def get_parent_bid(bid)
|
|
108
|
+
index = @bid_stack.index(bid)
|
|
109
|
+
raise "Invalid BID #{bid}" if index.nil? # Sanity Check - this shouldn't happen
|
|
110
|
+
|
|
111
|
+
index -= 1
|
|
112
|
+
if index >= 0
|
|
113
|
+
@bid_stack[index]
|
|
114
|
+
else
|
|
115
|
+
pbid = Batch.redis do |r|
|
|
116
|
+
callback_params = JSON.parse(r.hget("BID-#{bid}", "callback_params") || "{}")
|
|
117
|
+
callback_params['for_bid'] || r.hget("BID-#{bid}", "parent_bid")
|
|
118
|
+
end
|
|
119
|
+
@bid_stack.unshift(pbid)
|
|
120
|
+
pbid
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def resolve_hash(bid)
|
|
125
|
+
return nil unless bid.present?
|
|
126
|
+
return @hash_map[bid] if @hash_map.key?(bid)
|
|
127
|
+
|
|
128
|
+
context_json, editable = Batch.redis do |r|
|
|
129
|
+
r.multi do |r|
|
|
130
|
+
r.hget("BID-#{bid}", "context")
|
|
131
|
+
r.hget("BID-#{bid}", "allow_context_changes")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if context_json.present?
|
|
136
|
+
context_hash = JSON.parse(context_json)
|
|
137
|
+
context_hash = context_hash.with_indifferent_access
|
|
138
|
+
context_hash.each do |k, v|
|
|
139
|
+
v.freeze
|
|
140
|
+
end
|
|
141
|
+
context_hash.freeze unless editable
|
|
142
|
+
|
|
143
|
+
@hash_map[bid] = context_hash
|
|
144
|
+
else
|
|
145
|
+
@hash_map[bid] = nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def load_all
|
|
150
|
+
resolve_hash(@bid_stack[0]).freeze
|
|
151
|
+
while @bid_stack[0].present?
|
|
152
|
+
get_parent_hash(@bid_stack[0])
|
|
153
|
+
end
|
|
154
|
+
@hash_map
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
local function add_bids(root, depth)
|
|
3
|
+
local result_data = {}
|
|
4
|
+
|
|
5
|
+
if depth > 0 then
|
|
6
|
+
local sbids
|
|
7
|
+
if depth == tonumber(ARGV[2]) and ARGV[4] then
|
|
8
|
+
local min, max = ARGV[4]:match('(%d+):(%d+)')
|
|
9
|
+
sbids = redis.call('ZRANGE', 'BID-' .. root .. '-bids', min, max)
|
|
10
|
+
else
|
|
11
|
+
sbids = redis.call('ZRANGE', 'BID-' .. root .. '-bids', 0, tonumber(ARGV[3]) - 1)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
local sub_data = {}
|
|
15
|
+
for _,v in ipairs(sbids) do
|
|
16
|
+
table.insert(sub_data, add_bids(v, depth - 1))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
return { root, sub_data }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
return { root, result_data}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
return add_bids(ARGV[1], tonumber(ARGV[2]))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative './base_job'
|
|
2
|
+
|
|
3
|
+
module Joblin::Batching
|
|
4
|
+
class ConcurrentBatchJob < BaseJob
|
|
5
|
+
def self.make_batch(sub_jobs, **kwargs, &blk)
|
|
6
|
+
ManagedBatchJob.make_batch(
|
|
7
|
+
sub_jobs,
|
|
8
|
+
concurrency: true,
|
|
9
|
+
**kwargs,
|
|
10
|
+
ordered: false,
|
|
11
|
+
desc_prefix: 'ConcurrentBatchJob: ',
|
|
12
|
+
&blk
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def perform(sub_jobs, **kwargs)
|
|
17
|
+
self.class.make_batch(sub_jobs, **kwargs)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
require_relative './base_job'
|
|
2
|
+
|
|
3
|
+
module Joblin::Batching
|
|
4
|
+
class ManagedBatchJob < BaseJob
|
|
5
|
+
def self.make_batch(sub_jobs, ordered: true, concurrency: nil, context: nil, preflight_check: nil, desc_prefix: nil, &blk)
|
|
6
|
+
desc_prefix ||= ''
|
|
7
|
+
|
|
8
|
+
if concurrency == 0 || concurrency == nil || concurrency == true
|
|
9
|
+
concurrency = sub_jobs.count
|
|
10
|
+
elsif concurrency == false
|
|
11
|
+
concurrency = 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
root_batch = Batch.new
|
|
15
|
+
man_batch_id = nil
|
|
16
|
+
|
|
17
|
+
if concurrency < sub_jobs.count
|
|
18
|
+
man_batch_id = SecureRandom.urlsafe_base64(10)
|
|
19
|
+
|
|
20
|
+
Batch.redis do |r|
|
|
21
|
+
r.multi do |r|
|
|
22
|
+
r.hset("MNGBID-#{man_batch_id}", "root_bid", root_batch.bid)
|
|
23
|
+
r.hset("MNGBID-#{man_batch_id}", "ordered", ordered ? 1 : 0)
|
|
24
|
+
r.hset("MNGBID-#{man_batch_id}", "concurrency", concurrency)
|
|
25
|
+
r.hset("MNGBID-#{man_batch_id}", "preflight_check", preflight_check) if preflight_check.present?
|
|
26
|
+
r.expire("MNGBID-#{man_batch_id}", Batch::BID_EXPIRE_TTL)
|
|
27
|
+
|
|
28
|
+
mapped_sub_jobs = sub_jobs.each_with_index.map do |j, i|
|
|
29
|
+
j['_mngbid_index_'] = i # This allows duplicate jobs when a Redis Set is used
|
|
30
|
+
j = ::ActiveJob::Arguments.serialize([j])
|
|
31
|
+
JSON.unparse(j)
|
|
32
|
+
end
|
|
33
|
+
if ordered
|
|
34
|
+
r.rpush("MNGBID-#{man_batch_id}-jobs", mapped_sub_jobs)
|
|
35
|
+
else
|
|
36
|
+
r.sadd("MNGBID-#{man_batch_id}-jobs", mapped_sub_jobs)
|
|
37
|
+
end
|
|
38
|
+
r.expire("MNGBID-#{man_batch_id}-jobs", Batch::BID_EXPIRE_TTL)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
root_batch.allow_context_changes = (concurrency == 1)
|
|
43
|
+
root_batch.on(:success, "#{to_s}.cleanup_redis", managed_batch_id: man_batch_id)
|
|
44
|
+
|
|
45
|
+
desc_prefix = "MGD(#{man_batch_id}): #{desc_prefix}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
root_batch.context = context
|
|
49
|
+
|
|
50
|
+
blk.call(ManagedBatchProxy.new(root_batch)) if blk.present?
|
|
51
|
+
|
|
52
|
+
root_batch.description = "#{desc_prefix}#{root_batch.description || 'Root'}"
|
|
53
|
+
|
|
54
|
+
root_batch.context["managed_batch_bid"] = man_batch_id if man_batch_id
|
|
55
|
+
|
|
56
|
+
if concurrency < sub_jobs.count
|
|
57
|
+
root_batch.placeholder!
|
|
58
|
+
concurrency.times do
|
|
59
|
+
perform_next_sequence_job(man_batch_id, skip_preflight: true)
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
root_batch.jobs do
|
|
63
|
+
sub_jobs.each do |j|
|
|
64
|
+
ChainBuilder.enqueue_job(j)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
root_batch
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def perform(sub_jobs, **kwargs)
|
|
73
|
+
self.class.make_batch(sub_jobs, **kwargs)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.cleanup_redis(status, options)
|
|
77
|
+
man_batch_id = options['managed_batch_id']
|
|
78
|
+
Batch.redis do |r|
|
|
79
|
+
r.del(
|
|
80
|
+
"MNGBID-#{man_batch_id}",
|
|
81
|
+
"MNGBID-#{man_batch_id}-jobs",
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.job_succeeded_callback(status, options)
|
|
87
|
+
man_batch_id = options['managed_batch_id']
|
|
88
|
+
perform_next_sequence_job(man_batch_id)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
protected
|
|
92
|
+
|
|
93
|
+
def self.perform_next_sequence_job(man_batch_id, skip_preflight: false)
|
|
94
|
+
root_bid, ordered, preflight_check = Batch.redis do |r|
|
|
95
|
+
r.multi do |r|
|
|
96
|
+
r.hget("MNGBID-#{man_batch_id}", "root_bid")
|
|
97
|
+
r.hget("MNGBID-#{man_batch_id}", "ordered")
|
|
98
|
+
r.hget("MNGBID-#{man_batch_id}", "preflight_check")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if !skip_preflight && preflight_check.present?
|
|
103
|
+
if preflight_check.include?(".")
|
|
104
|
+
clazz, method_name = preflight_check.split('.')
|
|
105
|
+
clazz = clazz.constantize
|
|
106
|
+
else
|
|
107
|
+
clazz = Object
|
|
108
|
+
method_name = preflight_check
|
|
109
|
+
end
|
|
110
|
+
preflight_check = ->(*args) { clazz.send(method_name, *args) }
|
|
111
|
+
else
|
|
112
|
+
preflight_check = ->(*args) { true }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
ordered = Joblin::MiscHelper.to_boolean(ordered)
|
|
116
|
+
|
|
117
|
+
loop do
|
|
118
|
+
next_job_json = Batch.redis do |r|
|
|
119
|
+
if ordered
|
|
120
|
+
r.lpop("MNGBID-#{man_batch_id}-jobs")
|
|
121
|
+
else
|
|
122
|
+
r.spop("MNGBID-#{man_batch_id}-jobs")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
break unless next_job_json.present?
|
|
127
|
+
|
|
128
|
+
next_job = JSON.parse(next_job_json)
|
|
129
|
+
next_job = ::ActiveJob::Arguments.deserialize(next_job)[0]
|
|
130
|
+
|
|
131
|
+
preflight_result = preflight_check.call(next_job)
|
|
132
|
+
if preflight_result == :abort
|
|
133
|
+
cleanup_redis(nil, { "managed_batch_id" => man_batch_id })
|
|
134
|
+
break
|
|
135
|
+
elsif !preflight_check
|
|
136
|
+
next
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
Batch.new(root_bid).jobs do
|
|
140
|
+
Batch.new.tap do |batch|
|
|
141
|
+
batch.description = "Managed Batch Fiber (#{man_batch_id})"
|
|
142
|
+
batch.on(:success, "#{self.to_s}.job_succeeded_callback", managed_batch_id: man_batch_id)
|
|
143
|
+
|
|
144
|
+
if next_job[:chain_link].present?
|
|
145
|
+
# Annotate Batch with chain-step info
|
|
146
|
+
batch.context["csb:chain_link"] = next_job[:chain_link]
|
|
147
|
+
batch.on(:complete, "#{ChainBuilder.to_s}.chain_step_complete", chain_link: next_job[:chain_link])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
batch.jobs do
|
|
151
|
+
ChainBuilder.enqueue_job(next_job)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
break
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
class ManagedBatchProxy
|
|
161
|
+
def initialize(real_batch)
|
|
162
|
+
@real_batch = real_batch
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
delegate_missing_to :real_batch
|
|
166
|
+
|
|
167
|
+
def jobs
|
|
168
|
+
raise "Managed Batches do not support calling .jobs directly!"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
attr_reader :real_batch
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative './base_job'
|
|
2
|
+
|
|
3
|
+
module Joblin::Batching
|
|
4
|
+
class SerialBatchJob < BaseJob
|
|
5
|
+
def self.make_batch(sub_jobs, **kwargs, &blk)
|
|
6
|
+
ManagedBatchJob.make_batch(
|
|
7
|
+
sub_jobs,
|
|
8
|
+
**kwargs,
|
|
9
|
+
ordered: true,
|
|
10
|
+
concurrency: false,
|
|
11
|
+
desc_prefix: 'SerialBatchJob: ',
|
|
12
|
+
&blk
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def perform(sub_jobs, **kwargs)
|
|
17
|
+
self.class.make_batch(sub_jobs, **kwargs)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|