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
         |