inst-jobs 2.0.0 → 3.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 +4 -4
- data/db/migrate/20101216224513_create_delayed_jobs.rb +9 -7
- data/db/migrate/20110531144916_cleanup_delayed_jobs_indexes.rb +8 -13
- data/db/migrate/20110610213249_optimize_delayed_jobs.rb +8 -8
- data/db/migrate/20110831210257_add_delayed_jobs_next_in_strand.rb +25 -25
- data/db/migrate/20120510004759_delayed_jobs_delete_trigger_lock_for_update.rb +4 -8
- data/db/migrate/20120531150712_drop_psql_jobs_pop_fn.rb +1 -3
- data/db/migrate/20120607164022_delayed_jobs_use_advisory_locks.rb +11 -15
- data/db/migrate/20120607181141_index_jobs_on_locked_by.rb +1 -1
- data/db/migrate/20120608191051_add_jobs_run_at_index.rb +2 -2
- data/db/migrate/20120927184213_change_delayed_jobs_handler_to_text.rb +1 -1
- data/db/migrate/20140505215510_copy_failed_jobs_original_id.rb +2 -3
- data/db/migrate/20150807133223_add_max_concurrent_to_jobs.rb +9 -13
- data/db/migrate/20151210162949_improve_max_concurrent.rb +4 -8
- data/db/migrate/20161206323555_add_back_default_string_limits_jobs.rb +3 -2
- data/db/migrate/20181217155351_speed_up_max_concurrent_triggers.rb +13 -17
- data/db/migrate/20200330230722_add_id_to_get_delayed_jobs_index.rb +8 -8
- data/db/migrate/20200824222232_speed_up_max_concurrent_delete_trigger.rb +72 -77
- data/db/migrate/20200825011002_add_strand_order_override.rb +93 -97
- data/db/migrate/20210809145804_add_n_strand_index.rb +12 -0
- data/db/migrate/20210812210128_add_singleton_column.rb +200 -0
- data/db/migrate/20210917232626_add_delete_conflicting_singletons_before_unlock_trigger.rb +27 -0
- data/db/migrate/20210928174754_fix_singleton_condition_in_before_insert.rb +56 -0
- data/db/migrate/20210929204903_update_conflicting_singleton_function_to_use_index.rb +27 -0
- data/db/migrate/20211101190934_update_after_delete_trigger_for_singleton_index.rb +137 -0
- data/db/migrate/20211207094200_update_after_delete_trigger_for_singleton_transition_cases.rb +171 -0
- data/db/migrate/20211220112800_fix_singleton_race_condition_insert.rb +59 -0
- data/db/migrate/20211220113000_fix_singleton_race_condition_delete.rb +207 -0
- data/db/migrate/20220127091200_fix_singleton_unique_constraint.rb +31 -0
- data/db/migrate/20220128084800_update_insert_trigger_for_singleton_unique_constraint_change.rb +60 -0
- data/db/migrate/20220128084900_update_delete_trigger_for_singleton_unique_constraint_change.rb +209 -0
- data/db/migrate/20220203063200_remove_old_singleton_index.rb +31 -0
- data/db/migrate/20220328152900_add_failed_jobs_indicies.rb +12 -0
- data/exe/inst_jobs +3 -2
- data/lib/delayed/backend/active_record.rb +226 -168
- data/lib/delayed/backend/base.rb +119 -72
- data/lib/delayed/batch.rb +11 -9
- data/lib/delayed/cli.rb +98 -84
- data/lib/delayed/core_ext/kernel.rb +4 -2
- data/lib/delayed/daemon.rb +70 -74
- data/lib/delayed/job_tracking.rb +26 -25
- data/lib/delayed/lifecycle.rb +28 -23
- data/lib/delayed/log_tailer.rb +17 -17
- data/lib/delayed/logging.rb +13 -16
- data/lib/delayed/message_sending.rb +43 -52
- data/lib/delayed/performable_method.rb +6 -8
- data/lib/delayed/periodic.rb +72 -68
- data/lib/delayed/plugin.rb +2 -4
- data/lib/delayed/pool.rb +205 -168
- data/lib/delayed/rails_reloader_plugin.rb +30 -0
- data/lib/delayed/server/helpers.rb +6 -6
- data/lib/delayed/server.rb +51 -54
- data/lib/delayed/settings.rb +96 -81
- data/lib/delayed/testing.rb +21 -22
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed/work_queue/in_process.rb +21 -17
- data/lib/delayed/work_queue/parent_process/client.rb +55 -53
- data/lib/delayed/work_queue/parent_process/server.rb +245 -207
- data/lib/delayed/work_queue/parent_process.rb +52 -53
- data/lib/delayed/worker/consul_health_check.rb +32 -33
- data/lib/delayed/worker/health_check.rb +35 -27
- data/lib/delayed/worker/null_health_check.rb +3 -1
- data/lib/delayed/worker/process_helper.rb +11 -12
- data/lib/delayed/worker.rb +257 -244
- data/lib/delayed/yaml_extensions.rb +12 -10
- data/lib/delayed_job.rb +37 -37
- data/lib/inst-jobs.rb +1 -1
- data/spec/active_record_job_spec.rb +152 -139
- data/spec/delayed/cli_spec.rb +7 -7
- data/spec/delayed/daemon_spec.rb +10 -9
- data/spec/delayed/message_sending_spec.rb +16 -9
- data/spec/delayed/periodic_spec.rb +14 -21
- data/spec/delayed/server_spec.rb +38 -38
- data/spec/delayed/settings_spec.rb +26 -25
- data/spec/delayed/work_queue/in_process_spec.rb +8 -9
- data/spec/delayed/work_queue/parent_process/client_spec.rb +17 -12
- data/spec/delayed/work_queue/parent_process/server_spec.rb +118 -42
- data/spec/delayed/work_queue/parent_process_spec.rb +21 -23
- data/spec/delayed/worker/consul_health_check_spec.rb +37 -50
- data/spec/delayed/worker/health_check_spec.rb +60 -52
- data/spec/delayed/worker_spec.rb +53 -24
- data/spec/sample_jobs.rb +45 -15
- data/spec/shared/delayed_batch.rb +74 -67
- data/spec/shared/delayed_method.rb +143 -102
- data/spec/shared/performable_method.rb +39 -38
- data/spec/shared/shared_backend.rb +801 -440
- data/spec/shared/testing.rb +14 -14
- data/spec/shared/worker.rb +157 -149
- data/spec/shared_jobs_specs.rb +13 -13
- data/spec/spec_helper.rb +57 -56
- metadata +183 -103
- data/lib/delayed/backend/redis/bulk_update.lua +0 -50
- data/lib/delayed/backend/redis/destroy_job.lua +0 -2
- data/lib/delayed/backend/redis/enqueue.lua +0 -29
- data/lib/delayed/backend/redis/fail_job.lua +0 -5
- data/lib/delayed/backend/redis/find_available.lua +0 -3
- data/lib/delayed/backend/redis/functions.rb +0 -59
- data/lib/delayed/backend/redis/get_and_lock_next_available.lua +0 -17
- data/lib/delayed/backend/redis/includes/jobs_common.lua +0 -203
- data/lib/delayed/backend/redis/job.rb +0 -535
- data/lib/delayed/backend/redis/set_running.lua +0 -5
- data/lib/delayed/backend/redis/tickle_strand.lua +0 -2
- data/spec/gemfiles/42.gemfile +0 -7
- data/spec/gemfiles/50.gemfile +0 -7
- data/spec/gemfiles/51.gemfile +0 -7
- data/spec/gemfiles/52.gemfile +0 -7
- data/spec/gemfiles/60.gemfile +0 -7
- data/spec/redis_job_spec.rb +0 -148
    
        data/lib/delayed/backend/base.rb
    CHANGED
    
    | @@ -12,7 +12,8 @@ module Delayed | |
| 12 12 | 
             
                end
         | 
| 13 13 |  | 
| 14 14 | 
             
                module Base
         | 
| 15 | 
            -
                   | 
| 15 | 
            +
                  ON_HOLD_BLOCKER = "blocker job"
         | 
| 16 | 
            +
                  ON_HOLD_LOCKED_BY = "on hold"
         | 
| 16 17 | 
             
                  ON_HOLD_COUNT = 50
         | 
| 17 18 |  | 
| 18 19 | 
             
                  def self.included(base)
         | 
| @@ -22,9 +23,7 @@ module Delayed | |
| 22 23 | 
             
                  end
         | 
| 23 24 |  | 
| 24 25 | 
             
                  module ClassMethods
         | 
| 25 | 
            -
                    attr_accessor :batches
         | 
| 26 | 
            -
                    attr_accessor :batch_enqueue_args
         | 
| 27 | 
            -
                    attr_accessor :default_priority
         | 
| 26 | 
            +
                    attr_accessor :batches, :batch_enqueue_args, :default_priority
         | 
| 28 27 |  | 
| 29 28 | 
             
                    # Add a job to the queue
         | 
| 30 29 | 
             
                    # The first argument should be an object that respond_to?(:perform)
         | 
| @@ -32,29 +31,37 @@ module Delayed | |
| 32 31 | 
             
                    # :priority, :run_at, :queue, :strand, :singleton
         | 
| 33 32 | 
             
                    # Example: Delayed::Job.enqueue(object, priority: 0, run_at: time, queue: queue)
         | 
| 34 33 | 
             
                    def enqueue(object,
         | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 34 | 
            +
                                priority: default_priority,
         | 
| 35 | 
            +
                                run_at: nil,
         | 
| 36 | 
            +
                                expires_at: nil,
         | 
| 37 | 
            +
                                queue: Delayed::Settings.queue,
         | 
| 38 | 
            +
                                strand: nil,
         | 
| 39 | 
            +
                                singleton: nil,
         | 
| 40 | 
            +
                                n_strand: nil,
         | 
| 41 | 
            +
                                max_attempts: Delayed::Settings.max_attempts,
         | 
| 42 | 
            +
                                **kwargs)
         | 
| 44 43 |  | 
| 45 44 | 
             
                      unless object.respond_to?(:perform)
         | 
| 46 | 
            -
                        raise ArgumentError,  | 
| 45 | 
            +
                        raise ArgumentError, "Cannot enqueue items which do not respond to perform"
         | 
| 47 46 | 
             
                      end
         | 
| 48 47 |  | 
| 48 | 
            +
                      strand ||= singleton if Settings.infer_strand_from_singleton
         | 
| 49 | 
            +
             | 
| 49 50 | 
             
                      kwargs = Settings.default_job_options.merge(kwargs)
         | 
| 50 51 | 
             
                      kwargs[:payload_object] = object
         | 
| 51 52 | 
             
                      kwargs[:priority] = priority
         | 
| 52 53 | 
             
                      kwargs[:run_at] = run_at if run_at
         | 
| 53 54 | 
             
                      kwargs[:strand] = strand
         | 
| 54 55 | 
             
                      kwargs[:max_attempts] = max_attempts
         | 
| 55 | 
            -
                       | 
| 56 | 
            +
                      if defined?(Marginalia) && Marginalia::Comment.components
         | 
| 57 | 
            +
                        kwargs[:source] =
         | 
| 58 | 
            +
                          Marginalia::Comment.construct_comment
         | 
| 59 | 
            +
                      end
         | 
| 56 60 | 
             
                      kwargs[:expires_at] = expires_at
         | 
| 57 61 | 
             
                      kwargs[:queue] = queue
         | 
| 62 | 
            +
                      kwargs[:singleton] = singleton
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                      raise ArgumentError, "Only one of strand or n_strand can be used" if strand && n_strand
         | 
| 58 65 |  | 
| 59 66 | 
             
                      # If two parameters are given to n_strand, the first param is used
         | 
| 60 67 | 
             
                      # as the strand name for looking up the Setting, while the second
         | 
| @@ -79,15 +86,22 @@ module Delayed | |
| 79 86 | 
             
                        kwargs.merge!(n_strand_options(full_strand_name, num_strands))
         | 
| 80 87 | 
             
                      end
         | 
| 81 88 |  | 
| 89 | 
            +
                      job = nil
         | 
| 90 | 
            +
             | 
| 82 91 | 
             
                      if singleton
         | 
| 83 | 
            -
                         | 
| 84 | 
            -
             | 
| 92 | 
            +
                        Delayed::Worker.lifecycle.run_callbacks(:create, kwargs) do
         | 
| 93 | 
            +
                          job = create(**kwargs)
         | 
| 94 | 
            +
                        end
         | 
| 85 95 | 
             
                      elsif batches && strand.nil? && run_at.nil?
         | 
| 86 96 | 
             
                        batch_enqueue_args = kwargs.slice(*self.batch_enqueue_args)
         | 
| 87 97 | 
             
                        batches[batch_enqueue_args] << kwargs
         | 
| 88 98 | 
             
                        return true
         | 
| 89 99 | 
             
                      else
         | 
| 90 | 
            -
                         | 
| 100 | 
            +
                        raise ArgumentError, "on_conflict can only be provided with singleton" if kwargs[:on_conflict]
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                        Delayed::Worker.lifecycle.run_callbacks(:create, kwargs) do
         | 
| 103 | 
            +
                          job = create(**kwargs)
         | 
| 104 | 
            +
                        end
         | 
| 91 105 | 
             
                      end
         | 
| 92 106 |  | 
| 93 107 | 
             
                      JobTracking.job_created(job)
         | 
| @@ -118,10 +132,10 @@ module Delayed | |
| 118 132 |  | 
| 119 133 | 
             
                    def check_priorities(min_priority, max_priority)
         | 
| 120 134 | 
             
                      if min_priority && min_priority < Delayed::MIN_PRIORITY
         | 
| 121 | 
            -
                        raise | 
| 135 | 
            +
                        raise ArgumentError, "min_priority #{min_priority} can't be less than #{Delayed::MIN_PRIORITY}"
         | 
| 122 136 | 
             
                      end
         | 
| 123 | 
            -
                      if max_priority && max_priority > Delayed::MAX_PRIORITY
         | 
| 124 | 
            -
                        raise | 
| 137 | 
            +
                      if max_priority && max_priority > Delayed::MAX_PRIORITY # rubocop:disable Style/GuardClause
         | 
| 138 | 
            +
                        raise ArgumentError, "max_priority #{max_priority} can't be greater than #{Delayed::MAX_PRIORITY}"
         | 
| 125 139 | 
             
                      end
         | 
| 126 140 | 
             
                    end
         | 
| 127 141 |  | 
| @@ -134,13 +148,19 @@ module Delayed | |
| 134 148 |  | 
| 135 149 | 
             
                    def processes_locked_locally(name: nil)
         | 
| 136 150 | 
             
                      name ||= Socket.gethostname rescue x
         | 
| 137 | 
            -
                      running_jobs.select | 
| 151 | 
            +
                      local_jobs = running_jobs.select do |job|
         | 
| 152 | 
            +
                        job.locked_by.start_with?("#{name}:")
         | 
| 153 | 
            +
                      end
         | 
| 154 | 
            +
                      local_jobs.map { |job| job.locked_by.split(":").last.to_i }
         | 
| 138 155 | 
             
                    end
         | 
| 139 156 |  | 
| 140 157 | 
             
                    def unlock_orphaned_prefetched_jobs
         | 
| 141 | 
            -
                      horizon = db_time_now - Settings.parent_process[:prefetched_jobs_timeout] * 4
         | 
| 142 | 
            -
                      orphaned_jobs = running_jobs.select  | 
| 158 | 
            +
                      horizon = db_time_now - (Settings.parent_process[:prefetched_jobs_timeout] * 4)
         | 
| 159 | 
            +
                      orphaned_jobs = running_jobs.select do |job|
         | 
| 160 | 
            +
                        job.locked_by.start_with?("prefetch:") && job.locked_at < horizon
         | 
| 161 | 
            +
                      end
         | 
| 143 162 | 
             
                      return 0 if orphaned_jobs.empty?
         | 
| 163 | 
            +
             | 
| 144 164 | 
             
                      unlock(orphaned_jobs)
         | 
| 145 165 | 
             
                    end
         | 
| 146 166 |  | 
| @@ -153,16 +173,38 @@ module Delayed | |
| 153 173 | 
             
                      pid_regex = pid || '(\d+)'
         | 
| 154 174 | 
             
                      regex = Regexp.new("^#{Regexp.escape(name)}:#{pid_regex}$")
         | 
| 155 175 | 
             
                      unlocked_jobs = 0
         | 
| 176 | 
            +
                      escaped_name = name.gsub("\\", "\\\\")
         | 
| 177 | 
            +
                                         .gsub("%", "\\%")
         | 
| 178 | 
            +
                                         .gsub("_", "\\_")
         | 
| 179 | 
            +
                      locked_by_like = "#{escaped_name}:%"
         | 
| 156 180 | 
             
                      running = false if pid
         | 
| 157 | 
            -
                       | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
             | 
| 161 | 
            -
             | 
| 162 | 
            -
                         | 
| 163 | 
            -
                         | 
| 164 | 
            -
             | 
| 165 | 
            -
             | 
| 181 | 
            +
                      jobs = running_jobs.limit(100)
         | 
| 182 | 
            +
                      jobs = pid ? jobs.where(locked_by: "#{name}:#{pid}") : jobs.where("locked_by LIKE ?", locked_by_like)
         | 
| 183 | 
            +
                      ignores = []
         | 
| 184 | 
            +
                      loop do
         | 
| 185 | 
            +
                        batch_scope = ignores.empty? ? jobs : jobs.where.not(id: ignores)
         | 
| 186 | 
            +
                        # if we don't reload this it's possible to keep getting the
         | 
| 187 | 
            +
                        # same array each loop even after the jobs have been deleted.
         | 
| 188 | 
            +
                        batch = batch_scope.reload.to_a
         | 
| 189 | 
            +
                        break if batch.empty?
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                        batch.each do |job|
         | 
| 192 | 
            +
                          unless job.locked_by =~ regex
         | 
| 193 | 
            +
                            ignores << job.id
         | 
| 194 | 
            +
                            next
         | 
| 195 | 
            +
                          end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                          unless pid
         | 
| 198 | 
            +
                            job_pid = $1.to_i
         | 
| 199 | 
            +
                            running = Process.kill(0, job_pid) rescue false
         | 
| 200 | 
            +
                          end
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                          if running
         | 
| 203 | 
            +
                            ignores << job.id
         | 
| 204 | 
            +
                          else
         | 
| 205 | 
            +
                            unlocked_jobs += 1
         | 
| 206 | 
            +
                            job.reschedule("process died")
         | 
| 207 | 
            +
                          end
         | 
| 166 208 | 
             
                        end
         | 
| 167 209 | 
             
                      end
         | 
| 168 210 | 
             
                      unlocked_jobs
         | 
| @@ -172,33 +214,37 @@ module Delayed | |
| 172 214 | 
             
                  def failed?
         | 
| 173 215 | 
             
                    failed_at
         | 
| 174 216 | 
             
                  end
         | 
| 175 | 
            -
                   | 
| 217 | 
            +
                  alias failed failed?
         | 
| 176 218 |  | 
| 177 219 | 
             
                  def expired?
         | 
| 178 220 | 
             
                    expires_at && (self.class.db_time_now >= expires_at)
         | 
| 179 221 | 
             
                  end
         | 
| 180 222 |  | 
| 223 | 
            +
                  def inferred_max_attempts
         | 
| 224 | 
            +
                    max_attempts || Delayed::Settings.max_attempts
         | 
| 225 | 
            +
                  end
         | 
| 226 | 
            +
             | 
| 181 227 | 
             
                  # Reschedule the job in the future (when a job fails).
         | 
| 182 228 | 
             
                  # Uses an exponential scale depending on the number of failed attempts.
         | 
| 183 229 | 
             
                  def reschedule(error = nil, time = nil)
         | 
| 184 230 | 
             
                    begin
         | 
| 185 231 | 
             
                      obj = payload_object
         | 
| 186 | 
            -
                      return_code = obj.on_failure(error) if obj | 
| 232 | 
            +
                      return_code = obj.on_failure(error) if obj.respond_to?(:on_failure)
         | 
| 187 233 | 
             
                    rescue
         | 
| 188 234 | 
             
                      # don't allow a failed deserialization to prevent rescheduling
         | 
| 189 235 | 
             
                    end
         | 
| 190 236 |  | 
| 191 237 | 
             
                    self.attempts += 1 unless return_code == :unlock
         | 
| 192 238 |  | 
| 193 | 
            -
                    if self.attempts >=  | 
| 239 | 
            +
                    if self.attempts >= inferred_max_attempts
         | 
| 194 240 | 
             
                      permanent_failure error || "max attempts reached"
         | 
| 195 241 | 
             
                    elsif expired?
         | 
| 196 242 | 
             
                      permanent_failure error || "job has expired"
         | 
| 197 243 | 
             
                    else
         | 
| 198 | 
            -
                      time ||=  | 
| 244 | 
            +
                      time ||= reschedule_at
         | 
| 199 245 | 
             
                      self.run_at = time
         | 
| 200 | 
            -
                       | 
| 201 | 
            -
                       | 
| 246 | 
            +
                      unlock
         | 
| 247 | 
            +
                      save!
         | 
| 202 248 | 
             
                    end
         | 
| 203 249 | 
             
                  end
         | 
| 204 250 |  | 
| @@ -206,26 +252,24 @@ module Delayed | |
| 206 252 | 
             
                    begin
         | 
| 207 253 | 
             
                      # notify the payload_object of a permanent failure
         | 
| 208 254 | 
             
                      obj = payload_object
         | 
| 209 | 
            -
                      obj.on_permanent_failure(error) if obj | 
| 255 | 
            +
                      obj.on_permanent_failure(error) if obj.respond_to?(:on_permanent_failure)
         | 
| 210 256 | 
             
                    rescue
         | 
| 211 257 | 
             
                      # don't allow a failed deserialization to prevent destroying the job
         | 
| 212 258 | 
             
                    end
         | 
| 213 259 |  | 
| 214 260 | 
             
                    # optionally destroy the object
         | 
| 215 261 | 
             
                    destroy_self = true
         | 
| 216 | 
            -
                    if Delayed::Worker.on_max_failures
         | 
| 217 | 
            -
                      destroy_self = Delayed::Worker.on_max_failures.call(self, error)
         | 
| 218 | 
            -
                    end
         | 
| 262 | 
            +
                    destroy_self = Delayed::Worker.on_max_failures.call(self, error) if Delayed::Worker.on_max_failures
         | 
| 219 263 |  | 
| 220 264 | 
             
                    if destroy_self
         | 
| 221 | 
            -
                       | 
| 265 | 
            +
                      destroy
         | 
| 222 266 | 
             
                    else
         | 
| 223 | 
            -
                       | 
| 267 | 
            +
                      fail!
         | 
| 224 268 | 
             
                    end
         | 
| 225 269 | 
             
                  end
         | 
| 226 270 |  | 
| 227 271 | 
             
                  def payload_object
         | 
| 228 | 
            -
                    @payload_object ||= deserialize(self[ | 
| 272 | 
            +
                    @payload_object ||= deserialize(self["handler"].untaint)
         | 
| 229 273 | 
             
                  end
         | 
| 230 274 |  | 
| 231 275 | 
             
                  def name
         | 
| @@ -241,7 +285,7 @@ module Delayed | |
| 241 285 |  | 
| 242 286 | 
             
                  def full_name
         | 
| 243 287 | 
             
                    obj = payload_object rescue nil
         | 
| 244 | 
            -
                    if obj | 
| 288 | 
            +
                    if obj.respond_to?(:full_name)
         | 
| 245 289 | 
             
                      obj.full_name
         | 
| 246 290 | 
             
                    else
         | 
| 247 291 | 
             
                      name
         | 
| @@ -250,14 +294,14 @@ module Delayed | |
| 250 294 |  | 
| 251 295 | 
             
                  def payload_object=(object)
         | 
| 252 296 | 
             
                    @payload_object = object
         | 
| 253 | 
            -
                    self[ | 
| 254 | 
            -
                    self[ | 
| 255 | 
            -
             | 
| 256 | 
            -
             | 
| 257 | 
            -
             | 
| 258 | 
            -
             | 
| 259 | 
            -
             | 
| 260 | 
            -
             | 
| 297 | 
            +
                    self["handler"] = object.to_yaml
         | 
| 298 | 
            +
                    self["tag"] = if object.respond_to?(:tag)
         | 
| 299 | 
            +
                                    object.tag
         | 
| 300 | 
            +
                                  elsif object.is_a?(Module)
         | 
| 301 | 
            +
                                    "#{object}.perform"
         | 
| 302 | 
            +
                                  else
         | 
| 303 | 
            +
                                    "#{object.class}#perform"
         | 
| 304 | 
            +
                                  end
         | 
| 261 305 | 
             
                  end
         | 
| 262 306 |  | 
| 263 307 | 
             
                  # Moved into its own method so that new_relic can trace it.
         | 
| @@ -284,15 +328,16 @@ module Delayed | |
| 284 328 | 
             
                  end
         | 
| 285 329 |  | 
| 286 330 | 
             
                  def locked?
         | 
| 287 | 
            -
                    !!( | 
| 331 | 
            +
                    !!(locked_at || locked_by)
         | 
| 288 332 | 
             
                  end
         | 
| 289 333 |  | 
| 290 334 | 
             
                  def reschedule_at
         | 
| 291 | 
            -
                    new_time = self.class.db_time_now + (attempts | 
| 335 | 
            +
                    new_time = self.class.db_time_now + (attempts**4) + 5
         | 
| 292 336 | 
             
                    begin
         | 
| 293 337 | 
             
                      if payload_object.respond_to?(:reschedule_at)
         | 
| 294 338 | 
             
                        new_time = payload_object.reschedule_at(
         | 
| 295 | 
            -
             | 
| 339 | 
            +
                          self.class.db_time_now, attempts
         | 
| 340 | 
            +
                        )
         | 
| 296 341 | 
             
                      end
         | 
| 297 342 | 
             
                    rescue
         | 
| 298 343 | 
             
                      # TODO: just swallow errors from reschedule_at ?
         | 
| @@ -304,25 +349,26 @@ module Delayed | |
| 304 349 | 
             
                    self.locked_by = ON_HOLD_LOCKED_BY
         | 
| 305 350 | 
             
                    self.locked_at = self.class.db_time_now
         | 
| 306 351 | 
             
                    self.attempts = ON_HOLD_COUNT
         | 
| 307 | 
            -
                     | 
| 352 | 
            +
                    save!
         | 
| 308 353 | 
             
                  end
         | 
| 309 354 |  | 
| 310 355 | 
             
                  def unhold!
         | 
| 311 356 | 
             
                    self.locked_by = nil
         | 
| 312 357 | 
             
                    self.locked_at = nil
         | 
| 313 358 | 
             
                    self.attempts = 0
         | 
| 314 | 
            -
                    self.run_at = [self.class.db_time_now,  | 
| 359 | 
            +
                    self.run_at = [self.class.db_time_now, run_at].max
         | 
| 315 360 | 
             
                    self.failed_at = nil
         | 
| 316 | 
            -
                     | 
| 361 | 
            +
                    save!
         | 
| 317 362 | 
             
                  end
         | 
| 318 363 |  | 
| 319 364 | 
             
                  def on_hold?
         | 
| 320 | 
            -
                     | 
| 365 | 
            +
                    locked_by == "on hold" && locked_at && self.attempts == ON_HOLD_COUNT
         | 
| 321 366 | 
             
                  end
         | 
| 322 367 |  | 
| 323 | 
            -
             | 
| 368 | 
            +
                  private
         | 
| 324 369 |  | 
| 325 | 
            -
                   | 
| 370 | 
            +
                  PARSE_OBJECT_FROM_YAML = %r{!ruby/\w+:([^\s]+)}.freeze
         | 
| 371 | 
            +
                  private_constant :PARSE_OBJECT_FROM_YAML
         | 
| 326 372 |  | 
| 327 373 | 
             
                  def deserialize(source)
         | 
| 328 374 | 
             
                    handler = nil
         | 
| @@ -336,13 +382,13 @@ module Delayed | |
| 336 382 | 
             
                    return handler if handler.respond_to?(:perform)
         | 
| 337 383 |  | 
| 338 384 | 
             
                    raise DeserializationError,
         | 
| 339 | 
            -
             | 
| 385 | 
            +
                          "Job failed to load: Unknown handler. Try to manually require the appropriate file."
         | 
| 340 386 | 
             
                  rescue TypeError, LoadError, NameError => e
         | 
| 341 387 | 
             
                    raise DeserializationError,
         | 
| 342 | 
            -
             | 
| 388 | 
            +
                          "Job failed to load: #{e.message}. Try to manually require the required file."
         | 
| 343 389 | 
             
                  rescue Psych::SyntaxError => e
         | 
| 344 | 
            -
             | 
| 345 | 
            -
             | 
| 390 | 
            +
                    raise DeserializationError,
         | 
| 391 | 
            +
                          "YAML parsing error: #{e.message}. Probably not recoverable."
         | 
| 346 392 | 
             
                  end
         | 
| 347 393 |  | 
| 348 394 | 
             
                  def _yaml_deserialize(source)
         | 
| @@ -350,12 +396,13 @@ module Delayed | |
| 350 396 | 
             
                  end
         | 
| 351 397 |  | 
| 352 398 | 
             
                  def attempt_to_load_from_source(source)
         | 
| 353 | 
            -
                     | 
| 354 | 
            -
             | 
| 355 | 
            -
                     | 
| 399 | 
            +
                    return unless (md = PARSE_OBJECT_FROM_YAML.match(source))
         | 
| 400 | 
            +
             | 
| 401 | 
            +
                    md[1].constantize
         | 
| 356 402 | 
             
                  end
         | 
| 357 403 |  | 
| 358 | 
            -
             | 
| 404 | 
            +
                  public
         | 
| 405 | 
            +
             | 
| 359 406 | 
             
                  def initialize_defaults
         | 
| 360 407 | 
             
                    self.queue ||= Delayed::Settings.queue
         | 
| 361 408 | 
             
                    self.run_at ||= self.class.db_time_now
         | 
    
        data/lib/delayed/batch.rb
    CHANGED
    
    | @@ -2,11 +2,11 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Delayed
         | 
| 4 4 | 
             
              module Batch
         | 
| 5 | 
            -
                 | 
| 5 | 
            +
                PerformableBatch = Struct.new(:mode, :items) do
         | 
| 6 6 | 
             
                  def initialize(mode, items)
         | 
| 7 7 | 
             
                    raise "unsupported mode" unless mode == :serial
         | 
| 8 | 
            -
             | 
| 9 | 
            -
                     | 
| 8 | 
            +
             | 
| 9 | 
            +
                    super
         | 
| 10 10 | 
             
                  end
         | 
| 11 11 |  | 
| 12 12 | 
             
                  def display_name
         | 
| @@ -25,14 +25,16 @@ module Delayed | |
| 25 25 | 
             
                end
         | 
| 26 26 |  | 
| 27 27 | 
             
                class << self
         | 
| 28 | 
            -
                  def serial_batch(opts = {})
         | 
| 29 | 
            -
                    prepare_batches(:serial, opts) | 
| 28 | 
            +
                  def serial_batch(opts = {}, &block)
         | 
| 29 | 
            +
                    prepare_batches(:serial, opts, &block)
         | 
| 30 30 | 
             
                  end
         | 
| 31 31 |  | 
| 32 32 | 
             
                  private
         | 
| 33 | 
            +
             | 
| 33 34 | 
             
                  def prepare_batches(mode, opts)
         | 
| 34 35 | 
             
                    raise "nested batching is not supported" if Delayed::Job.batches
         | 
| 35 | 
            -
             | 
| 36 | 
            +
             | 
| 37 | 
            +
                    Delayed::Job.batches = Hash.new { |h, k| h[k] = Set.new }
         | 
| 36 38 | 
             
                    batch_enqueue_args = [:queue]
         | 
| 37 39 | 
             
                    batch_enqueue_args << :priority unless opts[:priority]
         | 
| 38 40 | 
             
                    Delayed::Job.batch_enqueue_args = batch_enqueue_args
         | 
| @@ -42,9 +44,9 @@ module Delayed | |
| 42 44 | 
             
                    Delayed::Job.batches = nil
         | 
| 43 45 | 
             
                    batch_args = opts.slice(:priority)
         | 
| 44 46 | 
             
                    batches.each do |enqueue_args, batch|
         | 
| 45 | 
            -
                      if batch.size | 
| 46 | 
            -
             | 
| 47 | 
            -
                       | 
| 47 | 
            +
                      next if batch.size.zero?
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                      if batch.size == 1
         | 
| 48 50 | 
             
                        args = batch.first.merge(batch_args)
         | 
| 49 51 | 
             
                        payload_object = args.delete(:payload_object)
         | 
| 50 52 | 
             
                        Delayed::Job.enqueue(payload_object, **args)
         | 
    
        data/lib/delayed/cli.rb
    CHANGED
    
    | @@ -1,111 +1,125 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require  | 
| 3 | 
            +
            require "optparse"
         | 
| 4 4 |  | 
| 5 5 | 
             
            module Delayed
         | 
| 6 | 
            -
            class CLI
         | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 6 | 
            +
              class CLI
         | 
| 7 | 
            +
                class << self
         | 
| 8 | 
            +
                  attr_accessor :instance
         | 
| 9 | 
            +
                end
         | 
| 10 10 |  | 
| 11 | 
            -
             | 
| 11 | 
            +
                attr_reader :config
         | 
| 12 12 |  | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 13 | 
            +
                def initialize(args = ARGV)
         | 
| 14 | 
            +
                  self.class.instance = self
         | 
| 15 15 |  | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 16 | 
            +
                  @args = args
         | 
| 17 | 
            +
                  # config that will be applied on Settings and passed to the created Pool
         | 
| 18 | 
            +
                  @config = {}
         | 
| 19 | 
            +
                  # CLI options that will be kept to this class
         | 
| 20 | 
            +
                  @options = {
         | 
| 21 | 
            +
                    config_file: Settings.default_worker_config_name,
         | 
| 22 | 
            +
                    pid_folder: Settings.expand_rails_path("tmp/pids"),
         | 
| 23 | 
            +
                    tail_logs: true # only in FG mode
         | 
| 24 | 
            +
                  }
         | 
| 25 | 
            +
                end
         | 
| 26 26 |  | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 27 | 
            +
                def run
         | 
| 28 | 
            +
                  parse_cli_options!
         | 
| 29 | 
            +
                  load_and_apply_config!
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  command = @args.shift
         | 
| 32 | 
            +
                  case command
         | 
| 33 | 
            +
                  when "start"
         | 
| 34 | 
            +
                    exit 1 if daemon.status(print: :alive) == :running
         | 
| 35 | 
            +
                    daemon.daemonize!
         | 
| 36 | 
            +
                    start
         | 
| 37 | 
            +
                  when "stop"
         | 
| 38 | 
            +
                    daemon.stop(kill: @options[:kill])
         | 
| 39 | 
            +
                  when "run"
         | 
| 40 | 
            +
                    start
         | 
| 41 | 
            +
                  when "status"
         | 
| 42 | 
            +
                    if daemon.status
         | 
| 43 | 
            +
                      exit 0
         | 
| 44 | 
            +
                    else
         | 
| 45 | 
            +
                      exit 1
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  when "restart"
         | 
| 48 | 
            +
                    daemon.stop(kill: @options[:kill])
         | 
| 49 | 
            +
                    daemon.daemonize!
         | 
| 50 | 
            +
                    start
         | 
| 51 | 
            +
                  when nil
         | 
| 52 | 
            +
                    puts option_parser.to_s
         | 
| 44 53 | 
             
                  else
         | 
| 45 | 
            -
                     | 
| 54 | 
            +
                    raise("Unknown command: #{command.inspect}")
         | 
| 46 55 | 
             
                  end
         | 
| 47 | 
            -
                when 'restart'
         | 
| 48 | 
            -
                  daemon.stop(kill: @options[:kill])
         | 
| 49 | 
            -
                  daemon.daemonize!
         | 
| 50 | 
            -
                  start
         | 
| 51 | 
            -
                when nil
         | 
| 52 | 
            -
                  puts option_parser.to_s
         | 
| 53 | 
            -
                else
         | 
| 54 | 
            -
                  raise("Unknown command: #{command.inspect}")
         | 
| 55 56 | 
             
                end
         | 
| 56 | 
            -
              end
         | 
| 57 57 |  | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 58 | 
            +
                def parse_cli_options!
         | 
| 59 | 
            +
                  option_parser.parse!(@args)
         | 
| 60 | 
            +
                  @options
         | 
| 61 | 
            +
                end
         | 
| 62 62 |  | 
| 63 | 
            -
             | 
| 63 | 
            +
                protected
         | 
| 64 64 |  | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 65 | 
            +
                def load_and_apply_config!
         | 
| 66 | 
            +
                  @config = Settings.worker_config(@options[:config_file])
         | 
| 67 | 
            +
                  Settings.apply_worker_config!(@config)
         | 
| 68 | 
            +
                end
         | 
| 69 69 |  | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 70 | 
            +
                def option_parser
         | 
| 71 | 
            +
                  @option_parser ||= OptionParser.new do |opts|
         | 
| 72 | 
            +
                    opts.banner = "Usage #{$0} <command> <options>"
         | 
| 73 | 
            +
                    opts.separator %(\nWhere <command> is one of:
         | 
| 74 74 | 
             
              start      start the jobs daemon
         | 
| 75 75 | 
             
              stop       stop the jobs daemon
         | 
| 76 76 | 
             
              run        start and run in the foreground
         | 
| 77 77 | 
             
              restart    stop and then start the jobs daemon
         | 
| 78 78 | 
             
              status     show daemon status
         | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 81 | 
            -
             | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 79 | 
            +
            )
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                    opts.separator "\n<options>"
         | 
| 82 | 
            +
                    opts.on("-c", "--config [CONFIG_PATH]", "Use alternate config file (default #{@options[:config_file]})") do |c|
         | 
| 83 | 
            +
                      @options[:config_file] = c
         | 
| 84 | 
            +
                    end
         | 
| 85 | 
            +
                    opts.on("-p", "--pid [PID_PATH]",
         | 
| 86 | 
            +
                            "Use alternate folder for PID files (default #{@options[:pid_folder]})") do |p|
         | 
| 87 | 
            +
                      @options[:pid_folder] = p
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                    opts.on("--no-tail", "Don't tail the logs (only affects non-daemon mode)") { @options[:tail_logs] = false }
         | 
| 90 | 
            +
                    opts.on("--with-prejudice", "When stopping, interrupt jobs in progress, instead of letting them drain") do
         | 
| 91 | 
            +
                      @options[:kill] ||= true
         | 
| 92 | 
            +
                    end
         | 
| 93 | 
            +
                    opts.on("--with-extreme-prejudice",
         | 
| 94 | 
            +
                            "When stopping, immediately kill jobs in progress, instead of letting them drain") do
         | 
| 95 | 
            +
                      @options[:kill] = 9
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
                    opts.on_tail("-h", "--help", "Show this message") do
         | 
| 98 | 
            +
                      puts opts
         | 
| 99 | 
            +
                      exit
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
                  end
         | 
| 88 102 | 
             
                end
         | 
| 89 | 
            -
              end
         | 
| 90 103 |  | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 104 | 
            +
                def daemon
         | 
| 105 | 
            +
                  @daemon ||= Delayed::Daemon.new(@options[:pid_folder])
         | 
| 106 | 
            +
                end
         | 
| 94 107 |  | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 108 | 
            +
                def start
         | 
| 109 | 
            +
                  load_rails
         | 
| 110 | 
            +
                  tail_rails_log unless daemon.daemonized?
         | 
| 111 | 
            +
                  Delayed::Pool.new(@config).start
         | 
| 112 | 
            +
                end
         | 
| 100 113 |  | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
             | 
| 104 | 
            -
             | 
| 114 | 
            +
                def load_rails
         | 
| 115 | 
            +
                  require(Settings.expand_rails_path("config/environment.rb"))
         | 
| 116 | 
            +
                  Dir.chdir(Rails.root)
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def tail_rails_log
         | 
| 120 | 
            +
                  return unless @options[:tail_logs]
         | 
| 105 121 |  | 
| 106 | 
            -
             | 
| 107 | 
            -
                 | 
| 108 | 
            -
                Delayed::LogTailer.new.run
         | 
| 122 | 
            +
                  Delayed::LogTailer.new.run
         | 
| 123 | 
            +
                end
         | 
| 109 124 | 
             
              end
         | 
| 110 125 | 
             
            end
         | 
| 111 | 
            -
            end
         | 
| @@ -1,9 +1,11 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Kernel
         | 
| 2 | 
            -
              def sender( | 
| 4 | 
            +
              def sender(idx = 0)
         | 
| 3 5 | 
             
                frame_self = nil
         | 
| 4 6 | 
             
                # 3. one for the block, one for this method, one for the method calling this
         | 
| 5 7 | 
             
                # method, and _then_ we get to the self for who sent the message we want
         | 
| 6 | 
            -
                RubyVM::DebugInspector.open { |dc| frame_self = dc.frame_self(3 +  | 
| 8 | 
            +
                RubyVM::DebugInspector.open { |dc| frame_self = dc.frame_self(3 + idx) }
         | 
| 7 9 | 
             
                frame_self
         | 
| 8 10 | 
             
              end
         | 
| 9 11 | 
             
            end
         |