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,254 @@ | |
| 1 | 
            +
            module Joblin::Batching
         | 
| 2 | 
            +
              class Pool
         | 
| 3 | 
            +
                include RediConn::RedisModel
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                POOL_REFILL = RediConn::RedisScript.new(Pathname.new(__FILE__) + "../pool_refill.lua")
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                attr_reader :pid
         | 
| 8 | 
            +
                redis_attr :description
         | 
| 9 | 
            +
                redis_attr :created_at
         | 
| 10 | 
            +
                redis_attr :concurrency, :int
         | 
| 11 | 
            +
                redis_attr :complete_count, :int
         | 
| 12 | 
            +
                redis_attr :order
         | 
| 13 | 
            +
                redis_attr :on_failed_job, :symbol
         | 
| 14 | 
            +
                redis_attr :clean_when_empty, :bool
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def initialize(pooolid = nil, **kwargs)
         | 
| 17 | 
            +
                  if pooolid
         | 
| 18 | 
            +
                    @existing = true
         | 
| 19 | 
            +
                    @pid = pooolid
         | 
| 20 | 
            +
                  else
         | 
| 21 | 
            +
                    @pid = SecureRandom.urlsafe_base64(10)
         | 
| 22 | 
            +
                    initialize_new(**kwargs)
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def self.from_pid(pid)
         | 
| 27 | 
            +
                  raise "PID must be given" unless pid.present?
         | 
| 28 | 
            +
                  new(pid)
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def <<(job_desc)
         | 
| 32 | 
            +
                  add_job(job_desc)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def add_job(job_desc)
         | 
| 36 | 
            +
                  add_jobs([job_desc])
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def add_jobs(job_descs, skip_refill: false)
         | 
| 40 | 
            +
                  job_descs.each do |job_desc|
         | 
| 41 | 
            +
                    wrapper = Batch.new
         | 
| 42 | 
            +
                    wrapper.description = "Pool Job Wrapper (PID: #{pid})"
         | 
| 43 | 
            +
                    checkin_event = (on_failed_job == :wait) ? :success : :complete
         | 
| 44 | 
            +
                    wrapper.on(checkin_event, "#{self.class.to_s}.job_checked_in", pool_id: pid)
         | 
| 45 | 
            +
                    wrapper.placeholder!
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    job_desc = job_desc.symbolize_keys
         | 
| 48 | 
            +
                    job_desc = job_desc.merge!(
         | 
| 49 | 
            +
                      job: job_desc[:job].to_s,
         | 
| 50 | 
            +
                      pool_wrapper_batch: wrapper.bid,
         | 
| 51 | 
            +
                    )
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    push_job_to_pool(job_desc)
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                  refill_allotment unless skip_refill
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def keep_open!(token = SecureRandom.urlsafe_base64(10))
         | 
| 59 | 
            +
                  if block_given?
         | 
| 60 | 
            +
                    begin
         | 
| 61 | 
            +
                      token = keep_open!(token)
         | 
| 62 | 
            +
                      yield
         | 
| 63 | 
            +
                    ensure
         | 
| 64 | 
            +
                      let_close!(token)
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
                  else
         | 
| 67 | 
            +
                    redis.multi do |r|
         | 
| 68 | 
            +
                      r.sadd("#{redis_key}-holds", token)
         | 
| 69 | 
            +
                      r.expire("#{redis_key}-holds", Batch::BID_EXPIRE_TTL)
         | 
| 70 | 
            +
                    end
         | 
| 71 | 
            +
                    token
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def let_close!(token = :unset)
         | 
| 76 | 
            +
                  if token == :unset # Legacy
         | 
| 77 | 
            +
                    redis.del("#{redis_key}-holds")
         | 
| 78 | 
            +
                    redis.hset(redis_key, 'keep_open', 'false')
         | 
| 79 | 
            +
                  else
         | 
| 80 | 
            +
                    redis.srem("#{redis_key}-holds", token)
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  cleanup_if_empty
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def cleanup_redis
         | 
| 87 | 
            +
                  Batch.logger.debug {"Cleaning redis of pool #{pid}"}
         | 
| 88 | 
            +
                  redis do |r|
         | 
| 89 | 
            +
                    r.zrem("pools", pid)
         | 
| 90 | 
            +
                    r.unlink(
         | 
| 91 | 
            +
                      "#{redis_key}",
         | 
| 92 | 
            +
                      "#{redis_key}-jobs",
         | 
| 93 | 
            +
                    )
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                def cleanup_if_empty
         | 
| 98 | 
            +
                  self.order
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  activec, pactivec, pendingc, clean_when_empty, keep_open, holds = redis.multi do |r|
         | 
| 101 | 
            +
                    r.hlen("#{redis_key}-active")
         | 
| 102 | 
            +
                    r.hget(redis_key, "_active_count")
         | 
| 103 | 
            +
                    pending_count(r)
         | 
| 104 | 
            +
                    r.hget(redis_key, 'clean_when_empty')
         | 
| 105 | 
            +
                    r.hget(redis_key, 'keep_open')
         | 
| 106 | 
            +
                    r.scard("#{redis_key}-holds")
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  return if keep_open == 'true' || clean_when_empty == 'false' || (holds && holds > 0)
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  if activec <= 0 && (pactivec.try(:to_i) || 0) <= 0 && pendingc <= 0
         | 
| 112 | 
            +
                    cleanup_redis
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                def active_count(r = redis)
         | 
| 117 | 
            +
                  r.hlen("#{redis_key}-active") + r.hincrby(redis_key, "_active_count", 0)
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                def active_jobs(r = redis)
         | 
| 121 | 
            +
                  r.hvals("#{redis_key}-active").map {|desc| JSON.parse(desc)[0] }
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                def pending_count(r = redis)
         | 
| 125 | 
            +
                  jobs_key = "#{redis_key}-jobs"
         | 
| 126 | 
            +
                  order = self.order || 'fifo'
         | 
| 127 | 
            +
                  case order.to_sym
         | 
| 128 | 
            +
                  when :fifo, :lifo
         | 
| 129 | 
            +
                    r.llen(jobs_key)
         | 
| 130 | 
            +
                  when :random
         | 
| 131 | 
            +
                    r.scard(jobs_key)
         | 
| 132 | 
            +
                  when :priority
         | 
| 133 | 
            +
                    r.zcard(jobs_key)
         | 
| 134 | 
            +
                  end
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                def job_checked_in(status, options)
         | 
| 138 | 
            +
                  active_count = refill_allotment(status.bid)
         | 
| 139 | 
            +
                  cleanup_if_empty unless active_count > 0
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                def self.job_checked_in(status, options)
         | 
| 143 | 
            +
                  pid = options['pool_id']
         | 
| 144 | 
            +
                  from_pid(pid).job_checked_in(status, options)
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                # Administrative/console method to cleanup expired pools from the WebUI
         | 
| 148 | 
            +
                def self.cleanup_redis_index!
         | 
| 149 | 
            +
                  suffixes = ["", "-active", "-jobs"]
         | 
| 150 | 
            +
                  r.zrangebyscore("pools", "0", Batch::BID_EXPIRE_TTL.seconds.ago.to_i).each do |pid|
         | 
| 151 | 
            +
                    r.zrem("pools", pid) if Batch.cleanup_redis_index_for("POOLID-#{pid}", suffixes)
         | 
| 152 | 
            +
                  end
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                protected
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                def redis_key
         | 
| 158 | 
            +
                  "POOLID-#{pid}"
         | 
| 159 | 
            +
                end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                def refill_allotment(checkin_bid = nil)
         | 
| 162 | 
            +
                  active_count, job_descs = POOL_REFILL.call(redis, [redis_key, "#{redis_key}-jobs", "#{redis_key}-active"], [checkin_bid || ""])
         | 
| 163 | 
            +
                  return active_count if active_count < 0
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                  pending_job_descs = job_descs.dup
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  added_jobs = {}
         | 
| 168 | 
            +
                  failed_to_add_jobs = []
         | 
| 169 | 
            +
                  add_exception = nil
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                  while pending_job_descs.count > 0
         | 
| 172 | 
            +
                    begin
         | 
| 173 | 
            +
                      job_json = pending_job_descs.shift
         | 
| 174 | 
            +
                      job_desc = ::ActiveJob::Arguments.deserialize(JSON.parse(job_json))[0]&.symbolize_keys
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                      wbid = job_desc[:pool_wrapper_batch]
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                      Batch.new(wbid).jobs do
         | 
| 179 | 
            +
                        ChainBuilder.enqueue_job(job_desc)
         | 
| 180 | 
            +
                      end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                      added_jobs[wbid] = job_json
         | 
| 183 | 
            +
                    rescue => ex
         | 
| 184 | 
            +
                      failed_to_add_jobs << job_json
         | 
| 185 | 
            +
                      add_exception = ex
         | 
| 186 | 
            +
                    end
         | 
| 187 | 
            +
                  end
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                  redis.multi do |r|
         | 
| 190 | 
            +
                    r.mapped_hmset("#{redis_key}-active", added_jobs) if added_jobs.count > 0
         | 
| 191 | 
            +
                    # Release reserved slots now that we've added the jobs to `-active`
         | 
| 192 | 
            +
                    r.hincrby(redis_key, "_active_count", -job_descs.count)
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                    r.expire(redis_key, Batch::BID_EXPIRE_TTL)
         | 
| 195 | 
            +
                    r.expire("#{redis_key}-active", Batch::BID_EXPIRE_TTL)
         | 
| 196 | 
            +
                    r.expire("#{redis_key}-jobs", Batch::BID_EXPIRE_TTL)
         | 
| 197 | 
            +
                  end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                  # If this happens, we end up in a bad state (as we don't try to re-add items to the pool or refill_allotment again), but
         | 
| 200 | 
            +
                  #   this should be a _really_ rare case that should only occur if we've lost connection to Redis or something, so we're
         | 
| 201 | 
            +
                  #   operating on the assumption that if we get here, any recovery logic will fail too
         | 
| 202 | 
            +
                  if add_exception.present?
         | 
| 203 | 
            +
                    Batch.logger.error {"Error popping jobs from Pool #{pid}: #{add_exception}"}
         | 
| 204 | 
            +
                    raise add_exception
         | 
| 205 | 
            +
                  end
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                  active_count + added_jobs.count
         | 
| 208 | 
            +
                end
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                def push_job_to_pool(job_desc)
         | 
| 211 | 
            +
                  jobs_key = "#{redis_key}-jobs"
         | 
| 212 | 
            +
                  # This allows duplicate jobs when a Redis Set is used
         | 
| 213 | 
            +
                  job_desc[:_pool_random_key_] = SecureRandom.urlsafe_base64(10)
         | 
| 214 | 
            +
                  job_json = JSON.unparse(::ActiveJob::Arguments.serialize([job_desc]))
         | 
| 215 | 
            +
                  order = self.order
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                  redis.multi do |r|
         | 
| 218 | 
            +
                    case order.to_sym
         | 
| 219 | 
            +
                    when :fifo, :lifo
         | 
| 220 | 
            +
                      r.rpush(jobs_key, job_json)
         | 
| 221 | 
            +
                    when :random
         | 
| 222 | 
            +
                      r.sadd(jobs_key, job_json)
         | 
| 223 | 
            +
                    when :priority
         | 
| 224 | 
            +
                      r.zadd(jobs_key, job_desc[:priority] || 0, job_json)
         | 
| 225 | 
            +
                    end
         | 
| 226 | 
            +
                    r.expire(redis_key, Batch::BID_EXPIRE_TTL)
         | 
| 227 | 
            +
                    r.expire(jobs_key, Batch::BID_EXPIRE_TTL)
         | 
| 228 | 
            +
                  end
         | 
| 229 | 
            +
                end
         | 
| 230 | 
            +
             | 
| 231 | 
            +
                def self.redis(&blk)
         | 
| 232 | 
            +
                  Batch.redis &blk
         | 
| 233 | 
            +
                end
         | 
| 234 | 
            +
                delegate :redis, to: :class
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                def flush_pending_attrs
         | 
| 237 | 
            +
                  super
         | 
| 238 | 
            +
                  redis.expire(redis_key, Batch::BID_EXPIRE_TTL)
         | 
| 239 | 
            +
                  redis.zadd("pools", created_at, pid)
         | 
| 240 | 
            +
                end
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                private
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                def initialize_new(concurrency: nil, order: :fifo, clean_when_empty: true, on_failed_job: :wait, description: nil)
         | 
| 245 | 
            +
                  self.created_at = Time.now.utc.to_f
         | 
| 246 | 
            +
                  self.description = description
         | 
| 247 | 
            +
                  self.order = order
         | 
| 248 | 
            +
                  self.concurrency = concurrency
         | 
| 249 | 
            +
                  self.clean_when_empty = clean_when_empty
         | 
| 250 | 
            +
                  self.on_failed_job = on_failed_job
         | 
| 251 | 
            +
                  flush_pending_attrs
         | 
| 252 | 
            +
                end
         | 
| 253 | 
            +
              end
         | 
| 254 | 
            +
            end
         | 
| @@ -0,0 +1,47 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            local poolkey = KEYS[1]
         | 
| 3 | 
            +
            local qkey = KEYS[2]
         | 
| 4 | 
            +
            local activekey = KEYS[3]
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            local checkin_item = ARGV[1]
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            if redis.call('EXISTS', poolkey) == 0 then
         | 
| 9 | 
            +
                return { -1, {} } -- pool doesn't exist
         | 
| 10 | 
            +
            end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            if checkin_item ~= "" then
         | 
| 13 | 
            +
                redis.call("HDEL", activekey, checkin_item)
         | 
| 14 | 
            +
                redis.call("HINCRBY", poolkey, "complete_count", 1)
         | 
| 15 | 
            +
            end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            local pool_type = redis.call('HGET', poolkey, "order")
         | 
| 18 | 
            +
            local allotment = tonumber(redis.call("HGET", poolkey, "concurrency"))
         | 
| 19 | 
            +
            local active = redis.call("HLEN", activekey) + (redis.call("HGET", poolkey, "_active_count") or 0)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            local pop_count = allotment - active
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            local popped_items = {}
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            if pop_count > 0 then
         | 
| 26 | 
            +
                if pool_type == "fifo" then
         | 
| 27 | 
            +
                    popped_items = redis.call("LPOP", qkey, pop_count) or {}
         | 
| 28 | 
            +
                elseif pool_type == "lifo" then
         | 
| 29 | 
            +
                    popped_items = redis.call("RPOP", qkey, pop_count) or {}
         | 
| 30 | 
            +
                elseif pool_type == "random" then
         | 
| 31 | 
            +
                    popped_items = redis.call("SPOP", qkey, pop_count) or {}
         | 
| 32 | 
            +
                elseif pool_type == "priority" then
         | 
| 33 | 
            +
                    local temp_items = redis.call("ZPOPMAX", qkey, pop_count) or {}
         | 
| 34 | 
            +
                    for i,v in ipairs(temp_items) do
         | 
| 35 | 
            +
                        if i % 2 == 1 then
         | 
| 36 | 
            +
                            table.insert(popped_items, v)
         | 
| 37 | 
            +
                        end
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
            end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            -- Reserve slots for these jobs while we return to Ruby and deserialize them
         | 
| 43 | 
            +
            --  This could also be inlined by just storing a key in the queue and storing parameters
         | 
| 44 | 
            +
            --  in a Hash, but this seems more efficient.
         | 
| 45 | 
            +
            redis.call('HINCRBY', poolkey, "_active_count", #popped_items)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            return { active, popped_items }
         | 
| @@ -0,0 +1,14 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            local previously_scheduled = redis.call('HGET', KEYS[1], ARGV[1])
         | 
| 3 | 
            +
            redis.call('HSET', KEYS[1], ARGV[1], 'true')
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            if previously_scheduled ~= 'true' then
         | 
| 6 | 
            +
                local pcb_key = KEYS[1] .. '-pending_callbacks'
         | 
| 7 | 
            +
                redis.call('SADD', pcb_key, ARGV[1] .. '-finalize')
         | 
| 8 | 
            +
                if ARGV[2] == 'true' then
         | 
| 9 | 
            +
                    redis.call('SADD', pcb_key, ARGV[1])
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
                redis.call('EXPIRE', pcb_key, ARGV[3])
         | 
| 12 | 
            +
            end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            return previously_scheduled
         | 
| @@ -0,0 +1,89 @@ | |
| 1 | 
            +
            module Joblin::Batching
         | 
| 2 | 
            +
              class Batch
         | 
| 3 | 
            +
                class Status
         | 
| 4 | 
            +
                  attr_reader :bid
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(bid)
         | 
| 7 | 
            +
                    bid = bid.bid if bid.is_a?(Batch)
         | 
| 8 | 
            +
                    @bid = bid
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def join
         | 
| 12 | 
            +
                    raise "Not supported"
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def pending
         | 
| 16 | 
            +
                    Batch.redis { |r| r.hget("BID-#{bid}", 'pending') }.to_i
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def failures
         | 
| 20 | 
            +
                    Batch.redis { |r| r.scard("BID-#{bid}-failed") }.to_i
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def dead
         | 
| 24 | 
            +
                    Batch.redis { |r| r.scard("BID-#{bid}-dead") }.to_i
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def completed_count
         | 
| 28 | 
            +
                    job_count - pending
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def job_count
         | 
| 32 | 
            +
                    Batch.redis { |r| r.hget("BID-#{bid}", "job_count") }.to_i
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def created_at
         | 
| 36 | 
            +
                    Batch.redis { |r| r.hget("BID-#{bid}", 'created_at') }
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def parent_bid
         | 
| 40 | 
            +
                    Batch.redis { |r| r.hget("BID-#{bid}", "parent_bid") }
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def failure_info
         | 
| 44 | 
            +
                    Batch.redis { |r| r.smembers("BID-#{bid}-failed") } || []
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def complete?
         | 
| 48 | 
            +
                    'true' == Batch.redis { |r| r.hget("BID-#{bid}", 'complete') }
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def success?
         | 
| 52 | 
            +
                    'true' == Batch.redis { |r| r.hget("BID-#{bid}", 'success') }
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def child_count
         | 
| 56 | 
            +
                    Batch.redis { |r| r.hget("BID-#{bid}", 'children') }.to_i
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def completed_children_count
         | 
| 60 | 
            +
                    Batch.redis { |r| r.scard("BID-#{bid}-batches-complete") }.to_i
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def successful_children_count
         | 
| 64 | 
            +
                    Batch.redis { |r| r.scard("BID-#{bid}-batches-success") }.to_i
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  def failed_children_count
         | 
| 68 | 
            +
                    Batch.redis { |r| r.scard("BID-#{bid}-batches-failed") }.to_i
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def data
         | 
| 72 | 
            +
                    {
         | 
| 73 | 
            +
                      bid: bid,
         | 
| 74 | 
            +
                      failures: failures,
         | 
| 75 | 
            +
                      pending: pending,
         | 
| 76 | 
            +
                      created_at: created_at,
         | 
| 77 | 
            +
                      complete: complete?,
         | 
| 78 | 
            +
                      success: success?,
         | 
| 79 | 
            +
                      failure_info: failure_info,
         | 
| 80 | 
            +
                      parent_bid: parent_bid,
         | 
| 81 | 
            +
                      child_count: child_count,
         | 
| 82 | 
            +
                      completed_children_count: completed_children_count,
         | 
| 83 | 
            +
                      successful_children_count: successful_children_count,
         | 
| 84 | 
            +
                      failed_children_count: failed_children_count,
         | 
| 85 | 
            +
                    }
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
            end
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            require "rails"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Joblin
         | 
| 4 | 
            +
              class Engine < ::Rails::Engine
         | 
| 5 | 
            +
                isolate_namespace Joblin
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                initializer :append_migrations do |app|
         | 
| 8 | 
            +
                  config.paths["db/migrate"].expanded.each do |expanded_path|
         | 
| 9 | 
            +
                    app.config.paths["db/migrate"] << expanded_path
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
                  # Apartment will modify this, but it doesn't fully support engine migrations, so we'll reset it here
         | 
| 12 | 
            +
                  ActiveRecord::Migrator.migrations_paths = Rails.application.paths["db/migrate"].to_a
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
| @@ -0,0 +1,72 @@ | |
| 1 | 
            +
            module Joblin
         | 
| 2 | 
            +
              module LazyAccess
         | 
| 3 | 
            +
                extend ActiveSupport::Concern
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(...)
         | 
| 6 | 
            +
                  super
         | 
| 7 | 
            +
                  @access_cache = {}
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def self.raw
         | 
| 11 | 
            +
                  current = Thread.current[:lazy_access_read_raw]
         | 
| 12 | 
            +
                  Thread.current[:lazy_access_read_raw] = true
         | 
| 13 | 
            +
                  yield
         | 
| 14 | 
            +
                ensure
         | 
| 15 | 
            +
                  Thread.current[:lazy_access_read_raw] = current
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def self.load(val)
         | 
| 19 | 
            +
                  case val
         | 
| 20 | 
            +
                  when String
         | 
| 21 | 
            +
                    if val.start_with?("gid://")
         | 
| 22 | 
            +
                      val = GlobalID::Locator.locate(val)
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                  when Hash
         | 
| 25 | 
            +
                    val = LazyAccessHash.new(val)
         | 
| 26 | 
            +
                  when Array
         | 
| 27 | 
            +
                    val = LazyAccessArray.new(val)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                  val
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def self.dump(val)
         | 
| 33 | 
            +
                  case val
         | 
| 34 | 
            +
                  when Array
         | 
| 35 | 
            +
                    val.to_a.map {|x| dump(x) }
         | 
| 36 | 
            +
                  when Hash
         | 
| 37 | 
            +
                    val.to_h.transform_values{|x| dump(x) }
         | 
| 38 | 
            +
                  when ActiveRecord::Base
         | 
| 39 | 
            +
                    val.to_gid
         | 
| 40 | 
            +
                  else
         | 
| 41 | 
            +
                    val
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def [](key)
         | 
| 46 | 
            +
                  if Thread.current[:lazy_access_read_raw]
         | 
| 47 | 
            +
                    super
         | 
| 48 | 
            +
                  else
         | 
| 49 | 
            +
                    key = key.to_s if key.is_a?(Symbol)
         | 
| 50 | 
            +
                    unless @access_cache.key?(key)
         | 
| 51 | 
            +
                      val = super
         | 
| 52 | 
            +
                      @access_cache[key] = LazyAccess.load(val)
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
                    @access_cache[key]
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def []=(key, value)
         | 
| 59 | 
            +
                  key = key.to_s if key.is_a?(Symbol)
         | 
| 60 | 
            +
                  @access_cache[key] = value
         | 
| 61 | 
            +
                  super
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
              class LazyAccessHash < HashWithIndifferentAccess
         | 
| 66 | 
            +
                include LazyAccess
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
              class LazyAccessArray < Array
         | 
| 70 | 
            +
                include LazyAccess
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
            end
         | 
| @@ -0,0 +1,75 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Joblin::Uniqueness
         | 
| 3 | 
            +
              module Compat
         | 
| 4 | 
            +
                module ActiveJob
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  class ActiveJobLockContext < LockContext
         | 
| 7 | 
            +
                    def job_scheduled_at
         | 
| 8 | 
            +
                      job_instance&.scheduled_at
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    def reenqueue(schedule_in:)
         | 
| 12 | 
            +
                      job_class.set(
         | 
| 13 | 
            +
                        queue: job_queue.to_sym,
         | 
| 14 | 
            +
                        wait: schedule_in,
         | 
| 15 | 
            +
                        priortity: job_instance.priority,
         | 
| 16 | 
            +
                      ).perform_later(*job_instance.arguments)
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  module UniqueJobExtension
         | 
| 21 | 
            +
                    extend ActiveSupport::Concern
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    included do
         | 
| 24 | 
            +
                      set_callback(:enqueue, :around, prepend: true) do |job, block|
         | 
| 25 | 
            +
                        ctx = uniqueness_lock_context
         | 
| 26 | 
            +
                        @uniqueness_cache_data = ctx.cache_data
         | 
| 27 | 
            +
                        ctx.handle_lifecycle!(:enqueue, &block)
         | 
| 28 | 
            +
                      end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                      around_perform do |job, block|
         | 
| 31 | 
            +
                        ctx = uniqueness_lock_context
         | 
| 32 | 
            +
                        ctx.handle_lifecycle!(:perform, &block)
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    def serialize
         | 
| 37 | 
            +
                      super.tap do |data|
         | 
| 38 | 
            +
                        data['uniqueness_cache_data'] = @uniqueness_cache_data.stringify_keys
         | 
| 39 | 
            +
                      end
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    def deserialize(data)
         | 
| 43 | 
            +
                      super
         | 
| 44 | 
            +
                      @uniqueness_cache_data = data['uniqueness_cache_data']&.symbolize_keys
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    def uniqueness_lock_context
         | 
| 48 | 
            +
                      ActiveJobLockContext.new({
         | 
| 49 | 
            +
                        job_clazz: self.class,
         | 
| 50 | 
            +
                        jid: self.job_id,
         | 
| 51 | 
            +
                        args: self.arguments,
         | 
| 52 | 
            +
                        queue: self.queue_name,
         | 
| 53 | 
            +
                        **(@uniqueness_cache_data || {})
         | 
| 54 | 
            +
                      }, job_instance: self)
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  module JobExtension
         | 
| 59 | 
            +
                    extend ActiveSupport::Concern
         | 
| 60 | 
            +
                    include UniqueJobCommon
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    class_methods do
         | 
| 63 | 
            +
                      def ensure_uniqueness(**kwargs)
         | 
| 64 | 
            +
                        super(**kwargs)
         | 
| 65 | 
            +
                        include UniqueJobExtension
         | 
| 66 | 
            +
                      end
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  def self.configure
         | 
| 71 | 
            +
                    ::ActiveJob::Base.include JobExtension
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
            end
         | 
| @@ -0,0 +1,135 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Joblin::Uniqueness
         | 
| 3 | 
            +
              module Compat
         | 
| 4 | 
            +
                module Sidekiq
         | 
| 5 | 
            +
                  module WorkerExtension
         | 
| 6 | 
            +
                    extend ActiveSupport::Concern
         | 
| 7 | 
            +
                    include UniqueJobCommon
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    class_methods do
         | 
| 10 | 
            +
                      def ensure_uniqueness(**kwargs)
         | 
| 11 | 
            +
                        super(**kwargs)
         | 
| 12 | 
            +
                        if !(defined?(@@validated_config) && @@validated_config)
         | 
| 13 | 
            +
                          Compat::Sidekiq.validate_middleware_placement!()
         | 
| 14 | 
            +
                          @@validated_config = true
         | 
| 15 | 
            +
                        end
         | 
| 16 | 
            +
                      end
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  class SidekiqLockContext < LockContext
         | 
| 21 | 
            +
                    def job_scheduled_at
         | 
| 22 | 
            +
                      @job_instance&.[]("at")
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def reenqueue(schedule_in:)
         | 
| 26 | 
            +
                      job_class.set(queue: job_queue.to_sym).perform_in(schedule_in, *@job_instance["args"])
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  class CommonMiddleware
         | 
| 31 | 
            +
                    def lock_context(msg)
         | 
| 32 | 
            +
                      opts = worker_uniqueness(msg)
         | 
| 33 | 
            +
                      return nil unless opts
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      SidekiqLockContext.new({
         | 
| 36 | 
            +
                        job_clazz: msg['class'],
         | 
| 37 | 
            +
                        jid: msg['jid'],
         | 
| 38 | 
            +
                        queue: msg['queue'],
         | 
| 39 | 
            +
                        args: msg['args'],
         | 
| 40 | 
            +
                        # kwargs: msg['kwargs'],
         | 
| 41 | 
            +
                        **(msg['uniqueness_cache_data']&.symbolize_keys || {}),
         | 
| 42 | 
            +
                      }, job_instance: msg)
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    def worker_uniqueness(msg)
         | 
| 46 | 
            +
                      return nil if Compat::Sidekiq.is_activejob_job?(msg)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                      worker_class = msg['class'].constantize
         | 
| 49 | 
            +
                      return nil unless worker_class.respond_to?(:unique_job_options)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                      worker_class.unique_job_options
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  class ClientMiddleware < CommonMiddleware
         | 
| 56 | 
            +
                    include ::Sidekiq::ClientMiddleware if defined? ::Sidekiq::ClientMiddleware
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    def call(_worker, msg, _queue, _redis_pool = nil, &blk)
         | 
| 59 | 
            +
                      ctx = lock_context(msg)
         | 
| 60 | 
            +
                      return blk.call unless ctx
         | 
| 61 | 
            +
                      msg['uniqueness_cache_data'] = ctx.cache_data.stringify_keys
         | 
| 62 | 
            +
                      ctx.handle_lifecycle!(:enqueue, &blk)
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  class ServerMiddleware < CommonMiddleware
         | 
| 67 | 
            +
                    include ::Sidekiq::ServerMiddleware if defined? ::Sidekiq::ServerMiddleware
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    def call(_worker, msg, _queue, &blk)
         | 
| 70 | 
            +
                      ctx = lock_context(msg)
         | 
| 71 | 
            +
                      return blk.call unless ctx
         | 
| 72 | 
            +
                      ctx.handle_lifecycle!(:perform, &blk)
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  def self.is_activejob_job?(msg)
         | 
| 77 | 
            +
                    return false unless defined?(::ActiveJob)
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    msg['class'] == 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper' && (msg['wrapped'].to_s).constantize < Compat::ActiveJob::UniqueJobExtension
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  def self.validate_middleware_order(chain, order)
         | 
| 83 | 
            +
                    chain_classes = chain.entries.map(&:klass)
         | 
| 84 | 
            +
                    filtered = chain_classes.select { |klass| order.include?(klass) }
         | 
| 85 | 
            +
                    raise "Middleware chain does not contain all required middleware: #{order - filtered}" unless order.all? { |klass| filtered.include?(klass) }
         | 
| 86 | 
            +
                    raise "Middleware must be in order: #{order.inspect}" if filtered != order
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  def self.sidekiq_middleware(placement, &blk)
         | 
| 90 | 
            +
                    install_middleware = ->(config) do
         | 
| 91 | 
            +
                      config.send("#{placement}_middleware") do |chain|
         | 
| 92 | 
            +
                        blk.call(chain)
         | 
| 93 | 
            +
                      end
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                    ::Sidekiq.configure_client(&install_middleware) if placement == :client
         | 
| 97 | 
            +
                    ::Sidekiq.configure_server(&install_middleware)
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  def self.validate_middleware_placement!
         | 
| 101 | 
            +
                    sidekiq_middleware(:client) do |chain|
         | 
| 102 | 
            +
                      # Unique middleware must come _before_ the Batch middleware so that the uniqueness middleware can wrap the job in a batch
         | 
| 103 | 
            +
                      validate_middleware_order(chain, [
         | 
| 104 | 
            +
                        Joblin::Uniqueness::Compat::Sidekiq::ClientMiddleware,
         | 
| 105 | 
            +
                        Joblin::Batching::Compat::Sidekiq::ClientMiddleware,
         | 
| 106 | 
            +
                      ])
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    sidekiq_middleware(:server) do |chain|
         | 
| 110 | 
            +
                      # Unique middleware must com _after_ the Batch middleware so that the Batch is loaded before reaching the uniqueness middleware
         | 
| 111 | 
            +
                      validate_middleware_order(chain, [
         | 
| 112 | 
            +
                        Joblin::Batching::Compat::Sidekiq::ServerMiddleware,
         | 
| 113 | 
            +
                        Joblin::Uniqueness::Compat::Sidekiq::ServerMiddleware,
         | 
| 114 | 
            +
                      ])
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                  def self.configure
         | 
| 119 | 
            +
                    sidekiq_middleware(:client) do |chain|
         | 
| 120 | 
            +
                      chain.insert_before Joblin::Batching::Compat::Sidekiq::ClientMiddleware, Compat::Sidekiq::ClientMiddleware
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    sidekiq_middleware(:server) do |chain|
         | 
| 124 | 
            +
                      chain.insert_after Joblin::Batching::Compat::Sidekiq::ServerMiddleware, Compat::Sidekiq::ServerMiddleware
         | 
| 125 | 
            +
                    end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                    ::Sidekiq::Worker.extend(ActiveSupport::Concern) unless ::Sidekiq::Worker < ActiveSupport::Concern
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                    ::Sidekiq::Worker.send(:include, Compat::Sidekiq::WorkerExtension)
         | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
              end
         | 
| 133 | 
            +
            end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
            # require_relative 'sidekiq/web'
         |