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,537 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            require_relative "./callback"
         | 
| 3 | 
            +
            require_relative "./context_hash"
         | 
| 4 | 
            +
            require_relative "./status"
         | 
| 5 | 
            +
            require_relative "./pool"
         | 
| 6 | 
            +
            Dir[File.dirname(__FILE__) + "/jobs/*.rb"].each { |file| require file }
         | 
| 7 | 
            +
            require_relative "./chain_builder"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            # Implement Job Batching similar to Sidekiq::Batch. Supports ActiveJob and Sidekiq, or a mix thereof.
         | 
| 10 | 
            +
            # Much of this code is modifed/extended from https://github.com/breamware/sidekiq-batch
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            module Joblin::Batching
         | 
| 13 | 
            +
              CURRENT_BATCH_THREAD_KEY = :job_batches_batch
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              class Batch
         | 
| 16 | 
            +
                include RediConn::RedisModel
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                class NoBlockGivenError < StandardError; end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                delegate :redis, to: :class
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                BID_EXPIRE_TTL = 90.days.to_i
         | 
| 23 | 
            +
                INDEX_ALL_BATCHES = false
         | 
| 24 | 
            +
                SCHEDULE_CALLBACK = RediConn::RedisScript.new(Pathname.new(__FILE__) + "../schedule_callback.lua")
         | 
| 25 | 
            +
                BID_HIERARCHY = RediConn::RedisScript.new(Pathname.new(__FILE__) + "../hier_batch_ids.lua")
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                attr_reader :bid
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def self.current
         | 
| 30 | 
            +
                  Thread.current[CURRENT_BATCH_THREAD_KEY]
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def self.current_context
         | 
| 34 | 
            +
                  self.current&.context
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def initialize(existing_bid = nil)
         | 
| 38 | 
            +
                  @bid = existing_bid || SecureRandom.urlsafe_base64(10)
         | 
| 39 | 
            +
                  @existing = !(!existing_bid || existing_bid.empty?)  # Basically existing_bid.present?
         | 
| 40 | 
            +
                  @initialized = false
         | 
| 41 | 
            +
                  @bidkey = "BID-" + @bid.to_s
         | 
| 42 | 
            +
                  self.created_at = Time.now.utc.to_f unless @existing
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                redis_attr :description
         | 
| 46 | 
            +
                redis_attr :created_at
         | 
| 47 | 
            +
                redis_attr :callback_queue, read_only: false
         | 
| 48 | 
            +
                redis_attr :callback_params, :json
         | 
| 49 | 
            +
                redis_attr :allow_context_changes, :bool
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def context
         | 
| 52 | 
            +
                  return @context if defined?(@context)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  if (@initialized || @existing)
         | 
| 55 | 
            +
                    @context = ContextHash.new(bid)
         | 
| 56 | 
            +
                  else
         | 
| 57 | 
            +
                    @context = ContextHash.new(bid, {})
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def context=(value)
         | 
| 62 | 
            +
                  raise "context is read-only once the batch has been started" if (@initialized || @existing) # && !allow_context_changes
         | 
| 63 | 
            +
                  raise "context must be a Hash" unless value.is_a?(Hash) || value.nil?
         | 
| 64 | 
            +
                  return nil if value.nil? && @context.nil?
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  value = {} if value.nil?
         | 
| 67 | 
            +
                  value = value.local if value.is_a?(ContextHash)
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  @context ||= ContextHash.new(bid, {})
         | 
| 70 | 
            +
                  @context.set_local(value)
         | 
| 71 | 
            +
                  # persist_bid_attr('context', JSON.unparse(@context.local))
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def save_context_changes
         | 
| 75 | 
            +
                  @context&.save!
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                # Events:
         | 
| 79 | 
            +
                # :complete - triggered once all jobs have been executed, regardless of success or failure.
         | 
| 80 | 
            +
                # :success - triggered once all jobs haves successfully executed.
         | 
| 81 | 
            +
                # :death - triggered after any job enters the dead state.
         | 
| 82 | 
            +
                # :stagnated - triggered when a job dies and no other jobs/batches are active.
         | 
| 83 | 
            +
                def on(event, callback, options = {})
         | 
| 84 | 
            +
                  return unless Callback::VALID_CALLBACKS.include?(event.to_sym)
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  callback_key = "#{@bidkey}-callbacks-#{event}"
         | 
| 87 | 
            +
                  redis.multi do |r|
         | 
| 88 | 
            +
                    r.sadd(callback_key, JSON.unparse({
         | 
| 89 | 
            +
                      callback: callback,
         | 
| 90 | 
            +
                      opts: options
         | 
| 91 | 
            +
                    }))
         | 
| 92 | 
            +
                    r.expire(callback_key, BID_EXPIRE_TTL)
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def jobs
         | 
| 97 | 
            +
                  raise NoBlockGivenError unless block_given?
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  persist!
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  # TODO This keep_open! block is probably desired, but it has some caveats:
         | 
| 102 | 
            +
                  #  - Old logic didn't auto-clean empty batches
         | 
| 103 | 
            +
                  #  - Could be an issue if the Thread crashes at a bad moment (do we even need to plan for this?)
         | 
| 104 | 
            +
                  #  - Technically there could be a race condition without it, but such hasn't been observed
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  # keep_open! do
         | 
| 107 | 
            +
                  begin
         | 
| 108 | 
            +
                    parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
         | 
| 109 | 
            +
                    Thread.current[CURRENT_BATCH_THREAD_KEY] = self
         | 
| 110 | 
            +
                    yield
         | 
| 111 | 
            +
                  ensure
         | 
| 112 | 
            +
                    Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                  # end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                  nil
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                # Mark this Batch as a placeholder. It will be persisted to Redis, but no jobs will be added.
         | 
| 120 | 
            +
                # From here, you can either use `.jobs` on the batch to add jobs as usual, or call `.let_close!` to allow cleanup to occur.
         | 
| 121 | 
            +
                def placeholder!
         | 
| 122 | 
            +
                  # TODO Provide a stable `let_close!` token?
         | 
| 123 | 
            +
                  persist!
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def invalidate_all
         | 
| 127 | 
            +
                  redis.setex("invalidated-bid-#{bid}", BID_EXPIRE_TTL, 1)
         | 
| 128 | 
            +
                end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def parent_bid
         | 
| 131 | 
            +
                  redis.hget(@bidkey, "parent_bid")
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                def parent
         | 
| 135 | 
            +
                  if parent_bid
         | 
| 136 | 
            +
                    Batch.new(parent_bid)
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                def valid?(batch = self)
         | 
| 141 | 
            +
                  valid = !redis.exists?("invalidated-bid-#{batch.bid}")
         | 
| 142 | 
            +
                  batch.parent ? valid && valid?(batch.parent) : valid
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                def keep_open!(token = SecureRandom.urlsafe_base64(10))
         | 
| 146 | 
            +
                  if block_given?
         | 
| 147 | 
            +
                    begin
         | 
| 148 | 
            +
                      token = keep_open!(token)
         | 
| 149 | 
            +
                      yield
         | 
| 150 | 
            +
                    ensure
         | 
| 151 | 
            +
                      let_close!(token)
         | 
| 152 | 
            +
                    end
         | 
| 153 | 
            +
                  else
         | 
| 154 | 
            +
                    persist!
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                    redis.multi do |r|
         | 
| 157 | 
            +
                      r.sadd("#{@bidkey}-holds", token)
         | 
| 158 | 
            +
                      r.expire("#{@bidkey}-holds", BID_EXPIRE_TTL)
         | 
| 159 | 
            +
                    end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                    assert_batch_is_open
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                    token
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                def let_close!(token = :unset)
         | 
| 168 | 
            +
                  self.class.with_callback_check(bid, only: %i[complete success]) do |r|
         | 
| 169 | 
            +
                    if token == :unset # Legacy
         | 
| 170 | 
            +
                      r.del("#{@bidkey}-holds")
         | 
| 171 | 
            +
                      r.hset(@bidkey, 'keep_open', "false")
         | 
| 172 | 
            +
                    else
         | 
| 173 | 
            +
                      r.srem("#{@bidkey}-holds", token)
         | 
| 174 | 
            +
                    end
         | 
| 175 | 
            +
                  end
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                def self.with_batch(batch)
         | 
| 179 | 
            +
                  batch = self.new(batch) if batch.is_a?(String)
         | 
| 180 | 
            +
                  parent = Thread.current[CURRENT_BATCH_THREAD_KEY]
         | 
| 181 | 
            +
                  Thread.current[CURRENT_BATCH_THREAD_KEY] = batch
         | 
| 182 | 
            +
                  yield
         | 
| 183 | 
            +
                ensure
         | 
| 184 | 
            +
                  Thread.current[CURRENT_BATCH_THREAD_KEY] = parent
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                # Any Batches or Jobs created in the given block won't be assocaiated to the current batch
         | 
| 188 | 
            +
                def self.without_batch(&blk)
         | 
| 189 | 
            +
                  with_batch(nil, &blk)
         | 
| 190 | 
            +
                end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                def append_jobs(jids)
         | 
| 193 | 
            +
                  jids = Array(jids)
         | 
| 194 | 
            +
                  jids = jids.uniq
         | 
| 195 | 
            +
                  return unless jids.size > 0
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                  redis do |r|
         | 
| 198 | 
            +
                    tme = Time.now.utc.to_f
         | 
| 199 | 
            +
                    added = r.zadd(@bidkey + "-jids", jids.map{|jid| [tme, jid] }, nx: true)
         | 
| 200 | 
            +
                    r.multi do |r|
         | 
| 201 | 
            +
                      r.hincrby(@bidkey, "pending", added)
         | 
| 202 | 
            +
                      r.hincrby(@bidkey, "job_count", added)
         | 
| 203 | 
            +
                      r.expire(@bidkey, BID_EXPIRE_TTL)
         | 
| 204 | 
            +
                      r.expire(@bidkey + "-jids", BID_EXPIRE_TTL)
         | 
| 205 | 
            +
                    end
         | 
| 206 | 
            +
                  end
         | 
| 207 | 
            +
                end
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                protected
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                def redis_key
         | 
| 212 | 
            +
                  @bidkey
         | 
| 213 | 
            +
                end
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                def flush_pending_attrs
         | 
| 216 | 
            +
                  super
         | 
| 217 | 
            +
                  redis.zadd("batches", created_at, bid) if INDEX_ALL_BATCHES
         | 
| 218 | 
            +
                end
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                def assert_batch_is_open
         | 
| 221 | 
            +
                  unless defined?(@closed)
         | 
| 222 | 
            +
                    @closed = redis.hget(@bidkey, 'complete') == 'true'
         | 
| 223 | 
            +
                  end
         | 
| 224 | 
            +
                  raise "Cannot add jobs to Batch #{} bid - it has already entered the callback-stage" if @closed
         | 
| 225 | 
            +
                end
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                def persist!
         | 
| 228 | 
            +
                  if !@existing && !@initialized
         | 
| 229 | 
            +
                    parent_bid = Thread.current[CURRENT_BATCH_THREAD_KEY]&.bid
         | 
| 230 | 
            +
             | 
| 231 | 
            +
                    redis.multi do |r|
         | 
| 232 | 
            +
                      r.hset(@bidkey, "parent_bid", parent_bid.to_s) if parent_bid
         | 
| 233 | 
            +
                      r.expire(@bidkey, BID_EXPIRE_TTL)
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                      if parent_bid
         | 
| 236 | 
            +
                        r.hincrby("BID-#{parent_bid}", "children", 1)
         | 
| 237 | 
            +
                        r.expire("BID-#{parent_bid}", BID_EXPIRE_TTL)
         | 
| 238 | 
            +
                        r.zadd("BID-#{parent_bid}-bids", created_at, bid)
         | 
| 239 | 
            +
                      else
         | 
| 240 | 
            +
                        r.zadd("BID-ROOT-bids", created_at, bid)
         | 
| 241 | 
            +
                      end
         | 
| 242 | 
            +
                    end
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                    flush_pending_attrs
         | 
| 245 | 
            +
                    @context&.save!
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                    @initialized = true
         | 
| 248 | 
            +
                  end
         | 
| 249 | 
            +
                end
         | 
| 250 | 
            +
             | 
| 251 | 
            +
                class << self
         | 
| 252 | 
            +
                  def current
         | 
| 253 | 
            +
                    Thread.current[CURRENT_BATCH_THREAD_KEY]
         | 
| 254 | 
            +
                  end
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                  def current_context
         | 
| 257 | 
            +
                    current&.context || {}
         | 
| 258 | 
            +
                  end
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                  # Perform a success/failure/complete/etc transaction against Redis, checking
         | 
| 261 | 
            +
                  #   if any callbacks should be triggered
         | 
| 262 | 
            +
                  def with_callback_check(bid, except: [], only: Callback::VALID_CALLBACKS, &blk)
         | 
| 263 | 
            +
                    except = Array(except).map(&:to_sym)
         | 
| 264 | 
            +
                    only = Array(only).map(&:to_sym)
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                    precount = 0
         | 
| 267 | 
            +
             | 
| 268 | 
            +
                    all_results = redis do |r|
         | 
| 269 | 
            +
                      return unless r.exists?("BID-#{bid}")
         | 
| 270 | 
            +
             | 
| 271 | 
            +
                      r.multi do |r|
         | 
| 272 | 
            +
                        # The block probably doesn't _have_ to be part of the same transaction, but it was easier than
         | 
| 273 | 
            +
                        #   re-assessing possible race-conditions and is technically more performant
         | 
| 274 | 
            +
                        blk.call(r)
         | 
| 275 | 
            +
                        futures = r.instance_variable_get(:@futures) || r.instance_variable_get(:@pipeline)&.futures || r.instance_variable_get(:@client)&.futures
         | 
| 276 | 
            +
                        precount = futures.size
         | 
| 277 | 
            +
             | 
| 278 | 
            +
                        # Misc
         | 
| 279 | 
            +
                        r.hget("BID-#{bid}", "parent_bid")
         | 
| 280 | 
            +
                        r.hget("BID-#{bid}", "keep_open")
         | 
| 281 | 
            +
                        r.scard("BID-#{bid}-holds")
         | 
| 282 | 
            +
             | 
| 283 | 
            +
                        # Jobs
         | 
| 284 | 
            +
                        r.hincrby("BID-#{bid}", "pending", 0)
         | 
| 285 | 
            +
                        r.scard("BID-#{bid}-failed")
         | 
| 286 | 
            +
                        r.scard("BID-#{bid}-dead")
         | 
| 287 | 
            +
             | 
| 288 | 
            +
                        # Batches
         | 
| 289 | 
            +
                        r.hincrby("BID-#{bid}", "children", 0)
         | 
| 290 | 
            +
                        r.scard("BID-#{bid}-batches-complete")
         | 
| 291 | 
            +
                        r.scard("BID-#{bid}-batches-success")
         | 
| 292 | 
            +
                        r.scard("BID-#{bid}-batches-failed")
         | 
| 293 | 
            +
                        r.scard("BID-#{bid}-batches-stagnated")
         | 
| 294 | 
            +
             | 
| 295 | 
            +
                        # Touch Expirations
         | 
| 296 | 
            +
                        r.expire("BID-#{bid}", BID_EXPIRE_TTL)
         | 
| 297 | 
            +
                        r.expire("BID-#{bid}-batches-success", BID_EXPIRE_TTL)
         | 
| 298 | 
            +
                      end
         | 
| 299 | 
            +
                    end
         | 
| 300 | 
            +
             | 
| 301 | 
            +
                    # Exclude return values from the passed block
         | 
| 302 | 
            +
                    actual_results = all_results[precount..-1]
         | 
| 303 | 
            +
             | 
| 304 | 
            +
                    # "pending" = not successful (yet)
         | 
| 305 | 
            +
                    # "failed" = dead or retrying
         | 
| 306 | 
            +
                    # "complete" = successful or failed
         | 
| 307 | 
            +
             | 
| 308 | 
            +
                    parent_bid, keep_open, holds, \
         | 
| 309 | 
            +
                      pending_jobs, failed_jobs, dead_jobs, \
         | 
| 310 | 
            +
                      child_batches, complete_batches, success_batches, failed_batches, stagnated_batches \
         | 
| 311 | 
            +
                      = actual_results
         | 
| 312 | 
            +
             | 
| 313 | 
            +
                    pending_batches = child_batches - success_batches
         | 
| 314 | 
            +
             | 
| 315 | 
            +
                    if keep_open == 'true' || (holds && holds > 0)
         | 
| 316 | 
            +
                      except << :complete
         | 
| 317 | 
            +
                      except << :success
         | 
| 318 | 
            +
                    end
         | 
| 319 | 
            +
             | 
| 320 | 
            +
                    trigger_callback = ->(callback) {
         | 
| 321 | 
            +
                      next if except.include?(callback.to_sym)
         | 
| 322 | 
            +
                      next unless only.include?(callback.to_sym)
         | 
| 323 | 
            +
             | 
| 324 | 
            +
                      Batch.logger.debug {"Finalize #{callback} bid: #{parent_bid}"}
         | 
| 325 | 
            +
                      enqueue_callbacks(callback, bid)
         | 
| 326 | 
            +
                    }
         | 
| 327 | 
            +
             | 
| 328 | 
            +
                    # Handling all of these cases in one method may be a little less performant than in more specialized methods, but
         | 
| 329 | 
            +
                    #   I was dealing with duplicate Redis queries and checks being made in up to 4 places and it was getting out of
         | 
| 330 | 
            +
                    #   hand - this combined method should be easier to maintain and reason about
         | 
| 331 | 
            +
             | 
| 332 | 
            +
                    all_successful = pending_jobs.zero? && child_batches == success_batches
         | 
| 333 | 
            +
             | 
| 334 | 
            +
                    if all_successful || (pending_jobs == failed_jobs && child_batches == complete_batches) # All Complete
         | 
| 335 | 
            +
                      trigger_callback.call(:complete)
         | 
| 336 | 
            +
                    end
         | 
| 337 | 
            +
             | 
| 338 | 
            +
                    if all_successful # All Successful
         | 
| 339 | 
            +
                      trigger_callback.call(:success)
         | 
| 340 | 
            +
                    elsif pending_jobs == dead_jobs && pending_batches == stagnated_batches # Stagnated
         | 
| 341 | 
            +
                      trigger_callback.call(:stagnated)
         | 
| 342 | 
            +
                    end
         | 
| 343 | 
            +
             | 
| 344 | 
            +
                    all_results[0...precount]
         | 
| 345 | 
            +
                  end
         | 
| 346 | 
            +
             | 
| 347 | 
            +
                  def process_failed_job(bid, jid)
         | 
| 348 | 
            +
                    with_callback_check(bid, except: [:success]) do |r|
         | 
| 349 | 
            +
                      r.sadd("BID-#{bid}-failed", jid)
         | 
| 350 | 
            +
                      r.expire("BID-#{bid}-failed", BID_EXPIRE_TTL)
         | 
| 351 | 
            +
                    end
         | 
| 352 | 
            +
                  end
         | 
| 353 | 
            +
             | 
| 354 | 
            +
                  # Dead jobs are a Sidekiq feature.
         | 
| 355 | 
            +
                  # If this is called for a job, process_failed_job was also called
         | 
| 356 | 
            +
                  def process_dead_job(bid, jid)
         | 
| 357 | 
            +
                    enqueue_callbacks(:death, bid)
         | 
| 358 | 
            +
             | 
| 359 | 
            +
                    with_callback_check(bid) do |r|
         | 
| 360 | 
            +
                      r.sadd("BID-#{bid}-dead", jid)
         | 
| 361 | 
            +
                      r.expire("BID-#{bid}-dead", BID_EXPIRE_TTL)
         | 
| 362 | 
            +
                    end
         | 
| 363 | 
            +
                  end
         | 
| 364 | 
            +
             | 
| 365 | 
            +
                  def process_successful_job(bid, jid)
         | 
| 366 | 
            +
                    with_callback_check(bid) do |r|
         | 
| 367 | 
            +
                      r.srem("BID-#{bid}-failed", jid)
         | 
| 368 | 
            +
                      r.hincrby("BID-#{bid}", "pending", -1)
         | 
| 369 | 
            +
                      r.hincrby("BID-#{bid}", "successful-jobs", 1)
         | 
| 370 | 
            +
                      r.zrem("BID-#{bid}-jids", jid)
         | 
| 371 | 
            +
                    end
         | 
| 372 | 
            +
                  end
         | 
| 373 | 
            +
             | 
| 374 | 
            +
                  def enqueue_callbacks(event, bid)
         | 
| 375 | 
            +
                    batch_key = "BID-#{bid}"
         | 
| 376 | 
            +
                    callback_key = "#{batch_key}-callbacks-#{event}"
         | 
| 377 | 
            +
             | 
| 378 | 
            +
                    exists, callbacks, queue, parent_bid, callback_params = redis do |r|
         | 
| 379 | 
            +
                      r.multi do |r|
         | 
| 380 | 
            +
                        r.exists?(batch_key)
         | 
| 381 | 
            +
             | 
| 382 | 
            +
                        r.smembers(callback_key)
         | 
| 383 | 
            +
                        r.hget(batch_key, "callback_queue")
         | 
| 384 | 
            +
                        r.hget(batch_key, "parent_bid")
         | 
| 385 | 
            +
                        r.hget(batch_key, "callback_params")
         | 
| 386 | 
            +
                      end
         | 
| 387 | 
            +
                    end
         | 
| 388 | 
            +
             | 
| 389 | 
            +
                    return unless exists
         | 
| 390 | 
            +
             | 
| 391 | 
            +
                    queue ||= "default"
         | 
| 392 | 
            +
                    parent_bid = !parent_bid || parent_bid.empty? ? nil : parent_bid # Basically parent_bid.blank?
         | 
| 393 | 
            +
             | 
| 394 | 
            +
                    # Internal callback params. If this is present, we're trying to enqueue callbacks for a callback, which is a special case that
         | 
| 395 | 
            +
                    # indicates that the callback completed and we need to close the triggering batch (which is in a done-but-not-cleaned state)
         | 
| 396 | 
            +
                    callback_params = JSON.parse(callback_params) if callback_params.present?
         | 
| 397 | 
            +
             | 
| 398 | 
            +
                    # User-configured parameters/arguments to pass to the callback
         | 
| 399 | 
            +
                    callback_args = callbacks.reduce([]) do |memo, jcb|
         | 
| 400 | 
            +
                      cb = JSON.load(jcb)
         | 
| 401 | 
            +
                      memo << [cb['callback'], event.to_s, cb['opts'], bid, parent_bid]
         | 
| 402 | 
            +
                    end
         | 
| 403 | 
            +
             | 
| 404 | 
            +
                    opts = {"bid" => bid, "event" => event}
         | 
| 405 | 
            +
                    should_schedule_batch = callback_args.present? && !callback_params.present?
         | 
| 406 | 
            +
                    already_processed = redis do |r|
         | 
| 407 | 
            +
                      SCHEDULE_CALLBACK.call(r, [batch_key], [event.to_s, should_schedule_batch.to_s, BID_EXPIRE_TTL])
         | 
| 408 | 
            +
                    end
         | 
| 409 | 
            +
             | 
| 410 | 
            +
                    return if already_processed == 'true'
         | 
| 411 | 
            +
             | 
| 412 | 
            +
                    if should_schedule_batch
         | 
| 413 | 
            +
                      logger.debug {"Enqueue callback bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
         | 
| 414 | 
            +
             | 
| 415 | 
            +
                      # Create a new Batch to handle the callbacks and add it to the _parent_ batch
         | 
| 416 | 
            +
                      #   (this ensures that the parent's lifecycle status can't change until the child's callbacks are done)
         | 
| 417 | 
            +
                      with_batch(parent_bid) do
         | 
| 418 | 
            +
                        cb_batch = self.new
         | 
| 419 | 
            +
                        cb_batch.callback_params = {
         | 
| 420 | 
            +
                          for_bid: bid,
         | 
| 421 | 
            +
                          event: event,
         | 
| 422 | 
            +
                        }
         | 
| 423 | 
            +
                        opts['callback_bid'] = cb_batch.bid
         | 
| 424 | 
            +
             | 
| 425 | 
            +
                        logger.debug {"Adding callback batch: #{cb_batch.bid} for batch: #{bid}"}
         | 
| 426 | 
            +
                        cb_batch.jobs do
         | 
| 427 | 
            +
                          push_callbacks(callback_args, queue)
         | 
| 428 | 
            +
                        end
         | 
| 429 | 
            +
                      end
         | 
| 430 | 
            +
                    end
         | 
| 431 | 
            +
             | 
| 432 | 
            +
                    if callback_params.present?
         | 
| 433 | 
            +
                      # This is a callback for a callback. Passing `origin` to the Finalizer allows it to also cleanup the original/callback-triggering batch
         | 
| 434 | 
            +
                      opts['origin'] = callback_params
         | 
| 435 | 
            +
                    end
         | 
| 436 | 
            +
             | 
| 437 | 
            +
                    # The Finalizer marks this batch as complete, bumps any necessary counters, cleans up this Batch _if_ no callbacks were scheduled,
         | 
| 438 | 
            +
                    #   and enqueues parent-Batch callbacks if needed.
         | 
| 439 | 
            +
                    logger.debug {"Run batch finalizer bid: #{bid} event: #{event} args: #{callback_args.inspect}"}
         | 
| 440 | 
            +
                    finalizer = Batch::Callback::Finalize.new
         | 
| 441 | 
            +
                    status = Status.new bid
         | 
| 442 | 
            +
                    finalizer.dispatch(status, opts)
         | 
| 443 | 
            +
                  end
         | 
| 444 | 
            +
             | 
| 445 | 
            +
                  def cleanup_redis(bid)
         | 
| 446 | 
            +
                    logger.debug {"Cleaning redis of batch #{bid}"}
         | 
| 447 | 
            +
                    redis do |r|
         | 
| 448 | 
            +
                      r.zrem("batches", bid)
         | 
| 449 | 
            +
                      r.zrem("BID-ROOT-bids", bid)
         | 
| 450 | 
            +
                      r.unlink(
         | 
| 451 | 
            +
                        "BID-#{bid}",
         | 
| 452 | 
            +
                        "BID-#{bid}-callbacks-complete",
         | 
| 453 | 
            +
                        "BID-#{bid}-callbacks-success",
         | 
| 454 | 
            +
                        "BID-#{bid}-failed",
         | 
| 455 | 
            +
                        "BID-#{bid}-dead",
         | 
| 456 | 
            +
             | 
| 457 | 
            +
                        "BID-#{bid}-batches-success",
         | 
| 458 | 
            +
                        "BID-#{bid}-batches-complete",
         | 
| 459 | 
            +
                        "BID-#{bid}-batches-failed",
         | 
| 460 | 
            +
                        "BID-#{bid}-bids",
         | 
| 461 | 
            +
                        "BID-#{bid}-jids",
         | 
| 462 | 
            +
                        "BID-#{bid}-pending_callbacks",
         | 
| 463 | 
            +
                      )
         | 
| 464 | 
            +
                    end
         | 
| 465 | 
            +
                  end
         | 
| 466 | 
            +
             | 
| 467 | 
            +
                  def delete_prematurely!(bid)
         | 
| 468 | 
            +
                    child_bids = redis do |r|
         | 
| 469 | 
            +
                      r.zrange("BID-#{bid}-bids", 0, -1)
         | 
| 470 | 
            +
                    end
         | 
| 471 | 
            +
                    child_bids.each do |cbid|
         | 
| 472 | 
            +
                      delete_prematurely!(cbid)
         | 
| 473 | 
            +
                    end
         | 
| 474 | 
            +
                    cleanup_redis(bid)
         | 
| 475 | 
            +
                  end
         | 
| 476 | 
            +
             | 
| 477 | 
            +
                  # Internal method to cleanup a Redis Hash and related keys
         | 
| 478 | 
            +
                  def cleanup_redis_index_for(key, suffixes = [""])
         | 
| 479 | 
            +
                    redis do |r|
         | 
| 480 | 
            +
                      if r.hget(k, "created_at").present?
         | 
| 481 | 
            +
                        r.multi do |r|
         | 
| 482 | 
            +
                          suffixes.each do |suffix|
         | 
| 483 | 
            +
                            r.expire(key + suffix, BID_EXPIRE_TTL)
         | 
| 484 | 
            +
                          end
         | 
| 485 | 
            +
                        end
         | 
| 486 | 
            +
                        false
         | 
| 487 | 
            +
                      else
         | 
| 488 | 
            +
                        r.multi do |r|
         | 
| 489 | 
            +
                          suffixes.each do |suffix|
         | 
| 490 | 
            +
                            r.unlink(key + suffix)
         | 
| 491 | 
            +
                          end
         | 
| 492 | 
            +
                        end
         | 
| 493 | 
            +
                        true
         | 
| 494 | 
            +
                      end
         | 
| 495 | 
            +
                    end
         | 
| 496 | 
            +
                  end
         | 
| 497 | 
            +
             | 
| 498 | 
            +
                  # Administrative/console method to cleanup expired batches from the WebUI
         | 
| 499 | 
            +
                  def cleanup_redis_index!
         | 
| 500 | 
            +
                    suffixes = ["", "-callbacks-complete", "-callbacks-success", "-failed", "-dead", "-batches-success", "-batches-complete", "-batches-failed", "-bids", "-jids", "-pending_callbacks"]
         | 
| 501 | 
            +
             | 
| 502 | 
            +
                    redis do |r|
         | 
| 503 | 
            +
                      cleanup_index = ->(index) {
         | 
| 504 | 
            +
                        r.zrangebyscore(index, "0", BID_EXPIRE_TTL.seconds.ago.to_i).each do |bid|
         | 
| 505 | 
            +
                          r.zrem(index, bid) if cleanup_redis_index_for("BID-#{bid}", suffixes)
         | 
| 506 | 
            +
                        end
         | 
| 507 | 
            +
                      }
         | 
| 508 | 
            +
             | 
| 509 | 
            +
                      cleanup_index.("BID-ROOT-bids")
         | 
| 510 | 
            +
                      cleanup_index.("batches")
         | 
| 511 | 
            +
                    end
         | 
| 512 | 
            +
                  end
         | 
| 513 | 
            +
             | 
| 514 | 
            +
                  def redis(&blk)
         | 
| 515 | 
            +
                    ::Joblin.redis(&blk)
         | 
| 516 | 
            +
                  end
         | 
| 517 | 
            +
             | 
| 518 | 
            +
                  def logger
         | 
| 519 | 
            +
                    ::Joblin.logger
         | 
| 520 | 
            +
                  end
         | 
| 521 | 
            +
             | 
| 522 | 
            +
                  def push_callbacks(args, queue)
         | 
| 523 | 
            +
                    Batch::Callback::worker_class.enqueue_all(args, queue)
         | 
| 524 | 
            +
                  end
         | 
| 525 | 
            +
             | 
| 526 | 
            +
                  def bid_hierarchy(bid, depth: 4, per_depth: 5, slice: nil)
         | 
| 527 | 
            +
                    args = [bid, depth, per_depth]
         | 
| 528 | 
            +
                    args << slice if slice
         | 
| 529 | 
            +
                    redis do |r|
         | 
| 530 | 
            +
                      BID_HIERARCHY.call(r, [], args)
         | 
| 531 | 
            +
                    end
         | 
| 532 | 
            +
                  end
         | 
| 533 | 
            +
                end
         | 
| 534 | 
            +
              end
         | 
| 535 | 
            +
            end
         | 
| 536 | 
            +
             | 
| 537 | 
            +
            require_relative './compat'
         | 
| @@ -0,0 +1,135 @@ | |
| 1 | 
            +
            module Joblin::Batching
         | 
| 2 | 
            +
              class Batch
         | 
| 3 | 
            +
                module Callback
         | 
| 4 | 
            +
                  mattr_accessor :worker_class
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  VALID_CALLBACKS = %i[success complete death stagnated].freeze
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  module CallbackWorkerCommon
         | 
| 9 | 
            +
                    def perform(definition, event, opts, bid, parent_bid)
         | 
| 10 | 
            +
                      return unless VALID_CALLBACKS.include?(event.to_sym)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                      method = nil
         | 
| 13 | 
            +
                      target = :instance
         | 
| 14 | 
            +
                      clazz = definition
         | 
| 15 | 
            +
                      if clazz.is_a?(String)
         | 
| 16 | 
            +
                        if clazz.include?('#')
         | 
| 17 | 
            +
                          clazz, method = clazz.split("#")
         | 
| 18 | 
            +
                        elsif clazz.include?('.')
         | 
| 19 | 
            +
                          clazz, method = clazz.split(".")
         | 
| 20 | 
            +
                          target = :class
         | 
| 21 | 
            +
                        end
         | 
| 22 | 
            +
                      end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                      method ||= "on_#{event}"
         | 
| 25 | 
            +
                      status = Batch::Status.new(bid)
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                      if clazz && object = Object.const_get(clazz)
         | 
| 28 | 
            +
                        target = target == :instance ? object.new : object
         | 
| 29 | 
            +
                        if target.respond_to?(method, true)
         | 
| 30 | 
            +
                          target.send(method, status, opts.with_indifferent_access)
         | 
| 31 | 
            +
                        else
         | 
| 32 | 
            +
                          Batch.logger.warn("Invalid callback method #{definition} - #{target.to_s} does not respond to #{method}")
         | 
| 33 | 
            +
                        end
         | 
| 34 | 
            +
                      else
         | 
| 35 | 
            +
                        Batch.logger.warn("Invalid callback method #{definition} - Class #{clazz} not found")
         | 
| 36 | 
            +
                      end
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  class Finalize
         | 
| 41 | 
            +
                    # The methods in this class are called after all same-named callbacks have been
         | 
| 42 | 
            +
                    # completed for the passed Batch.
         | 
| 43 | 
            +
                    # These methods mainly handle bubbling events up to the parent Batch
         | 
| 44 | 
            +
                    # You could say that they are the callbacks for callbacks.
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    def dispatch(status, opts)
         | 
| 47 | 
            +
                      bid = opts["bid"]
         | 
| 48 | 
            +
                      event = opts["event"].to_sym
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                      Batch.logger.debug {"Finalize #{event} batch id: #{opts["bid"]}"}
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                      batch_status = Status.new bid
         | 
| 53 | 
            +
                      send(event, bid, batch_status, batch_status.parent_bid)
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                      Batch.redis do |r|
         | 
| 56 | 
            +
                        r.srem("BID-#{bid}-pending_callbacks", "#{event}-finalize")
         | 
| 57 | 
            +
                      end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                      if event == :success
         | 
| 60 | 
            +
                        if opts['origin'].present?
         | 
| 61 | 
            +
                          # This is a callback for a callback. In this case we need to check if we should cleanup the original bid.
         | 
| 62 | 
            +
                          origin_bid = opts['origin']['for_bid']
         | 
| 63 | 
            +
                          _, pending, success_ran = Batch.redis do |r|
         | 
| 64 | 
            +
                            r.multi do |r|
         | 
| 65 | 
            +
                              r.srem("BID-#{origin_bid}-pending_callbacks", opts['origin']['event'])
         | 
| 66 | 
            +
                              r.scard("BID-#{origin_bid}-pending_callbacks")
         | 
| 67 | 
            +
                              r.hget("BID-#{origin_bid}", "success")
         | 
| 68 | 
            +
                            end
         | 
| 69 | 
            +
                          end
         | 
| 70 | 
            +
                          Batch.cleanup_redis(origin_bid) if pending == 0 && success_ran == 'true'
         | 
| 71 | 
            +
                        end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                        if (Batch.redis {|r| r.scard("BID-#{bid}-pending_callbacks") }) == 0
         | 
| 74 | 
            +
                          Batch.cleanup_redis(bid)
         | 
| 75 | 
            +
                        end
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    def success(bid, status, parent_bid)
         | 
| 80 | 
            +
                      return unless parent_bid
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                      Batch.with_callback_check(parent_bid) do |r|
         | 
| 83 | 
            +
                        r.sadd("BID-#{parent_bid}-batches-success", bid)
         | 
| 84 | 
            +
                        r.srem("BID-#{parent_bid}-batches-failed", bid)
         | 
| 85 | 
            +
                        r.sadd("BID-#{parent_bid}-batches-complete", bid)
         | 
| 86 | 
            +
                      end
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    def complete(bid, status, parent_bid)
         | 
| 90 | 
            +
                      return unless parent_bid
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                      pending, children, success = Batch.redis do |r|
         | 
| 93 | 
            +
                        r.multi do |r|
         | 
| 94 | 
            +
                          r.hincrby("BID-#{bid}", "pending", 0)
         | 
| 95 | 
            +
                          r.hincrby("BID-#{bid}", "children", 0)
         | 
| 96 | 
            +
                          r.scard("BID-#{bid}-batches-success")
         | 
| 97 | 
            +
                        end
         | 
| 98 | 
            +
                      end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                      if !(pending.to_i.zero? && children == success)
         | 
| 101 | 
            +
                        # If batch was not successfull check and see if its parent is complete
         | 
| 102 | 
            +
                        # if the parent is complete we can trigger its complete callback.
         | 
| 103 | 
            +
                        #
         | 
| 104 | 
            +
                        # Otherwise, we don't want to to trigger the parent's :complete here (and
         | 
| 105 | 
            +
                        # we instead opt to have success tigger parent :complete) - this
         | 
| 106 | 
            +
                        # allows the success callback to add additional jobs to the parent batch
         | 
| 107 | 
            +
                        # before triggering :complete.
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                        Batch.with_callback_check(parent_bid, except: [:success]) do |r|
         | 
| 110 | 
            +
                          r.sadd("BID-#{parent_bid}-batches-complete", bid)
         | 
| 111 | 
            +
                          r.sadd("BID-#{parent_bid}-batches-failed", bid)
         | 
| 112 | 
            +
                        end
         | 
| 113 | 
            +
                      end
         | 
| 114 | 
            +
                    end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                    def death(bid, status, parent_bid)
         | 
| 117 | 
            +
                      return unless parent_bid
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                      # We only need to bubble the event here - other events (eg stagnation) will be checked and bubbled elsewhere.
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                      Batch.enqueue_callbacks(:death, parent_bid)
         | 
| 122 | 
            +
                    end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    def stagnated(bid, status, parent_bid)
         | 
| 125 | 
            +
                      return unless parent_bid
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                      Batch.with_callback_check(parent_bid) do |r|
         | 
| 128 | 
            +
                        r.sadd("BID-#{parent_bid}-batches-stagnated", bid)
         | 
| 129 | 
            +
                        r.expire("BID-#{parent_bid}-batches-stagnated", BID_EXPIRE_TTL)
         | 
| 130 | 
            +
                      end
         | 
| 131 | 
            +
                    end
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
              end
         | 
| 135 | 
            +
            end
         |