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
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require  | 
| 3 | 
            +
            require "spec_helper"
         | 
| 4 4 |  | 
| 5 5 | 
             
            RSpec.describe Delayed::Worker::HealthCheck do
         | 
| 6 6 | 
             
              let(:klass) { Class.new(Delayed::Worker::HealthCheck) { self.type_name = :test } }
         | 
| @@ -10,116 +10,124 @@ RSpec.describe Delayed::Worker::HealthCheck do | |
| 10 10 | 
             
              end
         | 
| 11 11 |  | 
| 12 12 | 
             
              after do
         | 
| 13 | 
            -
                 | 
| 13 | 
            +
                described_class.subclasses.delete(klass)
         | 
| 14 14 | 
             
              end
         | 
| 15 15 |  | 
| 16 16 | 
             
              it "must maintain a list of its subclasses" do
         | 
| 17 17 | 
             
                klass
         | 
| 18 | 
            -
                expect( | 
| 18 | 
            +
                expect(described_class.subclasses).to include klass
         | 
| 19 19 | 
             
              end
         | 
| 20 20 |  | 
| 21 | 
            -
              describe  | 
| 22 | 
            -
                it  | 
| 23 | 
            -
                  check =  | 
| 21 | 
            +
              describe ".build(type:, config: {})" do
         | 
| 22 | 
            +
                it "must select the concrete class to use by the type_name in the subclass" do
         | 
| 23 | 
            +
                  check = described_class.build(type: "test", worker_name: "foobar")
         | 
| 24 24 | 
             
                  expect(check).to be_a(klass)
         | 
| 25 25 | 
             
                end
         | 
| 26 26 |  | 
| 27 27 | 
             
                it "must raise ArgumentError when the specified type doesn't exist" do
         | 
| 28 | 
            -
                  expect  | 
| 29 | 
            -
                     | 
| 30 | 
            -
                   | 
| 28 | 
            +
                  expect do
         | 
| 29 | 
            +
                    described_class.build(type: "nope", config: { worker_name: "foobar" })
         | 
| 30 | 
            +
                  end.to raise_error ArgumentError
         | 
| 31 31 | 
             
                end
         | 
| 32 32 |  | 
| 33 | 
            -
                it  | 
| 34 | 
            -
                  config = {foo:  | 
| 35 | 
            -
                  check =  | 
| 33 | 
            +
                it "must initiaize the specified class using the supplied config" do
         | 
| 34 | 
            +
                  config = { foo: "bar" }.with_indifferent_access
         | 
| 35 | 
            +
                  check = described_class.build(type: "test", worker_name: "foobar", config: config)
         | 
| 36 36 | 
             
                  expect(check.config).to eq config
         | 
| 37 37 | 
             
                end
         | 
| 38 38 | 
             
              end
         | 
| 39 39 |  | 
| 40 | 
            -
              describe  | 
| 41 | 
            -
                let(:klass)  | 
| 42 | 
            -
                   | 
| 43 | 
            -
             | 
| 44 | 
            -
                     | 
| 40 | 
            +
              describe ".reschedule_abandoned_jobs" do
         | 
| 41 | 
            +
                let(:klass) do
         | 
| 42 | 
            +
                  Class.new(Delayed::Worker::HealthCheck) do
         | 
| 43 | 
            +
                    self.type_name = :fake
         | 
| 44 | 
            +
                    class << self
         | 
| 45 | 
            +
                      attr_accessor :live_workers
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    def live_workers
         | 
| 49 | 
            +
                      self.class.live_workers
         | 
| 50 | 
            +
                    end
         | 
| 45 51 | 
             
                  end
         | 
| 52 | 
            +
                end
         | 
| 46 53 |  | 
| 47 | 
            -
             | 
| 48 | 
            -
                    self.class.live_workers
         | 
| 49 | 
            -
                  end
         | 
| 50 | 
            -
                } }
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                let(:initial_run_at) { Time.zone.now }
         | 
| 54 | 
            +
                let(:initial_run_at) { 10.minutes.ago }
         | 
| 53 55 |  | 
| 54 56 | 
             
                before do
         | 
| 55 | 
            -
                  klass.live_workers = %w | 
| 57 | 
            +
                  klass.live_workers = %w[alive]
         | 
| 56 58 | 
             
                  Delayed.select_backend(Delayed::Backend::ActiveRecord::Job)
         | 
| 57 59 |  | 
| 58 60 | 
             
                  2.times { Delayed::Job.enqueue(SimpleJob.new, run_at: initial_run_at, max_attempts: 4) }
         | 
| 59 61 | 
             
                  @alive_job = Delayed::Job.first
         | 
| 60 62 | 
             
                  @alive_job.update!({
         | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 63 | 
            +
                                       locked_by: "alive",
         | 
| 64 | 
            +
                                       locked_at: initial_run_at
         | 
| 65 | 
            +
                                     })
         | 
| 64 66 | 
             
                  @dead_job = Delayed::Job.last
         | 
| 65 67 | 
             
                  @dead_job.update!({
         | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 68 | 
            +
                                      locked_by: "dead",
         | 
| 69 | 
            +
                                      locked_at: initial_run_at
         | 
| 70 | 
            +
                                    })
         | 
| 69 71 | 
             
                  Delayed::Settings.worker_health_check_type = :fake
         | 
| 70 72 | 
             
                  Delayed::Settings.worker_health_check_config = {}
         | 
| 71 73 | 
             
                end
         | 
| 72 74 |  | 
| 73 75 | 
             
                after do
         | 
| 74 | 
            -
                   | 
| 76 | 
            +
                  described_class.subclasses.delete(klass)
         | 
| 75 77 | 
             
                  Delayed::Settings.worker_health_check_type = :none
         | 
| 76 78 | 
             
                  Delayed::Settings.worker_health_check_config = {}
         | 
| 77 79 | 
             
                end
         | 
| 78 80 |  | 
| 79 | 
            -
                it  | 
| 80 | 
            -
                   | 
| 81 | 
            +
                it "must leave jobs locked by live workers alone" do
         | 
| 82 | 
            +
                  described_class.reschedule_abandoned_jobs
         | 
| 81 83 | 
             
                  @alive_job.reload
         | 
| 82 84 | 
             
                  expect(@alive_job.run_at.to_i).to eq initial_run_at.to_i
         | 
| 83 85 | 
             
                  expect(@alive_job.locked_at.to_i).to eq initial_run_at.to_i
         | 
| 84 | 
            -
                  expect(@alive_job.locked_by).to eq  | 
| 86 | 
            +
                  expect(@alive_job.locked_by).to eq "alive"
         | 
| 85 87 | 
             
                end
         | 
| 86 88 |  | 
| 87 | 
            -
                it  | 
| 88 | 
            -
                   | 
| 89 | 
            +
                it "must reschedule jobs locked by dead workers" do
         | 
| 90 | 
            +
                  described_class.reschedule_abandoned_jobs
         | 
| 89 91 | 
             
                  @dead_job.reload
         | 
| 90 92 | 
             
                  expect(@dead_job.run_at).to be > initial_run_at
         | 
| 91 93 | 
             
                  expect(@dead_job.locked_at).to be_nil
         | 
| 92 94 | 
             
                  expect(@dead_job.locked_by).to be_nil
         | 
| 93 95 | 
             
                end
         | 
| 94 96 |  | 
| 95 | 
            -
                it  | 
| 96 | 
            -
                  Delayed::Job.where(id: @dead_job).update_all(locked_by:  | 
| 97 | 
            -
                   | 
| 98 | 
            -
                   | 
| 97 | 
            +
                it "ignores jobs that are re-locked after fetching from db" do
         | 
| 98 | 
            +
                  Delayed::Job.where(id: @dead_job).update_all(locked_by: "someone_else")
         | 
| 99 | 
            +
                  # we need to return @dead_job itself, which doesn't match the database
         | 
| 100 | 
            +
                  jobs_scope = double
         | 
| 101 | 
            +
                  allow(jobs_scope).to receive(:where).and_return(jobs_scope)
         | 
| 102 | 
            +
                  allow(jobs_scope).to receive(:not).and_return(jobs_scope)
         | 
| 103 | 
            +
                  allow(jobs_scope).to receive(:limit).and_return(jobs_scope)
         | 
| 104 | 
            +
                  allow(jobs_scope).to receive(:to_a).and_return([@dead_job], [])
         | 
| 105 | 
            +
                  allow(Delayed::Job).to receive(:running_jobs).and_return(jobs_scope)
         | 
| 106 | 
            +
                  described_class.reschedule_abandoned_jobs
         | 
| 99 107 | 
             
                  @dead_job.reload
         | 
| 100 | 
            -
                  expect(@dead_job.locked_by).to eq  | 
| 108 | 
            +
                  expect(@dead_job.locked_by).to eq "someone_else"
         | 
| 101 109 | 
             
                end
         | 
| 102 110 |  | 
| 103 | 
            -
                it  | 
| 104 | 
            -
                  Delayed::Job.where(id: @dead_job).update_all(locked_by:  | 
| 105 | 
            -
                  allow(Delayed::Job).to receive(:running_jobs).and_return( | 
| 106 | 
            -
                   | 
| 111 | 
            +
                it "ignores jobs that are prefetched" do
         | 
| 112 | 
            +
                  Delayed::Job.where(id: @dead_job).update_all(locked_by: "prefetch:some_node")
         | 
| 113 | 
            +
                  allow(Delayed::Job).to receive(:running_jobs).and_return(Delayed::Job.where(id: @dead_job.id))
         | 
| 114 | 
            +
                  described_class.reschedule_abandoned_jobs
         | 
| 107 115 | 
             
                  @dead_job.reload
         | 
| 108 | 
            -
                  expect(@dead_job.locked_by).to eq  | 
| 116 | 
            +
                  expect(@dead_job.locked_by).to eq "prefetch:some_node"
         | 
| 109 117 | 
             
                end
         | 
| 110 118 |  | 
| 111 119 | 
             
                it "bails immediately if advisory lock already taken" do
         | 
| 112 | 
            -
                  allow(Delayed:: | 
| 113 | 
            -
                   | 
| 120 | 
            +
                  allow(Delayed::Job).to receive(:attempt_advisory_lock).and_return(false)
         | 
| 121 | 
            +
                  described_class.reschedule_abandoned_jobs
         | 
| 114 122 | 
             
                  @dead_job.reload
         | 
| 115 123 | 
             
                  expect(@dead_job.run_at.to_i).to eq(initial_run_at.to_i)
         | 
| 116 | 
            -
                  expect(@dead_job.locked_at). | 
| 117 | 
            -
                  expect(@dead_job.locked_by). | 
| 124 | 
            +
                  expect(@dead_job.locked_at).not_to be_nil
         | 
| 125 | 
            +
                  expect(@dead_job.locked_by).not_to be_nil
         | 
| 118 126 | 
             
                end
         | 
| 119 127 | 
             
              end
         | 
| 120 128 |  | 
| 121 | 
            -
              describe  | 
| 122 | 
            -
                it  | 
| 129 | 
            +
              describe "#initialize" do
         | 
| 130 | 
            +
                it "must raise ArgumentError when the worker name is not supplied" do
         | 
| 123 131 | 
             
                  expect { klass.new }.to raise_error ArgumentError
         | 
| 124 132 | 
             
                end
         | 
| 125 133 | 
             
              end
         | 
    
        data/spec/delayed/worker_spec.rb
    CHANGED
    
    | @@ -3,45 +3,73 @@ | |
| 3 3 | 
             
            require_relative "../spec_helper"
         | 
| 4 4 |  | 
| 5 5 | 
             
            describe Delayed::Worker do
         | 
| 6 | 
            -
              let(:worker_config) { {
         | 
| 7 | 
            -
                  queue: "test", min_priority: 1, max_priority: 2, stuff: "stuff",
         | 
| 8 | 
            -
              }.freeze }
         | 
| 9 6 | 
             
              subject { described_class.new(worker_config.dup) }
         | 
| 10 7 |  | 
| 11 | 
            -
               | 
| 8 | 
            +
              let(:worker_config) do
         | 
| 9 | 
            +
                {
         | 
| 10 | 
            +
                  queue: "test", min_priority: 1, max_priority: 2, stuff: "stuff"
         | 
| 11 | 
            +
                }.freeze
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
              let(:job_attrs) do
         | 
| 14 | 
            +
                {
         | 
| 15 | 
            +
                  id: 42, name: "testjob", full_name: "testfullname", :last_error= => nil,
         | 
| 16 | 
            +
                  attempts: 1, reschedule: nil, :expired? => false,
         | 
| 17 | 
            +
                  payload_object: {}, priority: 25
         | 
| 18 | 
            +
                }.freeze
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              after { described_class.lifecycle.reset! }
         | 
| 12 22 |  | 
| 13 23 | 
             
              describe "#perform" do
         | 
| 14 24 | 
             
                it "fires off an error callback when a job raises an exception" do
         | 
| 15 25 | 
             
                  fired = false
         | 
| 16 | 
            -
                   | 
| 17 | 
            -
                  job = double( | 
| 18 | 
            -
                  subject.perform(job)
         | 
| 26 | 
            +
                  described_class.lifecycle.before(:error) { |_worker, _exception| fired = true }
         | 
| 27 | 
            +
                  job = double(job_attrs)
         | 
| 28 | 
            +
                  output_count = subject.perform(job)
         | 
| 19 29 | 
             
                  expect(fired).to be_truthy
         | 
| 30 | 
            +
                  expect(output_count).to eq(1)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                it "uses the retry callback for a retriable exception" do
         | 
| 34 | 
            +
                  error_fired = retry_fired = false
         | 
| 35 | 
            +
                  described_class.lifecycle.before(:error) { |_worker, _exception| error_fired = true }
         | 
| 36 | 
            +
                  described_class.lifecycle.before(:retry) { |_worker, _exception| retry_fired = true }
         | 
| 37 | 
            +
                  job = Delayed::Job.new(payload_object: {}, priority: 25, strand: "test_jobs", max_attempts: 3)
         | 
| 38 | 
            +
                  expect(job).to receive(:invoke_job) do
         | 
| 39 | 
            +
                    raise Delayed::RetriableError, "that's all this job does"
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                  output_count = subject.perform(job)
         | 
| 42 | 
            +
                  expect(error_fired).to be_falsey
         | 
| 43 | 
            +
                  expect(retry_fired).to be_truthy
         | 
| 44 | 
            +
                  expect(output_count).to eq(1)
         | 
| 20 45 | 
             
                end
         | 
| 21 46 |  | 
| 22 | 
            -
                it "reloads" do
         | 
| 23 | 
            -
                   | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
                    reloader: double()
         | 
| 29 | 
            -
                  )
         | 
| 47 | 
            +
                it "reloads Rails classes (never more than once)" do
         | 
| 48 | 
            +
                  fake_application = double("Rails.application",
         | 
| 49 | 
            +
                                            config: double("Rails.application.config",
         | 
| 50 | 
            +
                                                           cache_classes: false,
         | 
| 51 | 
            +
                                                           reload_classes_only_on_change: false),
         | 
| 52 | 
            +
                                            reloader: double)
         | 
| 30 53 |  | 
| 31 | 
            -
                  allow(Rails).to receive(:application).and_return( | 
| 54 | 
            +
                  allow(Rails).to receive(:application).and_return(fake_application)
         | 
| 32 55 | 
             
                  if Rails::VERSION::MAJOR >= 5
         | 
| 33 56 | 
             
                    expect(Rails.application.reloader).to receive(:reload!).once
         | 
| 34 57 | 
             
                  else
         | 
| 35 58 | 
             
                    expect(ActionDispatch::Reloader).to receive(:prepare!).once
         | 
| 36 59 | 
             
                    expect(ActionDispatch::Reloader).to receive(:cleanup!).once
         | 
| 37 60 | 
             
                  end
         | 
| 38 | 
            -
                  job = double( | 
| 61 | 
            +
                  job = double(job_attrs)
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  # Create extra workers to make sure we don't reload multiple times
         | 
| 64 | 
            +
                  described_class.new(worker_config.dup)
         | 
| 65 | 
            +
                  described_class.new(worker_config.dup)
         | 
| 66 | 
            +
             | 
| 39 67 | 
             
                  subject.perform(job)
         | 
| 40 68 | 
             
                end
         | 
| 41 69 | 
             
              end
         | 
| 42 70 |  | 
| 43 71 | 
             
              describe "#log_job" do
         | 
| 44 | 
            -
                around | 
| 72 | 
            +
                around do |block|
         | 
| 45 73 | 
             
                  prev_logger = Delayed::Settings.job_detailed_log_format
         | 
| 46 74 | 
             
                  block.call
         | 
| 47 75 | 
             
                  Delayed::Settings.job_detailed_log_format = prev_logger
         | 
| @@ -53,24 +81,25 @@ describe Delayed::Worker do | |
| 53 81 | 
             
                  short_log_format = subject.log_job(job, :short)
         | 
| 54 82 | 
             
                  expect(short_log_format).to eq("RSpec::Mocks::Double")
         | 
| 55 83 | 
             
                  long_format = subject.log_job(job, :long)
         | 
| 56 | 
            -
                  expect(long_format).to eq("RSpec::Mocks::Double {\"priority\":25,\"attempts\":0,\"created_at\":null,\"tag\":\"RSpec::Mocks::Double#perform\",\"max_attempts\":null,\"strand\":\"test_jobs\",\"source\":null}")
         | 
| 84 | 
            +
                  expect(long_format).to eq("RSpec::Mocks::Double {\"priority\":25,\"attempts\":0,\"created_at\":null,\"tag\":\"RSpec::Mocks::Double#perform\",\"max_attempts\":null,\"strand\":\"test_jobs\",\"source\":null,\"singleton\":null}") # rubocop:disable Layout/LineLength
         | 
| 57 85 | 
             
                end
         | 
| 58 86 |  | 
| 59 87 | 
             
                it "logging format can be changed with settings" do
         | 
| 60 | 
            -
                  Delayed::Settings.job_detailed_log_format = ->(job){ "override format #{job.strand}"}
         | 
| 88 | 
            +
                  Delayed::Settings.job_detailed_log_format = ->(job) { "override format detailed #{job.strand}" }
         | 
| 89 | 
            +
                  Delayed::Settings.job_short_log_format = ->(_job) { "override format short" }
         | 
| 61 90 | 
             
                  payload = double(perform: nil)
         | 
| 62 91 | 
             
                  job = Delayed::Job.new(payload_object: payload, priority: 25, strand: "test_jobs")
         | 
| 63 92 | 
             
                  short_log_format = subject.log_job(job, :short)
         | 
| 64 | 
            -
                  expect(short_log_format).to eq("RSpec::Mocks::Double")
         | 
| 93 | 
            +
                  expect(short_log_format).to eq("RSpec::Mocks::Double override format short")
         | 
| 65 94 | 
             
                  long_format = subject.log_job(job, :long)
         | 
| 66 | 
            -
                  expect(long_format).to eq("RSpec::Mocks::Double override format test_jobs")
         | 
| 95 | 
            +
                  expect(long_format).to eq("RSpec::Mocks::Double override format detailed test_jobs")
         | 
| 67 96 | 
             
                end
         | 
| 68 97 | 
             
              end
         | 
| 69 98 |  | 
| 70 99 | 
             
              describe "#run" do
         | 
| 71 100 | 
             
                it "passes extra config options through to the WorkQueue" do
         | 
| 72 | 
            -
                  expect(subject.work_queue).to receive(:get_and_lock_next_available) | 
| 73 | 
            -
                    with(subject.name, worker_config).and_return(nil)
         | 
| 101 | 
            +
                  expect(subject.work_queue).to receive(:get_and_lock_next_available)
         | 
| 102 | 
            +
                    .with(subject.name, worker_config).and_return(nil)
         | 
| 74 103 | 
             
                  subject.run
         | 
| 75 104 | 
             
                end
         | 
| 76 105 | 
             
              end
         | 
    
        data/spec/sample_jobs.rb
    CHANGED
    
    | @@ -1,49 +1,79 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            class SimpleJob
         | 
| 4 | 
            -
               | 
| 5 | 
            -
             | 
| 4 | 
            +
              class << self
         | 
| 5 | 
            +
                attr_accessor :runs
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              self.runs = 0
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def perform
         | 
| 11 | 
            +
                self.class.runs += 1
         | 
| 12 | 
            +
              end
         | 
| 6 13 | 
             
            end
         | 
| 7 14 |  | 
| 8 15 | 
             
            class ErrorJob
         | 
| 9 | 
            -
               | 
| 10 | 
            -
             | 
| 16 | 
            +
              class << self
         | 
| 17 | 
            +
                attr_accessor :runs, :last_error, :failure_runs, :permanent_failure_runs
         | 
| 18 | 
            +
              end
         | 
| 11 19 |  | 
| 12 | 
            -
               | 
| 20 | 
            +
              self.runs = 0
         | 
| 21 | 
            +
              def perform
         | 
| 22 | 
            +
                raise "did not work"
         | 
| 23 | 
            +
              end
         | 
| 13 24 |  | 
| 14 | 
            -
               | 
| 15 | 
            -
               | 
| 25 | 
            +
              self.last_error = nil
         | 
| 26 | 
            +
              self.failure_runs = 0
         | 
| 27 | 
            +
              def on_failure(error)
         | 
| 28 | 
            +
                self.class.last_error = error
         | 
| 29 | 
            +
                self.class.failure_runs += 1
         | 
| 30 | 
            +
              end
         | 
| 16 31 |  | 
| 17 | 
            -
               | 
| 18 | 
            -
              def on_permanent_failure(error) | 
| 32 | 
            +
              self.permanent_failure_runs = 0
         | 
| 33 | 
            +
              def on_permanent_failure(error)
         | 
| 34 | 
            +
                self.class.last_error = error
         | 
| 35 | 
            +
                self.class.permanent_failure_runs += 1
         | 
| 36 | 
            +
              end
         | 
| 19 37 | 
             
            end
         | 
| 20 38 |  | 
| 21 39 | 
             
            class UnlockJob
         | 
| 22 40 | 
             
              attr_accessor :times_to_unlock
         | 
| 41 | 
            +
             | 
| 23 42 | 
             
              def initialize(times_to_unlock)
         | 
| 24 43 | 
             
                @times_to_unlock = times_to_unlock
         | 
| 25 44 | 
             
              end
         | 
| 26 45 |  | 
| 27 | 
            -
              def perform | 
| 46 | 
            +
              def perform
         | 
| 47 | 
            +
                raise SystemExit, "raising to trigger on_failure"
         | 
| 48 | 
            +
              end
         | 
| 28 49 |  | 
| 29 | 
            -
              def on_failure( | 
| 50 | 
            +
              def on_failure(_error)
         | 
| 30 51 | 
             
                times_to_unlock -= 1
         | 
| 31 52 | 
             
                :unlock if times_to_unlock <= 0
         | 
| 32 53 | 
             
              end
         | 
| 33 54 | 
             
            end
         | 
| 34 55 |  | 
| 35 56 | 
             
            class LongRunningJob
         | 
| 36 | 
            -
              def perform | 
| 57 | 
            +
              def perform
         | 
| 58 | 
            +
                sleep 250
         | 
| 59 | 
            +
              end
         | 
| 37 60 | 
             
            end
         | 
| 38 61 |  | 
| 39 62 | 
             
            module M
         | 
| 40 63 | 
             
              class ModuleJob
         | 
| 41 | 
            -
                 | 
| 42 | 
            -
             | 
| 64 | 
            +
                class << self
         | 
| 65 | 
            +
                  attr_accessor :runs
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                cattr_accessor :runs
         | 
| 69 | 
            +
                self.runs = 0
         | 
| 70 | 
            +
                def perform
         | 
| 71 | 
            +
                  self.class.runs += 1
         | 
| 72 | 
            +
                end
         | 
| 43 73 | 
             
              end
         | 
| 44 74 | 
             
            end
         | 
| 45 75 |  | 
| 46 76 | 
             
            class DeserializeErrorJob < SimpleJob; end
         | 
| 47 | 
            -
            Psych.add_domain_type("ruby/object", "DeserializeErrorJob") do |_type,  | 
| 77 | 
            +
            Psych.add_domain_type("ruby/object", "DeserializeErrorJob") do |_type, _val|
         | 
| 48 78 | 
             
              raise "error deserializing"
         | 
| 49 79 | 
             
            end
         | 
| @@ -1,98 +1,105 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            shared_examples_for  | 
| 3 | 
            +
            shared_examples_for "Delayed::Batch" do
         | 
| 4 4 | 
             
              context "batching" do
         | 
| 5 | 
            -
                it " | 
| 5 | 
            +
                it "batches up all deferrable delayed methods" do
         | 
| 6 6 | 
             
                  later = 1.hour.from_now
         | 
| 7 | 
            -
                  Delayed::Batch.serial_batch  | 
| 8 | 
            -
                    "string".delay(ignore_transaction: true).size. | 
| 9 | 
            -
                     | 
| 10 | 
            -
                    "string".delay(ignore_transaction: true). | 
| 11 | 
            -
             | 
| 7 | 
            +
                  Delayed::Batch.serial_batch do
         | 
| 8 | 
            +
                    expect("string".delay(ignore_transaction: true).size).to be true
         | 
| 9 | 
            +
                    # won't be batched, it'll get its own job
         | 
| 10 | 
            +
                    expect("string".delay(run_at: later, ignore_transaction: true).reverse).to be_truthy
         | 
| 11 | 
            +
                    expect("string".delay(ignore_transaction: true).gsub(/./, "!")).to be_truthy
         | 
| 12 | 
            +
                  end
         | 
| 12 13 | 
             
                  batch_jobs = Delayed::Job.find_available(5)
         | 
| 13 14 | 
             
                  regular_jobs = Delayed::Job.list_jobs(:future, 5)
         | 
| 14 | 
            -
                  regular_jobs.size. | 
| 15 | 
            -
                  regular_jobs.first.batch | 
| 16 | 
            -
                  batch_jobs.size. | 
| 15 | 
            +
                  expect(regular_jobs.size).to eq(1)
         | 
| 16 | 
            +
                  expect(regular_jobs.first.batch?).to be(false)
         | 
| 17 | 
            +
                  expect(batch_jobs.size).to eq(1)
         | 
| 17 18 | 
             
                  batch_job = batch_jobs.first
         | 
| 18 | 
            -
                  batch_job.batch | 
| 19 | 
            -
                  batch_job.payload_object.mode. | 
| 20 | 
            -
                  batch_job.payload_object.jobs.map  | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 19 | 
            +
                  expect(batch_job.batch?).to be(true)
         | 
| 20 | 
            +
                  expect(batch_job.payload_object.mode).to eq(:serial)
         | 
| 21 | 
            +
                  expect(batch_job.payload_object.jobs.map do |j|
         | 
| 22 | 
            +
                           [j.payload_object.object, j.payload_object.method, j.payload_object.args]
         | 
| 23 | 
            +
                         end).to eq([
         | 
| 24 | 
            +
                                      [
         | 
| 25 | 
            +
                                        "string", :size, []
         | 
| 26 | 
            +
                                      ],
         | 
| 27 | 
            +
                                      [
         | 
| 28 | 
            +
                                        "string", :gsub, [
         | 
| 29 | 
            +
                                          /./, "!"
         | 
| 30 | 
            +
                                        ]
         | 
| 31 | 
            +
                                      ]
         | 
| 32 | 
            +
                                    ])
         | 
| 24 33 | 
             
                end
         | 
| 25 34 |  | 
| 26 | 
            -
                it " | 
| 27 | 
            -
                   | 
| 28 | 
            -
             | 
| 29 | 
            -
                    "string".delay(ignore_transaction: true). | 
| 30 | 
            -
             | 
| 31 | 
            -
                   | 
| 32 | 
            -
                  Delayed::Job.jobs_count(:current).should == 1
         | 
| 35 | 
            +
                it "does not let you invoke it directly" do
         | 
| 36 | 
            +
                  Delayed::Batch.serial_batch do
         | 
| 37 | 
            +
                    expect("string".delay(ignore_transaction: true).size).to be true
         | 
| 38 | 
            +
                    expect("string".delay(ignore_transaction: true).gsub(/./, "!")).to be true
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 33 41 | 
             
                  job = Delayed::Job.find_available(1).first
         | 
| 34 | 
            -
                  expect{ job.invoke_job }.to raise_error(RuntimeError)
         | 
| 42 | 
            +
                  expect { job.invoke_job }.to raise_error(RuntimeError)
         | 
| 35 43 | 
             
                end
         | 
| 36 44 |  | 
| 37 | 
            -
                it " | 
| 38 | 
            -
                  Delayed::Batch.serial_batch  | 
| 39 | 
            -
                    "string".delay(ignore_transaction: true).size. | 
| 40 | 
            -
                    "string".delay(ignore_transaction: true).gsub(/./, "!"). | 
| 41 | 
            -
                   | 
| 42 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 45 | 
            +
                it "creates valid jobs" do
         | 
| 46 | 
            +
                  Delayed::Batch.serial_batch do
         | 
| 47 | 
            +
                    expect("string".delay(ignore_transaction: true).size).to be true
         | 
| 48 | 
            +
                    expect("string".delay(ignore_transaction: true).gsub(/./, "!")).to be true
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 43 51 |  | 
| 44 52 | 
             
                  batch_job = Delayed::Job.find_available(1).first
         | 
| 45 | 
            -
                  batch_job.batch | 
| 53 | 
            +
                  expect(batch_job.batch?).to be(true)
         | 
| 46 54 | 
             
                  jobs = batch_job.payload_object.jobs
         | 
| 47 | 
            -
                  jobs.size. | 
| 48 | 
            -
                  jobs[0]. | 
| 49 | 
            -
                  jobs[0].payload_object.class. | 
| 50 | 
            -
                  jobs[0].payload_object.method. | 
| 51 | 
            -
                  jobs[0].payload_object.args. | 
| 52 | 
            -
                  jobs[0].payload_object.perform. | 
| 53 | 
            -
                  jobs[1]. | 
| 54 | 
            -
                  jobs[1].payload_object.class. | 
| 55 | 
            -
                  jobs[1].payload_object.method. | 
| 56 | 
            -
                  jobs[1].payload_object.args. | 
| 57 | 
            -
                  jobs[1].payload_object.perform. | 
| 55 | 
            +
                  expect(jobs.size).to eq(2)
         | 
| 56 | 
            +
                  expect(jobs[0]).to be_new_record
         | 
| 57 | 
            +
                  expect(jobs[0].payload_object.class).to   eq(Delayed::PerformableMethod)
         | 
| 58 | 
            +
                  expect(jobs[0].payload_object.method).to  eq(:size)
         | 
| 59 | 
            +
                  expect(jobs[0].payload_object.args).to    eq([])
         | 
| 60 | 
            +
                  expect(jobs[0].payload_object.perform).to eq(6)
         | 
| 61 | 
            +
                  expect(jobs[1]).to be_new_record
         | 
| 62 | 
            +
                  expect(jobs[1].payload_object.class).to   eq(Delayed::PerformableMethod)
         | 
| 63 | 
            +
                  expect(jobs[1].payload_object.method).to  eq(:gsub)
         | 
| 64 | 
            +
                  expect(jobs[1].payload_object.args).to    eq([/./, "!"])
         | 
| 65 | 
            +
                  expect(jobs[1].payload_object.perform).to eq("!!!!!!")
         | 
| 58 66 | 
             
                end
         | 
| 59 67 |  | 
| 60 | 
            -
                it " | 
| 61 | 
            -
                   | 
| 62 | 
            -
             | 
| 63 | 
            -
                    "string".delay( | 
| 64 | 
            -
             | 
| 65 | 
            -
                   | 
| 66 | 
            -
                  Delayed::Job.jobs_count(:current).should == 2
         | 
| 68 | 
            +
                it "creates a different batch for each priority" do
         | 
| 69 | 
            +
                  Delayed::Batch.serial_batch do
         | 
| 70 | 
            +
                    expect("string".delay(priority: Delayed::LOW_PRIORITY, ignore_transaction: true).size).to be true
         | 
| 71 | 
            +
                    expect("string".delay(ignore_transaction: true).gsub(/./, "!")).to be true
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(2)
         | 
| 67 74 | 
             
                end
         | 
| 68 75 |  | 
| 69 | 
            -
                it " | 
| 70 | 
            -
                  Delayed::Batch.serial_batch(: | 
| 71 | 
            -
                    "string".delay(priority: 20, ignore_transaction: true).size. | 
| 72 | 
            -
                    "string".delay(priority: 15, ignore_transaction: true).gsub(/./, "!"). | 
| 73 | 
            -
                   | 
| 74 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 75 | 
            -
                  Delayed::Job.find_available(1).first.priority. | 
| 76 | 
            +
                it "uses the given priority for all, if specified" do
         | 
| 77 | 
            +
                  Delayed::Batch.serial_batch(priority: 11) do
         | 
| 78 | 
            +
                    expect("string".delay(priority: 20, ignore_transaction: true).size).to be true
         | 
| 79 | 
            +
                    expect("string".delay(priority: 15, ignore_transaction: true).gsub(/./, "!")).to be true
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 82 | 
            +
                  expect(Delayed::Job.find_available(1).first.priority).to eq(11)
         | 
| 76 83 | 
             
                end
         | 
| 77 84 |  | 
| 78 | 
            -
                it " | 
| 79 | 
            -
                  Delayed::Batch.serial_batch(: | 
| 80 | 
            -
                    "string".delay(ignore_transaction: true).size. | 
| 81 | 
            -
                   | 
| 82 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 83 | 
            -
                  Delayed::Job.find_available(1).first.tag. | 
| 84 | 
            -
                  Delayed::Job.find_available(1).first.priority. | 
| 85 | 
            +
                it "justs create the job, if there's only one in the batch" do
         | 
| 86 | 
            +
                  Delayed::Batch.serial_batch(priority: 11) do
         | 
| 87 | 
            +
                    expect("string".delay(ignore_transaction: true).size).to be true
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 90 | 
            +
                  expect(Delayed::Job.find_available(1).first.tag).to eq("String#size")
         | 
| 91 | 
            +
                  expect(Delayed::Job.find_available(1).first.priority).to eq(11)
         | 
| 85 92 | 
             
                end
         | 
| 86 93 |  | 
| 87 | 
            -
                it " | 
| 88 | 
            -
                  Delayed::Batch.serial_batch(: | 
| 94 | 
            +
                it "lists a job only once when the same call is made multiple times" do
         | 
| 95 | 
            +
                  Delayed::Batch.serial_batch(priority: 11) do
         | 
| 89 96 | 
             
                    "string".delay(ignore_transaction: true).size
         | 
| 90 97 | 
             
                    "string".delay(ignore_transaction: true).gsub(/./, "!")
         | 
| 91 98 | 
             
                    "string".delay(ignore_transaction: true).size
         | 
| 92 | 
            -
                   | 
| 99 | 
            +
                  end
         | 
| 93 100 | 
             
                  batch_job = Delayed::Job.find_available(1).first
         | 
| 94 101 | 
             
                  jobs = batch_job.payload_object.jobs
         | 
| 95 | 
            -
                  jobs.size. | 
| 102 | 
            +
                  expect(jobs.size).to eq(2)
         | 
| 96 103 | 
             
                end
         | 
| 97 104 | 
             
              end
         | 
| 98 105 | 
             
            end
         |