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.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +1 -0
  3. data/app/models/joblin/background_task/api_access.rb +148 -0
  4. data/app/models/joblin/background_task/attachments.rb +47 -0
  5. data/app/models/joblin/background_task/executor.rb +63 -0
  6. data/app/models/joblin/background_task/options.rb +75 -0
  7. data/app/models/joblin/background_task/retention_policy.rb +28 -0
  8. data/app/models/joblin/background_task.rb +72 -0
  9. data/app/models/joblin/concerns/job_working_dirs.rb +21 -0
  10. data/db/migrate/20250903184852_create_background_tasks.rb +12 -0
  11. data/joblin.gemspec +35 -0
  12. data/lib/joblin/batching/batch.rb +537 -0
  13. data/lib/joblin/batching/callback.rb +135 -0
  14. data/lib/joblin/batching/chain_builder.rb +247 -0
  15. data/lib/joblin/batching/compat/active_job.rb +108 -0
  16. data/lib/joblin/batching/compat/sidekiq/web/batches_assets/css/styles.less +182 -0
  17. data/lib/joblin/batching/compat/sidekiq/web/batches_assets/js/batch_tree.js +108 -0
  18. data/lib/joblin/batching/compat/sidekiq/web/batches_assets/js/util.js +2 -0
  19. data/lib/joblin/batching/compat/sidekiq/web/helpers.rb +41 -0
  20. data/lib/joblin/batching/compat/sidekiq/web/views/_batch_tree.erb +6 -0
  21. data/lib/joblin/batching/compat/sidekiq/web/views/_batches_table.erb +44 -0
  22. data/lib/joblin/batching/compat/sidekiq/web/views/_common.erb +13 -0
  23. data/lib/joblin/batching/compat/sidekiq/web/views/_jobs_table.erb +21 -0
  24. data/lib/joblin/batching/compat/sidekiq/web/views/_pagination.erb +26 -0
  25. data/lib/joblin/batching/compat/sidekiq/web/views/batch.erb +81 -0
  26. data/lib/joblin/batching/compat/sidekiq/web/views/batches.erb +23 -0
  27. data/lib/joblin/batching/compat/sidekiq/web/views/pool.erb +137 -0
  28. data/lib/joblin/batching/compat/sidekiq/web/views/pools.erb +47 -0
  29. data/lib/joblin/batching/compat/sidekiq/web.rb +218 -0
  30. data/lib/joblin/batching/compat/sidekiq.rb +149 -0
  31. data/lib/joblin/batching/compat.rb +20 -0
  32. data/lib/joblin/batching/context_hash.rb +157 -0
  33. data/lib/joblin/batching/hier_batch_ids.lua +25 -0
  34. data/lib/joblin/batching/jobs/base_job.rb +7 -0
  35. data/lib/joblin/batching/jobs/concurrent_batch_job.rb +20 -0
  36. data/lib/joblin/batching/jobs/managed_batch_job.rb +175 -0
  37. data/lib/joblin/batching/jobs/serial_batch_job.rb +20 -0
  38. data/lib/joblin/batching/pool.rb +254 -0
  39. data/lib/joblin/batching/pool_refill.lua +47 -0
  40. data/lib/joblin/batching/schedule_callback.lua +14 -0
  41. data/lib/joblin/batching/status.rb +89 -0
  42. data/lib/joblin/engine.rb +15 -0
  43. data/lib/joblin/lazy_access.rb +72 -0
  44. data/lib/joblin/uniqueness/compat/active_job.rb +75 -0
  45. data/lib/joblin/uniqueness/compat/sidekiq.rb +135 -0
  46. data/lib/joblin/uniqueness/compat.rb +20 -0
  47. data/lib/joblin/uniqueness/configuration.rb +25 -0
  48. data/lib/joblin/uniqueness/job_uniqueness.rb +49 -0
  49. data/lib/joblin/uniqueness/lock_context.rb +199 -0
  50. data/lib/joblin/uniqueness/locksmith.rb +92 -0
  51. data/lib/joblin/uniqueness/on_conflict/base.rb +32 -0
  52. data/lib/joblin/uniqueness/on_conflict/log.rb +13 -0
  53. data/lib/joblin/uniqueness/on_conflict/null_strategy.rb +9 -0
  54. data/lib/joblin/uniqueness/on_conflict/raise.rb +11 -0
  55. data/lib/joblin/uniqueness/on_conflict/reject.rb +21 -0
  56. data/lib/joblin/uniqueness/on_conflict/reschedule.rb +20 -0
  57. data/lib/joblin/uniqueness/on_conflict.rb +62 -0
  58. data/lib/joblin/uniqueness/strategy/base.rb +107 -0
  59. data/lib/joblin/uniqueness/strategy/until_and_while_executing.rb +35 -0
  60. data/lib/joblin/uniqueness/strategy/until_executed.rb +20 -0
  61. data/lib/joblin/uniqueness/strategy/until_executing.rb +20 -0
  62. data/lib/joblin/uniqueness/strategy/until_expired.rb +16 -0
  63. data/lib/joblin/uniqueness/strategy/while_executing.rb +26 -0
  64. data/lib/joblin/uniqueness/strategy.rb +27 -0
  65. data/lib/joblin/uniqueness/unique_job_common.rb +79 -0
  66. data/lib/joblin/version.rb +3 -0
  67. data/lib/joblin.rb +37 -0
  68. data/spec/batching/batch_spec.rb +493 -0
  69. data/spec/batching/callback_spec.rb +38 -0
  70. data/spec/batching/compat/active_job_spec.rb +107 -0
  71. data/spec/batching/compat/sidekiq_spec.rb +127 -0
  72. data/spec/batching/context_hash_spec.rb +54 -0
  73. data/spec/batching/flow_spec.rb +82 -0
  74. data/spec/batching/integration/fail_then_succeed.rb +42 -0
  75. data/spec/batching/integration/integration.rb +57 -0
  76. data/spec/batching/integration/nested.rb +88 -0
  77. data/spec/batching/integration/simple.rb +47 -0
  78. data/spec/batching/integration/workflow.rb +134 -0
  79. data/spec/batching/integration_helper.rb +50 -0
  80. data/spec/batching/pool_spec.rb +161 -0
  81. data/spec/batching/status_spec.rb +76 -0
  82. data/spec/batching/support/base_job.rb +19 -0
  83. data/spec/batching/support/sample_callback.rb +2 -0
  84. data/spec/internal/config/database.yml +5 -0
  85. data/spec/internal/config/routes.rb +5 -0
  86. data/spec/internal/config/storage.yml +3 -0
  87. data/spec/internal/db/combustion_test.sqlite +0 -0
  88. data/spec/internal/db/schema.rb +6 -0
  89. data/spec/internal/log/test.log +48200 -0
  90. data/spec/internal/public/favicon.ico +0 -0
  91. data/spec/models/background_task_spec.rb +41 -0
  92. data/spec/spec_helper.rb +29 -0
  93. data/spec/uniqueness/compat/active_job_spec.rb +49 -0
  94. data/spec/uniqueness/compat/sidekiq_spec.rb +68 -0
  95. data/spec/uniqueness/lock_context_spec.rb +106 -0
  96. data/spec/uniqueness/on_conflict/log_spec.rb +11 -0
  97. data/spec/uniqueness/on_conflict/raise_spec.rb +10 -0
  98. data/spec/uniqueness/on_conflict/reschedule_spec.rb +63 -0
  99. data/spec/uniqueness/on_conflict_spec.rb +16 -0
  100. data/spec/uniqueness/spec_helper.rb +19 -0
  101. data/spec/uniqueness/strategy/base_spec.rb +100 -0
  102. data/spec/uniqueness/strategy/until_and_while_executing_spec.rb +48 -0
  103. data/spec/uniqueness/strategy/until_executed_spec.rb +23 -0
  104. data/spec/uniqueness/strategy/until_executing_spec.rb +23 -0
  105. data/spec/uniqueness/strategy/until_expired_spec.rb +23 -0
  106. data/spec/uniqueness/strategy/while_executing_spec.rb +33 -0
  107. data/spec/uniqueness/support/lock_strategy.rb +28 -0
  108. data/spec/uniqueness/support/on_conflict.rb +24 -0
  109. data/spec/uniqueness/support/test_worker.rb +19 -0
  110. data/spec/uniqueness/unique_job_common_spec.rb +45 -0
  111. 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,7 @@
1
+ require 'active_job'
2
+
3
+ module Joblin::Batching
4
+ class BaseJob < ::ActiveJob::Base
5
+
6
+ end
7
+ end
@@ -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