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,220 +1,243 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
             | 
| 3 | 
            +
            require "timeout"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module InDelayedJobTest
         | 
| 6 | 
            +
              def self.check_in_job
         | 
| 7 | 
            +
                Delayed::Job.in_delayed_job?.should == true
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
            end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            shared_examples_for "a backend" do
         | 
| 4 12 | 
             
              def create_job(opts = {})
         | 
| 5 | 
            -
                Delayed::Job.enqueue(SimpleJob.new, **{ : | 
| 13 | 
            +
                Delayed::Job.enqueue(SimpleJob.new, **{ queue: nil }.merge(opts))
         | 
| 6 14 | 
             
              end
         | 
| 7 15 |  | 
| 8 16 | 
             
              before do
         | 
| 9 17 | 
             
                SimpleJob.runs = 0
         | 
| 10 18 | 
             
              end
         | 
| 11 19 |  | 
| 12 | 
            -
              it " | 
| 13 | 
            -
                Delayed::Job.create(: | 
| 20 | 
            +
              it "sets run_at automatically if not set" do
         | 
| 21 | 
            +
                expect(Delayed::Job.create(payload_object: ErrorJob.new).run_at).not_to be_nil
         | 
| 14 22 | 
             
              end
         | 
| 15 23 |  | 
| 16 | 
            -
              it " | 
| 24 | 
            +
              it "does not set run_at automatically if already set" do
         | 
| 17 25 | 
             
                later = Delayed::Job.db_time_now + 5.minutes
         | 
| 18 | 
            -
                Delayed::Job.create(: | 
| 26 | 
            +
                expect(Delayed::Job.create(payload_object: ErrorJob.new, run_at: later).run_at).to be_within(1).of(later)
         | 
| 19 27 | 
             
              end
         | 
| 20 28 |  | 
| 21 | 
            -
              it " | 
| 22 | 
            -
                 | 
| 29 | 
            +
              it "raises ArgumentError when handler doesn't respond_to :perform" do
         | 
| 30 | 
            +
                expect { Delayed::Job.enqueue(Object.new) }.to raise_error(ArgumentError)
         | 
| 23 31 | 
             
              end
         | 
| 24 32 |  | 
| 25 | 
            -
              it " | 
| 33 | 
            +
              it "increases count after enqueuing items" do
         | 
| 26 34 | 
             
                Delayed::Job.enqueue SimpleJob.new
         | 
| 27 | 
            -
                Delayed::Job.jobs_count(:current). | 
| 35 | 
            +
                expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              it "triggers the lifecycle event around the create" do
         | 
| 39 | 
            +
                called = false
         | 
| 40 | 
            +
                called_args = nil
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                Delayed::Worker.lifecycle.after(:create) do |args|
         | 
| 43 | 
            +
                  called = true
         | 
| 44 | 
            +
                  called_args = args
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                job = SimpleJob.new
         | 
| 48 | 
            +
                Delayed::Job.enqueue(job)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                expect(called).to be_truthy
         | 
| 51 | 
            +
                expect(called_args[:payload_object]).to eq job
         | 
| 28 52 | 
             
              end
         | 
| 29 53 |  | 
| 30 | 
            -
              it " | 
| 31 | 
            -
                @job = Delayed::Job.enqueue SimpleJob.new, : | 
| 32 | 
            -
                @job.priority. | 
| 54 | 
            +
              it "is able to set priority when enqueuing items" do
         | 
| 55 | 
            +
                @job = Delayed::Job.enqueue SimpleJob.new, priority: 5
         | 
| 56 | 
            +
                expect(@job.priority).to eq(5)
         | 
| 33 57 | 
             
              end
         | 
| 34 58 |  | 
| 35 | 
            -
              it " | 
| 59 | 
            +
              it "uses the default priority when enqueuing items" do
         | 
| 36 60 | 
             
                Delayed::Job.default_priority = 0
         | 
| 37 61 | 
             
                @job = Delayed::Job.enqueue SimpleJob.new
         | 
| 38 | 
            -
                @job.priority. | 
| 62 | 
            +
                expect(@job.priority).to eq(0)
         | 
| 39 63 | 
             
                Delayed::Job.default_priority = 10
         | 
| 40 64 | 
             
                @job = Delayed::Job.enqueue SimpleJob.new
         | 
| 41 | 
            -
                @job.priority. | 
| 65 | 
            +
                expect(@job.priority).to eq(10)
         | 
| 42 66 | 
             
                Delayed::Job.default_priority = 0
         | 
| 43 67 | 
             
              end
         | 
| 44 68 |  | 
| 45 | 
            -
              it " | 
| 69 | 
            +
              it "is able to set run_at when enqueuing items" do
         | 
| 46 70 | 
             
                later = Delayed::Job.db_time_now + 5.minutes
         | 
| 47 | 
            -
                @job = Delayed::Job.enqueue SimpleJob.new, : | 
| 48 | 
            -
                @job.run_at. | 
| 71 | 
            +
                @job = Delayed::Job.enqueue SimpleJob.new, priority: 5, run_at: later
         | 
| 72 | 
            +
                expect(@job.run_at).to be_within(1).of(later)
         | 
| 49 73 | 
             
              end
         | 
| 50 74 |  | 
| 51 | 
            -
              it " | 
| 75 | 
            +
              it "is able to set expires_at when enqueuing items" do
         | 
| 52 76 | 
             
                later = Delayed::Job.db_time_now + 1.day
         | 
| 53 | 
            -
                @job = Delayed::Job.enqueue SimpleJob.new, : | 
| 54 | 
            -
                @job.expires_at. | 
| 77 | 
            +
                @job = Delayed::Job.enqueue SimpleJob.new, expires_at: later
         | 
| 78 | 
            +
                expect(@job.expires_at).to be_within(1).of(later)
         | 
| 55 79 | 
             
              end
         | 
| 56 80 |  | 
| 57 | 
            -
              it " | 
| 81 | 
            +
              it "works with jobs in modules" do
         | 
| 58 82 | 
             
                M::ModuleJob.runs = 0
         | 
| 59 83 | 
             
                job = Delayed::Job.enqueue M::ModuleJob.new
         | 
| 60 | 
            -
                 | 
| 84 | 
            +
                expect { job.invoke_job }.to change { M::ModuleJob.runs }.from(0).to(1)
         | 
| 61 85 | 
             
              end
         | 
| 62 86 |  | 
| 63 | 
            -
              it " | 
| 64 | 
            -
                job = Delayed::Job.new : | 
| 65 | 
            -
                 | 
| 87 | 
            +
              it "raises an DeserializationError when the job class is totally unknown" do
         | 
| 88 | 
            +
                job = Delayed::Job.new handler: "--- !ruby/object:JobThatDoesNotExist {}"
         | 
| 89 | 
            +
                expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
         | 
| 66 90 | 
             
              end
         | 
| 67 91 |  | 
| 68 | 
            -
              it " | 
| 69 | 
            -
                job = Delayed::Job.new : | 
| 70 | 
            -
                 | 
| 92 | 
            +
              it "tries to load the class when it is unknown at the time of the deserialization" do
         | 
| 93 | 
            +
                job = Delayed::Job.new handler: "--- !ruby/object:JobThatDoesNotExist {}"
         | 
| 94 | 
            +
                expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
         | 
| 71 95 | 
             
              end
         | 
| 72 96 |  | 
| 73 | 
            -
              it " | 
| 74 | 
            -
                job = Delayed::Job.new : | 
| 75 | 
            -
                 | 
| 97 | 
            +
              it "tries include the namespace when loading unknown objects" do
         | 
| 98 | 
            +
                job = Delayed::Job.new handler: "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
         | 
| 99 | 
            +
                expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
         | 
| 76 100 | 
             
              end
         | 
| 77 101 |  | 
| 78 | 
            -
              it " | 
| 79 | 
            -
                job = Delayed::Job.new : | 
| 80 | 
            -
                 | 
| 102 | 
            +
              it "alsoes try to load structs when they are unknown (raises TypeError)" do
         | 
| 103 | 
            +
                job = Delayed::Job.new handler: "--- !ruby/struct:JobThatDoesNotExist {}"
         | 
| 104 | 
            +
                expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
         | 
| 81 105 | 
             
              end
         | 
| 82 106 |  | 
| 83 | 
            -
              it " | 
| 84 | 
            -
                job = Delayed::Job.new : | 
| 85 | 
            -
                 | 
| 107 | 
            +
              it "tries include the namespace when loading unknown structs" do
         | 
| 108 | 
            +
                job = Delayed::Job.new handler: "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
         | 
| 109 | 
            +
                expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
         | 
| 86 110 | 
             
              end
         | 
| 87 111 |  | 
| 88 | 
            -
              it " | 
| 89 | 
            -
                job = Delayed::Job.new : | 
| 90 | 
            -
                 | 
| 112 | 
            +
              it "raises an DeserializationError when the handler is invalid YAML" do
         | 
| 113 | 
            +
                job = Delayed::Job.new handler: %(test: ""11")
         | 
| 114 | 
            +
                expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError, /parsing error/)
         | 
| 91 115 | 
             
              end
         | 
| 92 116 |  | 
| 93 117 | 
             
              describe "find_available" do
         | 
| 94 | 
            -
                it " | 
| 95 | 
            -
                  @job = create_job : | 
| 118 | 
            +
                it "does not find failed jobs" do
         | 
| 119 | 
            +
                  @job = create_job attempts: 50
         | 
| 96 120 | 
             
                  @job.fail!
         | 
| 97 | 
            -
                  Delayed::Job.find_available(5). | 
| 121 | 
            +
                  expect(Delayed::Job.find_available(5)).not_to include(@job)
         | 
| 98 122 | 
             
                end
         | 
| 99 123 |  | 
| 100 | 
            -
                it " | 
| 101 | 
            -
                  @job = create_job : | 
| 102 | 
            -
                  Delayed::Job.find_available(5). | 
| 124 | 
            +
                it "does not find jobs scheduled for the future" do
         | 
| 125 | 
            +
                  @job = create_job run_at: (Delayed::Job.db_time_now + 1.minute)
         | 
| 126 | 
            +
                  expect(Delayed::Job.find_available(5)).not_to include(@job)
         | 
| 103 127 | 
             
                end
         | 
| 104 128 |  | 
| 105 | 
            -
                it " | 
| 129 | 
            +
                it "does not find jobs locked by another worker" do
         | 
| 106 130 | 
             
                  @job = create_job
         | 
| 107 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 108 | 
            -
                  Delayed::Job.find_available(5). | 
| 131 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("other_worker")).to eq(@job)
         | 
| 132 | 
            +
                  expect(Delayed::Job.find_available(5)).not_to include(@job)
         | 
| 109 133 | 
             
                end
         | 
| 110 134 |  | 
| 111 | 
            -
                it " | 
| 135 | 
            +
                it "finds open jobs" do
         | 
| 112 136 | 
             
                  @job = create_job
         | 
| 113 | 
            -
                  Delayed::Job.find_available(5). | 
| 137 | 
            +
                  expect(Delayed::Job.find_available(5)).to include(@job)
         | 
| 114 138 | 
             
                end
         | 
| 115 139 |  | 
| 116 140 | 
             
                it "returns an empty hash when asking for multiple jobs, and there aren't any" do
         | 
| 117 | 
            -
                  locked_jobs = Delayed::Job.get_and_lock_next_available([ | 
| 118 | 
            -
                  locked_jobs. | 
| 141 | 
            +
                  locked_jobs = Delayed::Job.get_and_lock_next_available(%w[worker1 worker2])
         | 
| 142 | 
            +
                  expect(locked_jobs).to eq({})
         | 
| 119 143 | 
             
                end
         | 
| 120 144 | 
             
              end
         | 
| 121 145 |  | 
| 122 146 | 
             
              context "when another worker is already performing an task, it" do
         | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
                   | 
| 126 | 
            -
                  Delayed::Job.get_and_lock_next_available('worker1').should == @job
         | 
| 147 | 
            +
                before do
         | 
| 148 | 
            +
                  @job = Delayed::Job.create payload_object: SimpleJob.new
         | 
| 149 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("worker1")).to eq(@job)
         | 
| 127 150 | 
             
                end
         | 
| 128 151 |  | 
| 129 | 
            -
                it " | 
| 130 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 152 | 
            +
                it "does not allow a second worker to get exclusive access" do
         | 
| 153 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("worker2")).to be_nil
         | 
| 131 154 | 
             
                end
         | 
| 132 155 |  | 
| 133 | 
            -
                it " | 
| 134 | 
            -
                  Delayed::Job.find_available(1).length. | 
| 156 | 
            +
                it "is not found by another worker" do
         | 
| 157 | 
            +
                  expect(Delayed::Job.find_available(1).length).to eq(0)
         | 
| 135 158 | 
             
                end
         | 
| 136 159 | 
             
              end
         | 
| 137 160 |  | 
| 138 | 
            -
               | 
| 139 | 
            -
                it " | 
| 140 | 
            -
                  Delayed::Job.create(: | 
| 161 | 
            +
              describe "#name" do
         | 
| 162 | 
            +
                it "is the class name of the job that was enqueued" do
         | 
| 163 | 
            +
                  expect(Delayed::Job.create(payload_object: ErrorJob.new).name).to eq("ErrorJob")
         | 
| 141 164 | 
             
                end
         | 
| 142 165 |  | 
| 143 | 
            -
                it " | 
| 166 | 
            +
                it "is the method that will be called if its a performable method object" do
         | 
| 144 167 | 
             
                  @job = Story.delay(ignore_transaction: true).create
         | 
| 145 | 
            -
                  @job.name. | 
| 168 | 
            +
                  expect(@job.name).to eq("Story.create")
         | 
| 146 169 | 
             
                end
         | 
| 147 170 |  | 
| 148 | 
            -
                it " | 
| 149 | 
            -
                  @job = Story.create(: | 
| 150 | 
            -
                  @job.name. | 
| 171 | 
            +
                it "is the instance method that will be called if its a performable method object" do
         | 
| 172 | 
            +
                  @job = Story.create(text: "...").delay(ignore_transaction: true).save
         | 
| 173 | 
            +
                  expect(@job.name).to eq("Story#save")
         | 
| 151 174 | 
             
                end
         | 
| 152 175 | 
             
              end
         | 
| 153 176 |  | 
| 154 177 | 
             
              context "worker prioritization" do
         | 
| 155 | 
            -
                it " | 
| 156 | 
            -
                  10.times { create_job : | 
| 178 | 
            +
                it "fetches jobs ordered by priority" do
         | 
| 179 | 
            +
                  10.times { create_job priority: rand(10) }
         | 
| 157 180 | 
             
                  jobs = Delayed::Job.find_available(10)
         | 
| 158 | 
            -
                  jobs.size. | 
| 181 | 
            +
                  expect(jobs.size).to eq(10)
         | 
| 159 182 | 
             
                  jobs.each_cons(2) do |a, b|
         | 
| 160 | 
            -
                    a.priority. | 
| 183 | 
            +
                    expect(a.priority).to be <= b.priority
         | 
| 161 184 | 
             
                  end
         | 
| 162 185 | 
             
                end
         | 
| 163 186 |  | 
| 164 | 
            -
                it " | 
| 165 | 
            -
                   | 
| 166 | 
            -
                  found = Delayed::Job.get_and_lock_next_available( | 
| 167 | 
            -
                  found. | 
| 168 | 
            -
                  job2 = create_job : | 
| 169 | 
            -
                  found = Delayed::Job.get_and_lock_next_available( | 
| 170 | 
            -
                  found. | 
| 171 | 
            -
                  job3 = create_job : | 
| 172 | 
            -
                  found = Delayed::Job.get_and_lock_next_available( | 
| 173 | 
            -
                  found. | 
| 174 | 
            -
                end
         | 
| 175 | 
            -
             | 
| 176 | 
            -
                it " | 
| 177 | 
            -
                   | 
| 178 | 
            -
                  found = Delayed::Job.get_and_lock_next_available( | 
| 179 | 
            -
                  found. | 
| 180 | 
            -
                  job2 = create_job : | 
| 181 | 
            -
                  found = Delayed::Job.get_and_lock_next_available( | 
| 182 | 
            -
                  found. | 
| 183 | 
            -
                  job3 = create_job : | 
| 184 | 
            -
                  found = Delayed::Job.get_and_lock_next_available( | 
| 185 | 
            -
                  found. | 
| 187 | 
            +
                it "does not find jobs lower than the given priority" do
         | 
| 188 | 
            +
                  create_job priority: 5
         | 
| 189 | 
            +
                  found = Delayed::Job.get_and_lock_next_available("test1", Delayed::Settings.queue, 10, 20)
         | 
| 190 | 
            +
                  expect(found).to be_nil
         | 
| 191 | 
            +
                  job2 = create_job priority: 10
         | 
| 192 | 
            +
                  found = Delayed::Job.get_and_lock_next_available("test1", Delayed::Settings.queue, 10, 20)
         | 
| 193 | 
            +
                  expect(found).to eq(job2)
         | 
| 194 | 
            +
                  job3 = create_job priority: 15
         | 
| 195 | 
            +
                  found = Delayed::Job.get_and_lock_next_available("test2", Delayed::Settings.queue, 10, 20)
         | 
| 196 | 
            +
                  expect(found).to eq(job3)
         | 
| 197 | 
            +
                end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                it "does not find jobs higher than the given priority" do
         | 
| 200 | 
            +
                  create_job priority: 25
         | 
| 201 | 
            +
                  found = Delayed::Job.get_and_lock_next_available("test1", Delayed::Settings.queue, 10, 20)
         | 
| 202 | 
            +
                  expect(found).to be_nil
         | 
| 203 | 
            +
                  job2 = create_job priority: 20
         | 
| 204 | 
            +
                  found = Delayed::Job.get_and_lock_next_available("test1", Delayed::Settings.queue, 10, 20)
         | 
| 205 | 
            +
                  expect(found).to eq(job2)
         | 
| 206 | 
            +
                  job3 = create_job priority: 15
         | 
| 207 | 
            +
                  found = Delayed::Job.get_and_lock_next_available("test2", Delayed::Settings.queue, 10, 20)
         | 
| 208 | 
            +
                  expect(found).to eq(job3)
         | 
| 186 209 | 
             
                end
         | 
| 187 210 | 
             
              end
         | 
| 188 211 |  | 
| 189 212 | 
             
              context "clear_locks!" do
         | 
| 190 213 | 
             
                before do
         | 
| 191 | 
            -
                  @job = create_job(: | 
| 214 | 
            +
                  @job = create_job(locked_by: "worker", locked_at: Delayed::Job.db_time_now)
         | 
| 192 215 | 
             
                end
         | 
| 193 216 |  | 
| 194 | 
            -
                it " | 
| 195 | 
            -
                  Delayed::Job.clear_locks!( | 
| 196 | 
            -
                  Delayed::Job.find_available(5). | 
| 217 | 
            +
                it "clears locks for the given worker" do
         | 
| 218 | 
            +
                  Delayed::Job.clear_locks!("worker")
         | 
| 219 | 
            +
                  expect(Delayed::Job.find_available(5)).to include(@job)
         | 
| 197 220 | 
             
                end
         | 
| 198 221 |  | 
| 199 | 
            -
                it " | 
| 200 | 
            -
                  Delayed::Job.clear_locks!( | 
| 201 | 
            -
                  Delayed::Job.find_available(5). | 
| 222 | 
            +
                it "does not clear locks for other workers" do
         | 
| 223 | 
            +
                  Delayed::Job.clear_locks!("worker1")
         | 
| 224 | 
            +
                  expect(Delayed::Job.find_available(5)).not_to include(@job)
         | 
| 202 225 | 
             
                end
         | 
| 203 226 | 
             
              end
         | 
| 204 227 |  | 
| 205 228 | 
             
              context "unlock" do
         | 
| 206 229 | 
             
                before do
         | 
| 207 | 
            -
                  @job = create_job(: | 
| 230 | 
            +
                  @job = create_job(locked_by: "worker", locked_at: Delayed::Job.db_time_now)
         | 
| 208 231 | 
             
                end
         | 
| 209 232 |  | 
| 210 | 
            -
                it " | 
| 233 | 
            +
                it "clears locks" do
         | 
| 211 234 | 
             
                  @job.unlock
         | 
| 212 | 
            -
                  @job.locked_by. | 
| 213 | 
            -
                  @job.locked_at. | 
| 235 | 
            +
                  expect(@job.locked_by).to be_nil
         | 
| 236 | 
            +
                  expect(@job.locked_at).to be_nil
         | 
| 214 237 | 
             
                end
         | 
| 215 238 |  | 
| 216 239 | 
             
                it "clears locks from multiple jobs" do
         | 
| 217 | 
            -
                  job2 = create_job(: | 
| 240 | 
            +
                  job2 = create_job(locked_by: "worker", locked_at: Delayed::Job.db_time_now)
         | 
| 218 241 | 
             
                  Delayed::Job.unlock([@job, job2])
         | 
| 219 242 | 
             
                  expect(@job.locked_at).to be_nil
         | 
| 220 243 | 
             
                  expect(job2.locked_at).to be_nil
         | 
| @@ -225,295 +248,621 @@ shared_examples_for 'a backend' do | |
| 225 248 |  | 
| 226 249 | 
             
              describe "#transfer_lock" do
         | 
| 227 250 | 
             
                it "works" do
         | 
| 228 | 
            -
                  job = create_job(: | 
| 229 | 
            -
                  expect(job.transfer_lock!(from:  | 
| 230 | 
            -
                  expect(Delayed::Job.find(job.id).locked_by).to eq  | 
| 251 | 
            +
                  job = create_job(locked_by: "worker", locked_at: Delayed::Job.db_time_now)
         | 
| 252 | 
            +
                  expect(job.transfer_lock!(from: "worker", to: "worker2")).to be true
         | 
| 253 | 
            +
                  expect(Delayed::Job.find(job.id).locked_by).to eq "worker2"
         | 
| 231 254 | 
             
                end
         | 
| 232 255 | 
             
              end
         | 
| 233 256 |  | 
| 234 257 | 
             
              context "strands" do
         | 
| 235 | 
            -
                it " | 
| 236 | 
            -
                  job1 = create_job(: | 
| 237 | 
            -
                  job2 = create_job(: | 
| 238 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 239 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 258 | 
            +
                it "runs strand jobs in strict order" do
         | 
| 259 | 
            +
                  job1 = create_job(strand: "myjobs")
         | 
| 260 | 
            +
                  job2 = create_job(strand: "myjobs")
         | 
| 261 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
         | 
| 262 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w2")).to be_nil
         | 
| 240 263 | 
             
                  job1.destroy
         | 
| 241 264 | 
             
                  # update time since the failed lock pushed it forward
         | 
| 242 265 | 
             
                  job2.run_at = 1.minute.ago
         | 
| 243 266 | 
             
                  job2.save!
         | 
| 244 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 245 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 267 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w3")).to eq(job2)
         | 
| 268 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w4")).to be_nil
         | 
| 246 269 | 
             
                end
         | 
| 247 270 |  | 
| 248 | 
            -
                it " | 
| 249 | 
            -
                  job1 = create_job(: | 
| 250 | 
            -
                  job2 = create_job(: | 
| 251 | 
            -
                  Delayed::Job.find_available(2). | 
| 252 | 
            -
                  Delayed::Job.find_available(2). | 
| 271 | 
            +
                it "fails to lock if an earlier job gets locked" do
         | 
| 272 | 
            +
                  job1 = create_job(strand: "myjobs")
         | 
| 273 | 
            +
                  job2 = create_job(strand: "myjobs")
         | 
| 274 | 
            +
                  expect(Delayed::Job.find_available(2)).to eq([job1])
         | 
| 275 | 
            +
                  expect(Delayed::Job.find_available(2)).to eq([job1])
         | 
| 253 276 |  | 
| 254 277 | 
             
                  # job1 gets locked by w1
         | 
| 255 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 278 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
         | 
| 256 279 |  | 
| 257 280 | 
             
                  # normally w2 would now be able to lock job2, but strands prevent it
         | 
| 258 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 281 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w2")).to be_nil
         | 
| 259 282 |  | 
| 260 283 | 
             
                  # now job1 is done
         | 
| 261 284 | 
             
                  job1.destroy
         | 
| 262 285 | 
             
                  # update time since the failed lock pushed it forward
         | 
| 263 286 | 
             
                  job2.run_at = 1.minute.ago
         | 
| 264 287 | 
             
                  job2.save!
         | 
| 265 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 288 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w2")).to eq(job2)
         | 
| 266 289 | 
             
                end
         | 
| 267 290 |  | 
| 268 | 
            -
                it " | 
| 269 | 
            -
                  job1 = create_job(: | 
| 270 | 
            -
                  job2 = create_job(: | 
| 271 | 
            -
                  job3 = create_job(: | 
| 272 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 273 | 
            -
                  Delayed::Job.find_available(1). | 
| 291 | 
            +
                it "keeps strand jobs in order as they are rescheduled" do
         | 
| 292 | 
            +
                  job1 = create_job(strand: "myjobs")
         | 
| 293 | 
            +
                  job2 = create_job(strand: "myjobs")
         | 
| 294 | 
            +
                  job3 = create_job(strand: "myjobs")
         | 
| 295 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
         | 
| 296 | 
            +
                  expect(Delayed::Job.find_available(1)).to eq([])
         | 
| 274 297 | 
             
                  job1.destroy
         | 
| 275 | 
            -
                  Delayed::Job.find_available(1). | 
| 298 | 
            +
                  expect(Delayed::Job.find_available(1)).to eq([job2])
         | 
| 276 299 | 
             
                  # move job2's time forward
         | 
| 277 300 | 
             
                  job2.run_at = 1.second.ago
         | 
| 278 301 | 
             
                  job2.save!
         | 
| 279 302 | 
             
                  job3.run_at = 5.seconds.ago
         | 
| 280 303 | 
             
                  job3.save!
         | 
| 281 304 | 
             
                  # we should still get job2, not job3
         | 
| 282 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 305 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job2)
         | 
| 283 306 | 
             
                end
         | 
| 284 307 |  | 
| 285 | 
            -
                it " | 
| 286 | 
            -
                  job1 = create_job(: | 
| 287 | 
            -
                  job2 = create_job(: | 
| 308 | 
            +
                it "allows to run the next job if a failed job is present" do
         | 
| 309 | 
            +
                  job1 = create_job(strand: "myjobs")
         | 
| 310 | 
            +
                  job2 = create_job(strand: "myjobs")
         | 
| 288 311 | 
             
                  job1.fail!
         | 
| 289 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 312 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job2)
         | 
| 290 313 | 
             
                end
         | 
| 291 314 |  | 
| 292 | 
            -
                it " | 
| 293 | 
            -
                  jobs = [create_job(: | 
| 294 | 
            -
                  locked = [Delayed::Job.get_and_lock_next_available( | 
| 295 | 
            -
                            Delayed::Job.get_and_lock_next_available( | 
| 296 | 
            -
                  jobs. | 
| 297 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 315 | 
            +
                it "does not interfere with jobs with no strand" do
         | 
| 316 | 
            +
                  jobs = [create_job(strand: nil), create_job(strand: "myjobs")]
         | 
| 317 | 
            +
                  locked = [Delayed::Job.get_and_lock_next_available("w1"),
         | 
| 318 | 
            +
                            Delayed::Job.get_and_lock_next_available("w2")]
         | 
| 319 | 
            +
                  expect(jobs).to eq locked
         | 
| 320 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w3")).to be_nil
         | 
| 298 321 | 
             
                end
         | 
| 299 322 |  | 
| 300 | 
            -
                it " | 
| 301 | 
            -
                  jobs = [create_job(: | 
| 302 | 
            -
                  locked = [Delayed::Job.get_and_lock_next_available( | 
| 303 | 
            -
                            Delayed::Job.get_and_lock_next_available( | 
| 304 | 
            -
                  jobs. | 
| 305 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 323 | 
            +
                it "does not interfere with jobs in other strands" do
         | 
| 324 | 
            +
                  jobs = [create_job(strand: "strand1"), create_job(strand: "strand2")]
         | 
| 325 | 
            +
                  locked = [Delayed::Job.get_and_lock_next_available("w1"),
         | 
| 326 | 
            +
                            Delayed::Job.get_and_lock_next_available("w2")]
         | 
| 327 | 
            +
                  expect(jobs).to eq locked
         | 
| 328 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w3")).to be_nil
         | 
| 306 329 | 
             
                end
         | 
| 307 330 |  | 
| 308 | 
            -
                it " | 
| 309 | 
            -
                  jobs = [create_job(: | 
| 310 | 
            -
                  first = Delayed::Job.get_and_lock_next_available( | 
| 311 | 
            -
                  second = Delayed::Job.get_and_lock_next_available( | 
| 331 | 
            +
                it "does not find next jobs when given no priority" do
         | 
| 332 | 
            +
                  jobs = [create_job(strand: "strand1"), create_job(strand: "strand1")]
         | 
| 333 | 
            +
                  first = Delayed::Job.get_and_lock_next_available("w1", Delayed::Settings.queue, nil, nil)
         | 
| 334 | 
            +
                  second = Delayed::Job.get_and_lock_next_available("w2", Delayed::Settings.queue, nil, nil)
         | 
| 312 335 | 
             
                  expect(first).to eq jobs.first
         | 
| 313 | 
            -
                  expect(second).to  | 
| 336 | 
            +
                  expect(second).to be_nil
         | 
| 314 337 | 
             
                end
         | 
| 315 338 |  | 
| 316 | 
            -
                 | 
| 317 | 
            -
                   | 
| 318 | 
            -
             | 
| 319 | 
            -
             | 
| 320 | 
            -
             | 
| 339 | 
            +
                it "complains if you pass more than one strand-based option" do
         | 
| 340 | 
            +
                  expect { create_job(strand: "a", n_strand: "b") }.to raise_error(ArgumentError)
         | 
| 341 | 
            +
                end
         | 
| 342 | 
            +
             | 
| 343 | 
            +
                context "singleton" do
         | 
| 344 | 
            +
                  it "creates if there's no jobs on the strand" do
         | 
| 345 | 
            +
                    @job = create_job(singleton: "myjobs")
         | 
| 346 | 
            +
                    expect(@job).to be_present
         | 
| 347 | 
            +
                    expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(@job)
         | 
| 321 348 | 
             
                  end
         | 
| 322 349 |  | 
| 323 | 
            -
                  it " | 
| 324 | 
            -
                    @job = create_job(: | 
| 325 | 
            -
                    @job. | 
| 326 | 
            -
                    Delayed::Job.get_and_lock_next_available( | 
| 350 | 
            +
                  it "creates if there's another job on the strand, but it's running" do
         | 
| 351 | 
            +
                    @job = create_job(singleton: "myjobs")
         | 
| 352 | 
            +
                    expect(@job).to be_present
         | 
| 353 | 
            +
                    expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(@job)
         | 
| 327 354 |  | 
| 328 | 
            -
                    @job2 = create_job(: | 
| 329 | 
            -
                    @job. | 
| 330 | 
            -
                    @job2. | 
| 355 | 
            +
                    @job2 = create_job(singleton: "myjobs")
         | 
| 356 | 
            +
                    expect(@job).to be_present
         | 
| 357 | 
            +
                    expect(@job2).not_to eq(@job)
         | 
| 331 358 | 
             
                  end
         | 
| 332 359 |  | 
| 333 | 
            -
                  it " | 
| 334 | 
            -
                    @job = create_job(: | 
| 335 | 
            -
                    @job. | 
| 360 | 
            +
                  it "does not create if there's another non-running job on the strand" do
         | 
| 361 | 
            +
                    @job = create_job(singleton: "myjobs")
         | 
| 362 | 
            +
                    expect(@job).to be_present
         | 
| 336 363 |  | 
| 337 | 
            -
                    @job2 = create_job(: | 
| 338 | 
            -
                    @job2. | 
| 364 | 
            +
                    @job2 = create_job(singleton: "myjobs")
         | 
| 365 | 
            +
                    expect(@job2).to be_new_record
         | 
| 339 366 | 
             
                  end
         | 
| 340 367 |  | 
| 341 | 
            -
                  it " | 
| 342 | 
            -
                    @job = create_job(: | 
| 343 | 
            -
                    @job. | 
| 344 | 
            -
                    Delayed::Job.get_and_lock_next_available( | 
| 368 | 
            +
                  it "does not create if there's a job running and one waiting on the strand" do
         | 
| 369 | 
            +
                    @job = create_job(singleton: "myjobs")
         | 
| 370 | 
            +
                    expect(@job).to be_present
         | 
| 371 | 
            +
                    expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(@job)
         | 
| 345 372 |  | 
| 346 | 
            -
                    @job2 = create_job(: | 
| 347 | 
            -
                    @job2. | 
| 348 | 
            -
                    @job2. | 
| 373 | 
            +
                    @job2 = create_job(singleton: "myjobs")
         | 
| 374 | 
            +
                    expect(@job2).to be_present
         | 
| 375 | 
            +
                    expect(@job2).not_to eq(@job)
         | 
| 349 376 |  | 
| 350 | 
            -
                    @job3 = create_job(: | 
| 351 | 
            -
                    @job3. | 
| 377 | 
            +
                    @job3 = create_job(singleton: "myjobs")
         | 
| 378 | 
            +
                    expect(@job3).to be_new_record
         | 
| 352 379 | 
             
                  end
         | 
| 353 380 |  | 
| 354 | 
            -
                  it " | 
| 355 | 
            -
                    job1 = create_job(singleton:  | 
| 356 | 
            -
                    job2 = create_job(singleton:  | 
| 357 | 
            -
                    job2. | 
| 381 | 
            +
                  it "updates existing job if new job is set to run sooner" do
         | 
| 382 | 
            +
                    job1 = create_job(singleton: "myjobs", run_at: 1.hour.from_now)
         | 
| 383 | 
            +
                    job2 = create_job(singleton: "myjobs")
         | 
| 384 | 
            +
                    expect(job2).to eq(job1)
         | 
| 358 385 | 
             
                    # it should be scheduled to run immediately
         | 
| 359 | 
            -
                    Delayed::Job.get_and_lock_next_available( | 
| 386 | 
            +
                    expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
         | 
| 360 387 | 
             
                  end
         | 
| 361 388 |  | 
| 362 | 
            -
                  it " | 
| 389 | 
            +
                  it "updates existing job to a later date if requested" do
         | 
| 363 390 | 
             
                    t1 = 1.hour.from_now
         | 
| 364 391 | 
             
                    t2 = 2.hours.from_now
         | 
| 365 | 
            -
                    job1 = create_job(singleton:  | 
| 366 | 
            -
                    job2 = create_job(singleton:  | 
| 367 | 
            -
                    job2. | 
| 368 | 
            -
             | 
| 369 | 
            -
             | 
| 370 | 
            -
                    job3  | 
| 371 | 
            -
                    job3. | 
| 372 | 
            -
                    job3.run_at.to_i.should == t2.to_i
         | 
| 392 | 
            +
                    job1 = create_job(singleton: "myjobs", run_at: t1)
         | 
| 393 | 
            +
                    job2 = create_job(singleton: "myjobs", run_at: t2)
         | 
| 394 | 
            +
                    expect(job2).to be_new_record
         | 
| 395 | 
            +
             | 
| 396 | 
            +
                    job3 = create_job(singleton: "myjobs", run_at: t2, on_conflict: :overwrite)
         | 
| 397 | 
            +
                    expect(job3).to eq(job1)
         | 
| 398 | 
            +
                    expect(job3.run_at.to_i).to eq(t2.to_i)
         | 
| 373 399 | 
             
                  end
         | 
| 374 400 |  | 
| 375 | 
            -
                  it " | 
| 376 | 
            -
                    job1 = Delayed::Job.enqueue(SimpleJob.new, queue: nil, singleton:  | 
| 377 | 
            -
                    job2 = Delayed::Job.enqueue(ErrorJob.new, queue: nil, singleton:  | 
| 378 | 
            -
                    job2. | 
| 379 | 
            -
                    expect( | 
| 401 | 
            +
                  it "updates existing singleton job handler if requested" do
         | 
| 402 | 
            +
                    job1 = Delayed::Job.enqueue(SimpleJob.new, queue: nil, singleton: "myjobs", on_conflict: :overwrite)
         | 
| 403 | 
            +
                    job2 = Delayed::Job.enqueue(ErrorJob.new, queue: nil, singleton: "myjobs", on_conflict: :overwrite)
         | 
| 404 | 
            +
                    expect(job2).to eq(job1)
         | 
| 405 | 
            +
                    expect(job1.reload.handler).to include("ErrorJob")
         | 
| 380 406 | 
             
                  end
         | 
| 381 407 |  | 
| 382 | 
            -
                   | 
| 383 | 
            -
                     | 
| 384 | 
            -
                     | 
| 385 | 
            -
                     | 
| 386 | 
            -
             | 
| 387 | 
            -
                     | 
| 408 | 
            +
                  context "next_in_strand management - deadlocks and race conditions", non_transactional: true, slow: true do
         | 
| 409 | 
            +
                    # The following unit tests are fairly slow and non-deterministic. It may be
         | 
| 410 | 
            +
                    # easier to make them fail quicker and more consistently by adding a random
         | 
| 411 | 
            +
                    # sleep into the appropriate trigger(s).
         | 
| 412 | 
            +
             | 
| 413 | 
            +
                    def loop_secs(val)
         | 
| 414 | 
            +
                      loop_start = Time.now.utc
         | 
| 415 | 
            +
             | 
| 416 | 
            +
                      loop do
         | 
| 417 | 
            +
                        break if Time.now.utc >= loop_start + val
         | 
| 418 | 
            +
             | 
| 419 | 
            +
                        yield
         | 
| 420 | 
            +
                      end
         | 
| 421 | 
            +
                    end
         | 
| 422 | 
            +
             | 
| 423 | 
            +
                    def loop_until_found(params)
         | 
| 424 | 
            +
                      found = false
         | 
| 425 | 
            +
             | 
| 426 | 
            +
                      loop_secs(10.seconds) do
         | 
| 427 | 
            +
                        if Delayed::Job.exists?(**params)
         | 
| 428 | 
            +
                          found = true
         | 
| 429 | 
            +
                          break
         | 
| 430 | 
            +
                        end
         | 
| 431 | 
            +
                      end
         | 
| 432 | 
            +
             | 
| 433 | 
            +
                      raise "timed out waiting for condition" unless found
         | 
| 434 | 
            +
                    end
         | 
| 435 | 
            +
             | 
| 436 | 
            +
                    def thread_body
         | 
| 437 | 
            +
                      yield
         | 
| 438 | 
            +
                    rescue
         | 
| 439 | 
            +
                      Thread.current.thread_variable_set(:fail, true)
         | 
| 440 | 
            +
                      raise
         | 
| 441 | 
            +
                    end
         | 
| 442 | 
            +
             | 
| 443 | 
            +
                    it "doesn't orphan the singleton when two are queued consecutively" do
         | 
| 444 | 
            +
                      # In order to reproduce this one efficiently, you'll probably want to add
         | 
| 445 | 
            +
                      # a sleep within delayed_jobs_before_insert_row_tr_fn.
         | 
| 446 | 
            +
                      # IF NEW.singleton IS NOT NULL THEN
         | 
| 447 | 
            +
                      #   ...
         | 
| 448 | 
            +
                      #   PERFORM pg_sleep(random() * 2);
         | 
| 449 | 
            +
                      # END IF;
         | 
| 450 | 
            +
             | 
| 451 | 
            +
                      threads = []
         | 
| 452 | 
            +
             | 
| 453 | 
            +
                      threads << Thread.new do
         | 
| 454 | 
            +
                        thread_body do
         | 
| 455 | 
            +
                          loop do
         | 
| 456 | 
            +
                            create_job(singleton: "singleton_job")
         | 
| 457 | 
            +
                            create_job(singleton: "singleton_job")
         | 
| 458 | 
            +
                          end
         | 
| 459 | 
            +
                        end
         | 
| 460 | 
            +
                      end
         | 
| 461 | 
            +
             | 
| 462 | 
            +
                      threads << Thread.new do
         | 
| 463 | 
            +
                        thread_body do
         | 
| 464 | 
            +
                          loop do
         | 
| 465 | 
            +
                            Delayed::Job.get_and_lock_next_available("w1")&.destroy
         | 
| 466 | 
            +
                          end
         | 
| 467 | 
            +
                        end
         | 
| 468 | 
            +
                      end
         | 
| 469 | 
            +
             | 
| 470 | 
            +
                      threads << Thread.new do
         | 
| 471 | 
            +
                        thread_body do
         | 
| 472 | 
            +
                          loop do
         | 
| 473 | 
            +
                            loop_until_found(singleton: "singleton_job", next_in_strand: true)
         | 
| 474 | 
            +
                          end
         | 
| 475 | 
            +
                        end
         | 
| 476 | 
            +
                      end
         | 
| 477 | 
            +
             | 
| 478 | 
            +
                      begin
         | 
| 479 | 
            +
                        loop_secs(60.seconds) do
         | 
| 480 | 
            +
                          if threads.any? { |x| x.thread_variable_get(:fail) }
         | 
| 481 | 
            +
                            raise "at least one job became orphaned or other error"
         | 
| 482 | 
            +
                          end
         | 
| 483 | 
            +
                        end
         | 
| 484 | 
            +
                      ensure
         | 
| 485 | 
            +
                        threads.each(&:kill)
         | 
| 486 | 
            +
                        threads.each(&:join)
         | 
| 487 | 
            +
                      end
         | 
| 488 | 
            +
                    end
         | 
| 489 | 
            +
             | 
| 490 | 
            +
                    it "doesn't deadlock when transitioning from strand_a to strand_b" do
         | 
| 491 | 
            +
                      # In order to reproduce this one efficiently, you'll probably want to add
         | 
| 492 | 
            +
                      # a sleep within delayed_jobs_after_delete_row_tr_fn.
         | 
| 493 | 
            +
                      # PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
         | 
| 494 | 
            +
                      # PERFORM pg_sleep(random() * 2);
         | 
| 495 | 
            +
             | 
| 496 | 
            +
                      threads = []
         | 
| 497 | 
            +
             | 
| 498 | 
            +
                      threads << Thread.new do
         | 
| 499 | 
            +
                        thread_body do
         | 
| 500 | 
            +
                          loop do
         | 
| 501 | 
            +
                            j1 = create_job(singleton: "myjobs", strand: "myjobs2", locked_by: "w1")
         | 
| 502 | 
            +
                            j2 = create_job(singleton: "myjobs", strand: "myjobs")
         | 
| 503 | 
            +
             | 
| 504 | 
            +
                            j1.delete
         | 
| 505 | 
            +
                            j2.delete
         | 
| 506 | 
            +
                          end
         | 
| 507 | 
            +
                        end
         | 
| 508 | 
            +
                      end
         | 
| 509 | 
            +
             | 
| 510 | 
            +
                      threads << Thread.new do
         | 
| 511 | 
            +
                        thread_body do
         | 
| 512 | 
            +
                          loop do
         | 
| 513 | 
            +
                            j1 = create_job(singleton: "myjobs2", strand: "myjobs", locked_by: "w1")
         | 
| 514 | 
            +
                            j2 = create_job(singleton: "myjobs2", strand: "myjobs2")
         | 
| 515 | 
            +
             | 
| 516 | 
            +
                            j1.delete
         | 
| 517 | 
            +
                            j2.delete
         | 
| 518 | 
            +
                          end
         | 
| 519 | 
            +
                        end
         | 
| 520 | 
            +
                      end
         | 
| 521 | 
            +
             | 
| 522 | 
            +
                      threads << Thread.new do
         | 
| 523 | 
            +
                        thread_body do
         | 
| 524 | 
            +
                          loop do
         | 
| 525 | 
            +
                            loop_until_found(singleton: "myjobs", next_in_strand: true)
         | 
| 526 | 
            +
                          end
         | 
| 527 | 
            +
                        end
         | 
| 528 | 
            +
                      end
         | 
| 529 | 
            +
             | 
| 530 | 
            +
                      threads << Thread.new do
         | 
| 531 | 
            +
                        thread_body do
         | 
| 532 | 
            +
                          loop do
         | 
| 533 | 
            +
                            loop_until_found(singleton: "myjobs2", next_in_strand: true)
         | 
| 534 | 
            +
                          end
         | 
| 535 | 
            +
                        end
         | 
| 536 | 
            +
                      end
         | 
| 537 | 
            +
             | 
| 538 | 
            +
                      begin
         | 
| 539 | 
            +
                        loop_secs(60.seconds) do
         | 
| 540 | 
            +
                          if threads.any? { |x| x.thread_variable_get(:fail) }
         | 
| 541 | 
            +
                            raise "at least one thread hit a deadlock or other error"
         | 
| 542 | 
            +
                          end
         | 
| 543 | 
            +
                        end
         | 
| 544 | 
            +
                      ensure
         | 
| 545 | 
            +
                        threads.each(&:kill)
         | 
| 546 | 
            +
                        threads.each(&:join)
         | 
| 547 | 
            +
                      end
         | 
| 548 | 
            +
                    end
         | 
| 549 | 
            +
                  end
         | 
| 550 | 
            +
             | 
| 551 | 
            +
                  context "next_in_strand management" do
         | 
| 552 | 
            +
                    it "handles transitions correctly when going from stranded to not stranded" do
         | 
| 553 | 
            +
                      @job1 = create_job(singleton: "myjobs", strand: "myjobs")
         | 
| 554 | 
            +
                      Delayed::Job.get_and_lock_next_available("w1")
         | 
| 555 | 
            +
                      @job2 = create_job(singleton: "myjobs")
         | 
| 556 | 
            +
             | 
| 557 | 
            +
                      expect(@job1.reload.next_in_strand).to be true
         | 
| 558 | 
            +
                      expect(@job2.reload.next_in_strand).to be false
         | 
| 559 | 
            +
             | 
| 560 | 
            +
                      @job1.destroy
         | 
| 561 | 
            +
                      expect(@job2.reload.next_in_strand).to be true
         | 
| 562 | 
            +
                    end
         | 
| 563 | 
            +
             | 
| 564 | 
            +
                    it "handles transitions correctly when going from not stranded to stranded" do
         | 
| 565 | 
            +
                      @job1 = create_job(singleton: "myjobs2", strand: "myjobs")
         | 
| 566 | 
            +
                      @job2 = create_job(singleton: "myjobs")
         | 
| 567 | 
            +
                      Delayed::Job.get_and_lock_next_available("w1")
         | 
| 568 | 
            +
                      Delayed::Job.get_and_lock_next_available("w1")
         | 
| 569 | 
            +
                      @job3 = create_job(singleton: "myjobs", strand: "myjobs2")
         | 
| 570 | 
            +
             | 
| 571 | 
            +
                      expect(@job1.reload.next_in_strand).to be true
         | 
| 572 | 
            +
                      expect(@job2.reload.next_in_strand).to be true
         | 
| 573 | 
            +
                      expect(@job3.reload.next_in_strand).to be false
         | 
| 574 | 
            +
             | 
| 575 | 
            +
                      @job2.destroy
         | 
| 576 | 
            +
                      expect(@job1.reload.next_in_strand).to be true
         | 
| 577 | 
            +
                      expect(@job3.reload.next_in_strand).to be true
         | 
| 578 | 
            +
                    end
         | 
| 579 | 
            +
             | 
| 580 | 
            +
                    it "does not violate n_strand=1 constraints when going from not stranded to stranded" do
         | 
| 581 | 
            +
                      @job1 = create_job(singleton: "myjobs2", strand: "myjobs")
         | 
| 582 | 
            +
                      @job2 = create_job(singleton: "myjobs")
         | 
| 583 | 
            +
                      Delayed::Job.get_and_lock_next_available("w1")
         | 
| 584 | 
            +
                      Delayed::Job.get_and_lock_next_available("w1")
         | 
| 585 | 
            +
                      @job3 = create_job(singleton: "myjobs", strand: "myjobs")
         | 
| 586 | 
            +
             | 
| 587 | 
            +
                      expect(@job1.reload.next_in_strand).to be true
         | 
| 588 | 
            +
                      expect(@job2.reload.next_in_strand).to be true
         | 
| 589 | 
            +
                      expect(@job3.reload.next_in_strand).to be false
         | 
| 590 | 
            +
             | 
| 591 | 
            +
                      @job2.destroy
         | 
| 592 | 
            +
                      expect(@job1.reload.next_in_strand).to be true
         | 
| 593 | 
            +
                      expect(@job3.reload.next_in_strand).to be false
         | 
| 594 | 
            +
                    end
         | 
| 595 | 
            +
             | 
| 596 | 
            +
                    it "handles transitions correctly when going from stranded to another strand" do
         | 
| 597 | 
            +
                      @job1 = create_job(singleton: "myjobs", strand: "myjobs")
         | 
| 598 | 
            +
                      Delayed::Job.get_and_lock_next_available("w1")
         | 
| 599 | 
            +
                      @job2 = create_job(singleton: "myjobs", strand: "myjobs2")
         | 
| 600 | 
            +
             | 
| 601 | 
            +
                      expect(@job1.reload.next_in_strand).to be true
         | 
| 602 | 
            +
                      expect(@job2.reload.next_in_strand).to be false
         | 
| 603 | 
            +
             | 
| 604 | 
            +
                      @job1.destroy
         | 
| 605 | 
            +
                      expect(@job2.reload.next_in_strand).to be true
         | 
| 606 | 
            +
                    end
         | 
| 607 | 
            +
             | 
| 608 | 
            +
                    it "does not violate n_strand=1 constraints when going from stranded to another strand" do
         | 
| 609 | 
            +
                      @job1 = create_job(singleton: "myjobs2", strand: "myjobs2")
         | 
| 610 | 
            +
                      @job2 = create_job(singleton: "myjobs", strand: "myjobs")
         | 
| 611 | 
            +
                      Delayed::Job.get_and_lock_next_available("w1")
         | 
| 612 | 
            +
                      Delayed::Job.get_and_lock_next_available("w1")
         | 
| 613 | 
            +
                      @job3 = create_job(singleton: "myjobs", strand: "myjobs2")
         | 
| 614 | 
            +
             | 
| 615 | 
            +
                      expect(@job1.reload.next_in_strand).to be true
         | 
| 616 | 
            +
                      expect(@job2.reload.next_in_strand).to be true
         | 
| 617 | 
            +
                      expect(@job3.reload.next_in_strand).to be false
         | 
| 618 | 
            +
             | 
| 619 | 
            +
                      @job2.destroy
         | 
| 620 | 
            +
                      expect(@job1.reload.next_in_strand).to be true
         | 
| 621 | 
            +
                      expect(@job3.reload.next_in_strand).to be false
         | 
| 622 | 
            +
                    end
         | 
| 623 | 
            +
             | 
| 624 | 
            +
                    it "creates first as true, and second as false, then transitions to second when deleted" do
         | 
| 625 | 
            +
                      @job1 = create_job(singleton: "myjobs")
         | 
| 626 | 
            +
                      Delayed::Job.get_and_lock_next_available("w1")
         | 
| 627 | 
            +
                      @job2 = create_job(singleton: "myjobs")
         | 
| 628 | 
            +
                      expect(@job1.reload.next_in_strand).to be true
         | 
| 629 | 
            +
                      expect(@job2.reload.next_in_strand).to be false
         | 
| 630 | 
            +
             | 
| 631 | 
            +
                      @job1.destroy
         | 
| 632 | 
            +
                      expect(@job2.reload.next_in_strand).to be true
         | 
| 633 | 
            +
                    end
         | 
| 634 | 
            +
             | 
| 635 | 
            +
                    it "when combined with a strand" do
         | 
| 636 | 
            +
                      job1 = create_job(singleton: "singleton", strand: "strand")
         | 
| 637 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job1
         | 
| 638 | 
            +
                      job2 = create_job(singleton: "singleton", strand: "strand")
         | 
| 639 | 
            +
                      expect(job2).not_to eq job1
         | 
| 640 | 
            +
                      expect(job2).not_to be_new_record
         | 
| 641 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 642 | 
            +
                      job3 = create_job(strand: "strand")
         | 
| 643 | 
            +
                      job4 = create_job(strand: "strand")
         | 
| 644 | 
            +
                      expect(job3.reload).not_to be_next_in_strand
         | 
| 645 | 
            +
                      expect(job4.reload).not_to be_next_in_strand
         | 
| 646 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 647 | 
            +
                      job1.destroy
         | 
| 648 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job2
         | 
| 649 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 650 | 
            +
                      job2.destroy
         | 
| 651 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job3
         | 
| 652 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 653 | 
            +
                      job3.destroy
         | 
| 654 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job4
         | 
| 655 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 656 | 
            +
                    end
         | 
| 657 | 
            +
             | 
| 658 | 
            +
                    it "when combined with a small n_strand" do
         | 
| 659 | 
            +
                      allow(Delayed::Settings).to receive(:num_strands).and_return(->(*) { 2 })
         | 
| 660 | 
            +
             | 
| 661 | 
            +
                      job1 = create_job(singleton: "singleton", n_strand: "strand")
         | 
| 662 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job1
         | 
| 663 | 
            +
                      job2 = create_job(singleton: "singleton", n_strand: "strand")
         | 
| 664 | 
            +
                      expect(job2).not_to eq job1
         | 
| 665 | 
            +
                      expect(job2).not_to be_new_record
         | 
| 666 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 667 | 
            +
                      job3 = create_job(n_strand: "strand")
         | 
| 668 | 
            +
                      job4 = create_job(n_strand: "strand")
         | 
| 669 | 
            +
                      expect(job3.reload).to be_next_in_strand
         | 
| 670 | 
            +
                      expect(job4.reload).not_to be_next_in_strand
         | 
| 671 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job3
         | 
| 672 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 673 | 
            +
                      # this doesn't unlock job2, even though it's ahead of job4
         | 
| 674 | 
            +
                      job3.destroy
         | 
| 675 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job4
         | 
| 676 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 677 | 
            +
                      job4.destroy
         | 
| 678 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 679 | 
            +
                      job1.destroy
         | 
| 680 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job2
         | 
| 681 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 682 | 
            +
                    end
         | 
| 683 | 
            +
             | 
| 684 | 
            +
                    it "when combined with a larger n_strand" do
         | 
| 685 | 
            +
                      allow(Delayed::Settings).to receive(:num_strands).and_return(->(*) { 10 })
         | 
| 686 | 
            +
             | 
| 687 | 
            +
                      job1 = create_job(singleton: "singleton", n_strand: "strand")
         | 
| 688 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job1
         | 
| 689 | 
            +
                      job2 = create_job(singleton: "singleton", n_strand: "strand")
         | 
| 690 | 
            +
                      expect(job2).not_to eq job1
         | 
| 691 | 
            +
                      expect(job2).not_to be_new_record
         | 
| 692 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 693 | 
            +
                      job3 = create_job(n_strand: "strand")
         | 
| 694 | 
            +
                      job4 = create_job(n_strand: "strand")
         | 
| 695 | 
            +
                      expect(job3.reload).to be_next_in_strand
         | 
| 696 | 
            +
                      expect(job4.reload).to be_next_in_strand
         | 
| 697 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job3
         | 
| 698 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job4
         | 
| 699 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 700 | 
            +
                      # this doesn't unlock job2
         | 
| 701 | 
            +
                      job3.destroy
         | 
| 702 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 703 | 
            +
                      job4.destroy
         | 
| 704 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 705 | 
            +
                      job1.destroy
         | 
| 706 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job2
         | 
| 707 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 708 | 
            +
                    end
         | 
| 709 | 
            +
                  end
         | 
| 710 | 
            +
             | 
| 711 | 
            +
                  context "with on_conflict: loose and strand-inferred-from-singleton" do
         | 
| 712 | 
            +
                    around do |example|
         | 
| 713 | 
            +
                      Delayed::Settings.infer_strand_from_singleton = true
         | 
| 714 | 
            +
                      example.call
         | 
| 715 | 
            +
                    ensure
         | 
| 716 | 
            +
                      Delayed::Settings.infer_strand_from_singleton = false
         | 
| 717 | 
            +
                    end
         | 
| 718 | 
            +
             | 
| 719 | 
            +
                    it "does not create if there's another non-running job on the strand" do
         | 
| 720 | 
            +
                      @job = create_job(singleton: "myjobs", on_conflict: :loose)
         | 
| 721 | 
            +
                      expect(@job).to be_present
         | 
| 722 | 
            +
             | 
| 723 | 
            +
                      @job2 = create_job(singleton: "myjobs", on_conflict: :loose)
         | 
| 724 | 
            +
                      expect(@job2).to be_new_record
         | 
| 725 | 
            +
                    end
         | 
| 726 | 
            +
                  end
         | 
| 727 | 
            +
             | 
| 728 | 
            +
                  context "when unlocking with another singleton pending" do
         | 
| 729 | 
            +
                    it "deletes the pending singleton" do
         | 
| 730 | 
            +
                      @job1 = create_job(singleton: "myjobs", max_attempts: 2)
         | 
| 731 | 
            +
                      expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(@job1)
         | 
| 732 | 
            +
             | 
| 733 | 
            +
                      @job2 = create_job(singleton: "myjobs", max_attempts: 2)
         | 
| 734 | 
            +
             | 
| 735 | 
            +
                      @job1.reload.reschedule
         | 
| 736 | 
            +
                      expect { @job1.reload }.not_to raise_error
         | 
| 737 | 
            +
                      expect { @job2.reload }.to raise_error(ActiveRecord::RecordNotFound)
         | 
| 738 | 
            +
                    end
         | 
| 388 739 | 
             
                  end
         | 
| 389 740 | 
             
                end
         | 
| 390 741 | 
             
              end
         | 
| 391 742 |  | 
| 392 743 | 
             
              context "on hold" do
         | 
| 393 | 
            -
                it " | 
| 394 | 
            -
                  job1 = create_job | 
| 744 | 
            +
                it "hold/unholds jobs" do
         | 
| 745 | 
            +
                  job1 = create_job
         | 
| 395 746 | 
             
                  job1.hold!
         | 
| 396 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 747 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
         | 
| 397 748 |  | 
| 398 749 | 
             
                  job1.unhold!
         | 
| 399 | 
            -
                  Delayed::Job.get_and_lock_next_available( | 
| 750 | 
            +
                  expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
         | 
| 400 751 | 
             
                end
         | 
| 401 752 | 
             
              end
         | 
| 402 753 |  | 
| 403 754 | 
             
              context "periodic jobs" do
         | 
| 404 | 
            -
                before | 
| 755 | 
            +
                before do
         | 
| 405 756 | 
             
                  # make the periodic job get scheduled in the past
         | 
| 406 757 | 
             
                  @cron_time = 10.minutes.ago
         | 
| 407 758 | 
             
                  allow(Delayed::Periodic).to receive(:now).and_return(@cron_time)
         | 
| 408 759 | 
             
                  Delayed::Periodic.scheduled = {}
         | 
| 409 | 
            -
                  Delayed::Periodic.cron( | 
| 760 | 
            +
                  Delayed::Periodic.cron("my SimpleJob", "*/5 * * * * *") do
         | 
| 410 761 | 
             
                    Delayed::Job.enqueue(SimpleJob.new)
         | 
| 411 762 | 
             
                  end
         | 
| 412 763 | 
             
                end
         | 
| 413 764 |  | 
| 414 | 
            -
                it " | 
| 415 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 765 | 
            +
                it "schedules jobs if they aren't scheduled yet" do
         | 
| 766 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(0)
         | 
| 416 767 | 
             
                  Delayed::Periodic.perform_audit!
         | 
| 417 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 418 | 
            -
                  job = Delayed::Job.get_and_lock_next_available( | 
| 419 | 
            -
                  job.tag. | 
| 420 | 
            -
                  job.payload_object. | 
| 421 | 
            -
                  job.run_at. | 
| 422 | 
            -
                  job.run_at. | 
| 423 | 
            -
                  job. | 
| 768 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 769 | 
            +
                  job = Delayed::Job.get_and_lock_next_available("test1")
         | 
| 770 | 
            +
                  expect(job.tag).to eq("periodic: my SimpleJob")
         | 
| 771 | 
            +
                  expect(job.payload_object).to eq(Delayed::Periodic.scheduled["my SimpleJob"])
         | 
| 772 | 
            +
                  expect(job.run_at).to be >= @cron_time
         | 
| 773 | 
            +
                  expect(job.run_at).to be <= @cron_time + 6.minutes
         | 
| 774 | 
            +
                  expect(job.singleton).to eq(job.tag)
         | 
| 424 775 | 
             
                end
         | 
| 425 776 |  | 
| 426 | 
            -
                it " | 
| 427 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 777 | 
            +
                it "schedules jobs if there are only failed jobs on the queue" do
         | 
| 778 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(0)
         | 
| 428 779 | 
             
                  expect { Delayed::Periodic.perform_audit! }.to change { Delayed::Job.jobs_count(:current) }.by(1)
         | 
| 429 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 430 | 
            -
                  job = Delayed::Job.get_and_lock_next_available( | 
| 780 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 781 | 
            +
                  job = Delayed::Job.get_and_lock_next_available("test1")
         | 
| 431 782 | 
             
                  job.fail!
         | 
| 432 | 
            -
                  expect { Delayed::Periodic.perform_audit! }.to change{ Delayed::Job.jobs_count(:current) }.by(1)
         | 
| 783 | 
            +
                  expect { Delayed::Periodic.perform_audit! }.to change { Delayed::Job.jobs_count(:current) }.by(1)
         | 
| 433 784 | 
             
                end
         | 
| 434 785 |  | 
| 435 | 
            -
                it " | 
| 436 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 786 | 
            +
                it "does not schedule jobs that are already scheduled" do
         | 
| 787 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(0)
         | 
| 437 788 | 
             
                  Delayed::Periodic.perform_audit!
         | 
| 438 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 789 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 439 790 | 
             
                  job = Delayed::Job.find_available(1).first
         | 
| 440 791 | 
             
                  Delayed::Periodic.perform_audit!
         | 
| 441 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 792 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 442 793 | 
             
                  # verify that the same job still exists, it wasn't just replaced with a new one
         | 
| 443 | 
            -
                  job. | 
| 794 | 
            +
                  expect(job).to eq(Delayed::Job.find_available(1).first)
         | 
| 444 795 | 
             
                end
         | 
| 445 796 |  | 
| 446 | 
            -
                it " | 
| 447 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 797 | 
            +
                it "schedules the next job run after performing" do
         | 
| 798 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(0)
         | 
| 448 799 | 
             
                  Delayed::Periodic.perform_audit!
         | 
| 449 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 450 | 
            -
                  job = Delayed::Job.get_and_lock_next_available( | 
| 800 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(1)
         | 
| 801 | 
            +
                  job = Delayed::Job.get_and_lock_next_available("test")
         | 
| 451 802 | 
             
                  run_job(job)
         | 
| 452 803 |  | 
| 453 | 
            -
                  job = Delayed::Job.get_and_lock_next_available( | 
| 454 | 
            -
                  job.tag. | 
| 804 | 
            +
                  job = Delayed::Job.get_and_lock_next_available("test1")
         | 
| 805 | 
            +
                  expect(job.tag).to eq("SimpleJob#perform")
         | 
| 455 806 |  | 
| 456 | 
            -
                  next_scheduled = Delayed::Job.get_and_lock_next_available( | 
| 457 | 
            -
                  next_scheduled.tag. | 
| 458 | 
            -
                  next_scheduled.payload_object. | 
| 807 | 
            +
                  next_scheduled = Delayed::Job.get_and_lock_next_available("test2")
         | 
| 808 | 
            +
                  expect(next_scheduled.tag).to eq("periodic: my SimpleJob")
         | 
| 809 | 
            +
                  expect(next_scheduled.payload_object).to be_is_a(Delayed::Periodic)
         | 
| 459 810 | 
             
                end
         | 
| 460 811 |  | 
| 461 | 
            -
                it " | 
| 462 | 
            -
                   | 
| 812 | 
            +
                it "rejects duplicate named jobs" do
         | 
| 813 | 
            +
                  expect { Delayed::Periodic.cron("my SimpleJob", "*/15 * * * * *") { nil } }.to raise_error(ArgumentError)
         | 
| 463 814 | 
             
                end
         | 
| 464 815 |  | 
| 465 | 
            -
                it " | 
| 816 | 
            +
                it "handles jobs that are no longer scheduled" do
         | 
| 466 817 | 
             
                  Delayed::Periodic.perform_audit!
         | 
| 467 818 | 
             
                  Delayed::Periodic.scheduled = {}
         | 
| 468 | 
            -
                  job = Delayed::Job.get_and_lock_next_available( | 
| 819 | 
            +
                  job = Delayed::Job.get_and_lock_next_available("test")
         | 
| 469 820 | 
             
                  run_job(job)
         | 
| 470 821 | 
             
                  # shouldn't error, and the job should now be deleted
         | 
| 471 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 822 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(0)
         | 
| 472 823 | 
             
                end
         | 
| 473 824 |  | 
| 474 | 
            -
                it " | 
| 475 | 
            -
                  change_setting(Delayed::Periodic, :overrides, {  | 
| 825 | 
            +
                it "allows overriding schedules using periodic_jobs.yml" do
         | 
| 826 | 
            +
                  change_setting(Delayed::Periodic, :overrides, { "my ChangedJob" => "*/10 * * * * *" }) do
         | 
| 476 827 | 
             
                    Delayed::Periodic.scheduled = {}
         | 
| 477 | 
            -
                    Delayed::Periodic.cron( | 
| 828 | 
            +
                    Delayed::Periodic.cron("my ChangedJob", "*/5 * * * * *") do
         | 
| 478 829 | 
             
                      Delayed::Job.enqueue(SimpleJob.new)
         | 
| 479 830 | 
             
                    end
         | 
| 480 | 
            -
                    Delayed::Periodic.scheduled[ | 
| 831 | 
            +
                    expect(Delayed::Periodic.scheduled["my ChangedJob"].cron.original).to eq("*/10 * * * * *")
         | 
| 481 832 | 
             
                  end
         | 
| 482 833 | 
             
                end
         | 
| 483 834 |  | 
| 484 | 
            -
                it " | 
| 485 | 
            -
                  change_setting(Delayed::Periodic, :overrides, {  | 
| 835 | 
            +
                it "fails if the override cron line is invalid" do
         | 
| 836 | 
            +
                  change_setting(Delayed::Periodic, :overrides, { "my ChangedJob" => "*/10 * * * * * *" }) do # extra asterisk
         | 
| 486 837 | 
             
                    Delayed::Periodic.scheduled = {}
         | 
| 487 | 
            -
                    expect  | 
| 488 | 
            -
                      Delayed:: | 
| 489 | 
            -
             | 
| 838 | 
            +
                    expect do
         | 
| 839 | 
            +
                      Delayed::Periodic.cron("my ChangedJob", "*/5 * * * * *") do
         | 
| 840 | 
            +
                        Delayed::Job.enqueue(SimpleJob.new)
         | 
| 841 | 
            +
                      end
         | 
| 842 | 
            +
                    end.to raise_error(ArgumentError)
         | 
| 490 843 | 
             
                  end
         | 
| 491 844 |  | 
| 492 | 
            -
                  expect  | 
| 493 | 
            -
             | 
| 494 | 
            -
             | 
| 495 | 
            -
             | 
| 496 | 
            -
              module InDelayedJobTest
         | 
| 497 | 
            -
                def self.check_in_job
         | 
| 498 | 
            -
                  Delayed::Job.in_delayed_job?.should == true
         | 
| 845 | 
            +
                  expect do
         | 
| 846 | 
            +
                    Delayed::Periodic.add_overrides({ "my ChangedJob" => "*/10 * * * * * *" })
         | 
| 847 | 
            +
                  end.to raise_error(ArgumentError)
         | 
| 499 848 | 
             
                end
         | 
| 500 849 | 
             
              end
         | 
| 501 850 |  | 
| 502 | 
            -
              it " | 
| 851 | 
            +
              it "sets in_delayed_job?" do
         | 
| 503 852 | 
             
                job = InDelayedJobTest.delay(ignore_transaction: true).check_in_job
         | 
| 504 | 
            -
                Delayed::Job.in_delayed_job | 
| 853 | 
            +
                expect(Delayed::Job.in_delayed_job?).to be(false)
         | 
| 505 854 | 
             
                job.invoke_job
         | 
| 506 | 
            -
                Delayed::Job.in_delayed_job | 
| 855 | 
            +
                expect(Delayed::Job.in_delayed_job?).to be(false)
         | 
| 507 856 | 
             
              end
         | 
| 508 857 |  | 
| 509 | 
            -
              it " | 
| 510 | 
            -
                story = Story.new : | 
| 511 | 
            -
                 | 
| 858 | 
            +
              it "fails on job creation if an unsaved AR object is used" do
         | 
| 859 | 
            +
                story = Story.new text: "Once upon..."
         | 
| 860 | 
            +
                expect { story.delay.text }.to raise_error(RuntimeError)
         | 
| 512 861 |  | 
| 513 862 | 
             
                reader = StoryReader.new
         | 
| 514 | 
            -
                 | 
| 863 | 
            +
                expect { reader.delay.read(story) }.to raise_error(RuntimeError)
         | 
| 515 864 |  | 
| 516 | 
            -
                 | 
| 865 | 
            +
                expect { [story, 1, story, false].delay.first }.to raise_error(RuntimeError)
         | 
| 517 866 | 
             
              end
         | 
| 518 867 |  | 
| 519 868 | 
             
              # the sort order of current_jobs and list_jobs depends on the back-end
         | 
| @@ -521,62 +870,62 @@ shared_examples_for 'a backend' do | |
| 521 870 | 
             
              describe "current jobs, queue size, strand_size" do
         | 
| 522 871 | 
             
                before do
         | 
| 523 872 | 
             
                  @jobs = []
         | 
| 524 | 
            -
                  3.times { @jobs << create_job(: | 
| 525 | 
            -
                  @jobs.unshift create_job(: | 
| 526 | 
            -
                  @jobs.unshift create_job(: | 
| 527 | 
            -
                  @jobs << create_job(: | 
| 528 | 
            -
                  @future_job = create_job(: | 
| 529 | 
            -
                  2.times { @jobs << create_job(: | 
| 530 | 
            -
                  @jobs << create_job(: | 
| 531 | 
            -
                  @failed_job = create_job.tap | 
| 532 | 
            -
                  @other_queue_job = create_job(: | 
| 873 | 
            +
                  3.times { @jobs << create_job(priority: 3) }
         | 
| 874 | 
            +
                  @jobs.unshift create_job(priority: 2)
         | 
| 875 | 
            +
                  @jobs.unshift create_job(priority: 1)
         | 
| 876 | 
            +
                  @jobs << create_job(priority: 3, strand: "test1")
         | 
| 877 | 
            +
                  @future_job = create_job(run_at: 5.hours.from_now)
         | 
| 878 | 
            +
                  2.times { @jobs << create_job(priority: 3) }
         | 
| 879 | 
            +
                  @jobs << create_job(priority: 3, strand: "test1")
         | 
| 880 | 
            +
                  @failed_job = create_job.tap(&:fail!)
         | 
| 881 | 
            +
                  @other_queue_job = create_job(queue: "another")
         | 
| 533 882 | 
             
                end
         | 
| 534 883 |  | 
| 535 | 
            -
                it " | 
| 536 | 
            -
                  Delayed::Job.list_jobs(:current, 100).map(&:id).sort. | 
| 884 | 
            +
                it "returns the queued jobs" do
         | 
| 885 | 
            +
                  expect(Delayed::Job.list_jobs(:current, 100).map(&:id).sort).to eq(@jobs.map(&:id).sort)
         | 
| 537 886 | 
             
                end
         | 
| 538 887 |  | 
| 539 | 
            -
                it " | 
| 888 | 
            +
                it "paginates the returned jobs" do
         | 
| 540 889 | 
             
                  @returned = []
         | 
| 541 890 | 
             
                  @returned += Delayed::Job.list_jobs(:current, 3, 0)
         | 
| 542 891 | 
             
                  @returned += Delayed::Job.list_jobs(:current, 4, 3)
         | 
| 543 892 | 
             
                  @returned += Delayed::Job.list_jobs(:current, 100, 7)
         | 
| 544 | 
            -
                  @returned.sort_by | 
| 893 | 
            +
                  expect(@returned.sort_by(&:id)).to eq(@jobs.sort_by(&:id))
         | 
| 545 894 | 
             
                end
         | 
| 546 895 |  | 
| 547 | 
            -
                it " | 
| 548 | 
            -
                  Delayed::Job.list_jobs(:current, 5, 0, "another"). | 
| 896 | 
            +
                it "returns other queues" do
         | 
| 897 | 
            +
                  expect(Delayed::Job.list_jobs(:current, 5, 0, "another")).to eq([@other_queue_job])
         | 
| 549 898 | 
             
                end
         | 
| 550 899 |  | 
| 551 | 
            -
                it " | 
| 552 | 
            -
                  Delayed::Job.jobs_count(:current). | 
| 553 | 
            -
                  Delayed::Job.jobs_count(:current, "another"). | 
| 554 | 
            -
                  Delayed::Job.jobs_count(:current, "bogus"). | 
| 900 | 
            +
                it "returns queue size" do
         | 
| 901 | 
            +
                  expect(Delayed::Job.jobs_count(:current)).to eq(@jobs.size)
         | 
| 902 | 
            +
                  expect(Delayed::Job.jobs_count(:current, "another")).to eq(1)
         | 
| 903 | 
            +
                  expect(Delayed::Job.jobs_count(:current, "bogus")).to eq(0)
         | 
| 555 904 | 
             
                end
         | 
| 556 905 |  | 
| 557 | 
            -
                it " | 
| 558 | 
            -
                  Delayed::Job.strand_size("test1"). | 
| 559 | 
            -
                  Delayed::Job.strand_size("bogus"). | 
| 906 | 
            +
                it "returns strand size" do
         | 
| 907 | 
            +
                  expect(Delayed::Job.strand_size("test1")).to eq(2)
         | 
| 908 | 
            +
                  expect(Delayed::Job.strand_size("bogus")).to eq(0)
         | 
| 560 909 | 
             
                end
         | 
| 561 910 | 
             
              end
         | 
| 562 911 |  | 
| 563 | 
            -
              it " | 
| 912 | 
            +
              it "returns the jobs in a strand" do
         | 
| 564 913 | 
             
                strand_jobs = []
         | 
| 565 | 
            -
                3.times { strand_jobs << create_job(: | 
| 566 | 
            -
                2.times { create_job(: | 
| 567 | 
            -
                strand_jobs << create_job(: | 
| 914 | 
            +
                3.times { strand_jobs << create_job(strand: "test1") }
         | 
| 915 | 
            +
                2.times { create_job(strand: "test2") }
         | 
| 916 | 
            +
                strand_jobs << create_job(strand: "test1", run_at: 5.hours.from_now)
         | 
| 568 917 | 
             
                create_job
         | 
| 569 918 |  | 
| 570 919 | 
             
                jobs = Delayed::Job.list_jobs(:strand, 3, 0, "test1")
         | 
| 571 | 
            -
                jobs.size. | 
| 920 | 
            +
                expect(jobs.size).to eq(3)
         | 
| 572 921 |  | 
| 573 922 | 
             
                jobs += Delayed::Job.list_jobs(:strand, 3, 3, "test1")
         | 
| 574 | 
            -
                jobs.size. | 
| 923 | 
            +
                expect(jobs.size).to eq(4)
         | 
| 575 924 |  | 
| 576 | 
            -
                jobs.sort_by | 
| 925 | 
            +
                expect(jobs.sort_by(&:id)).to eq(strand_jobs.sort_by(&:id))
         | 
| 577 926 | 
             
              end
         | 
| 578 927 |  | 
| 579 | 
            -
              it " | 
| 928 | 
            +
              it "returns the jobs for a tag" do
         | 
| 580 929 | 
             
                tag_jobs = []
         | 
| 581 930 | 
             
                3.times { tag_jobs << "test".delay(ignore_transaction: true).to_s }
         | 
| 582 931 | 
             
                2.times { "test".delay.to_i }
         | 
| @@ -586,62 +935,62 @@ shared_examples_for 'a backend' do | |
| 586 935 | 
             
                create_job
         | 
| 587 936 |  | 
| 588 937 | 
             
                jobs = Delayed::Job.list_jobs(:tag, 3, 0, "String#to_s")
         | 
| 589 | 
            -
                jobs.size. | 
| 938 | 
            +
                expect(jobs.size).to eq(3)
         | 
| 590 939 |  | 
| 591 940 | 
             
                jobs += Delayed::Job.list_jobs(:tag, 3, 3, "String#to_s")
         | 
| 592 | 
            -
                jobs.size. | 
| 941 | 
            +
                expect(jobs.size).to eq(5)
         | 
| 593 942 |  | 
| 594 | 
            -
                jobs.sort_by | 
| 943 | 
            +
                expect(jobs.sort_by(&:id)).to eq(tag_jobs.sort_by(&:id))
         | 
| 595 944 | 
             
              end
         | 
| 596 945 |  | 
| 597 946 | 
             
              describe "running_jobs" do
         | 
| 598 | 
            -
                it " | 
| 947 | 
            +
                it "returns the running jobs, ordered by locked_at" do
         | 
| 599 948 | 
             
                  Timecop.freeze(10.minutes.ago) { 3.times { create_job } }
         | 
| 600 | 
            -
                  j1 = Timecop.freeze(2.minutes.ago) { Delayed::Job.get_and_lock_next_available( | 
| 601 | 
            -
                  j2 = Timecop.freeze(5.minutes.ago) { Delayed::Job.get_and_lock_next_available( | 
| 602 | 
            -
                  j3 = Timecop.freeze(5.seconds.ago) { Delayed::Job.get_and_lock_next_available( | 
| 603 | 
            -
                  [j1, j2, j3].compact.size. | 
| 949 | 
            +
                  j1 = Timecop.freeze(2.minutes.ago) { Delayed::Job.get_and_lock_next_available("w1") }
         | 
| 950 | 
            +
                  j2 = Timecop.freeze(5.minutes.ago) { Delayed::Job.get_and_lock_next_available("w2") }
         | 
| 951 | 
            +
                  j3 = Timecop.freeze(5.seconds.ago) { Delayed::Job.get_and_lock_next_available("w3") }
         | 
| 952 | 
            +
                  expect([j1, j2, j3].compact.size).to eq(3)
         | 
| 604 953 |  | 
| 605 | 
            -
                  Delayed::Job.running_jobs. | 
| 954 | 
            +
                  expect(Delayed::Job.running_jobs).to eq([j2, j1, j3])
         | 
| 606 955 | 
             
                end
         | 
| 607 956 | 
             
              end
         | 
| 608 957 |  | 
| 609 958 | 
             
              describe "future jobs" do
         | 
| 610 | 
            -
                it " | 
| 611 | 
            -
                  Timecop.freeze  | 
| 612 | 
            -
                    @job = create_job : | 
| 959 | 
            +
                it "finds future jobs once their run_at rolls by" do
         | 
| 960 | 
            +
                  Timecop.freeze do
         | 
| 961 | 
            +
                    @job = create_job run_at: 5.minutes.from_now
         | 
| 613 962 | 
             
                    expect(Delayed::Job.find_available(5)).not_to include(@job)
         | 
| 614 | 
            -
                   | 
| 615 | 
            -
                  Timecop.freeze(1.hour.from_now)  | 
| 963 | 
            +
                  end
         | 
| 964 | 
            +
                  Timecop.freeze(1.hour.from_now) do
         | 
| 616 965 | 
             
                    expect(Delayed::Job.find_available(5)).to include(@job)
         | 
| 617 | 
            -
                    Delayed::Job.get_and_lock_next_available( | 
| 618 | 
            -
                   | 
| 966 | 
            +
                    expect(Delayed::Job.get_and_lock_next_available("test")).to eq(@job)
         | 
| 967 | 
            +
                  end
         | 
| 619 968 | 
             
                end
         | 
| 620 969 |  | 
| 621 | 
            -
                it " | 
| 970 | 
            +
                it "returns future jobs sorted by their run_at" do
         | 
| 622 971 | 
             
                  @j1 = create_job
         | 
| 623 | 
            -
                  @j2 = create_job : | 
| 624 | 
            -
                  @j3 = create_job : | 
| 625 | 
            -
                  Delayed::Job.list_jobs(:future, 1). | 
| 626 | 
            -
                  Delayed::Job.list_jobs(:future, 5). | 
| 627 | 
            -
                  Delayed::Job.list_jobs(:future, 1, 1). | 
| 972 | 
            +
                  @j2 = create_job run_at: 1.hour.from_now
         | 
| 973 | 
            +
                  @j3 = create_job run_at: 30.minutes.from_now
         | 
| 974 | 
            +
                  expect(Delayed::Job.list_jobs(:future, 1)).to eq([@j3])
         | 
| 975 | 
            +
                  expect(Delayed::Job.list_jobs(:future, 5)).to eq([@j3, @j2])
         | 
| 976 | 
            +
                  expect(Delayed::Job.list_jobs(:future, 1, 1)).to eq([@j2])
         | 
| 628 977 | 
             
                end
         | 
| 629 978 | 
             
              end
         | 
| 630 979 |  | 
| 631 980 | 
             
              describe "failed jobs" do
         | 
| 632 981 | 
             
                # the sort order of failed_jobs depends on the back-end implementation,
         | 
| 633 982 | 
             
                # so sort order isn't tested here
         | 
| 634 | 
            -
                it " | 
| 983 | 
            +
                it "returns the list of failed jobs" do
         | 
| 635 984 | 
             
                  jobs = []
         | 
| 636 | 
            -
                  3.times { jobs << create_job(: | 
| 637 | 
            -
                  jobs = jobs.sort_by | 
| 638 | 
            -
                  Delayed::Job.list_jobs(:failed, 1). | 
| 985 | 
            +
                  3.times { jobs << create_job(priority: 3) }
         | 
| 986 | 
            +
                  jobs = jobs.sort_by(&:id)
         | 
| 987 | 
            +
                  expect(Delayed::Job.list_jobs(:failed, 1)).to eq([])
         | 
| 639 988 | 
             
                  jobs[0].fail!
         | 
| 640 989 | 
             
                  jobs[1].fail!
         | 
| 641 | 
            -
                  failed = (Delayed::Job.list_jobs(:failed, 1, 0) + Delayed::Job.list_jobs(:failed, 1, 1)).sort_by | 
| 642 | 
            -
                  failed.size. | 
| 643 | 
            -
                  failed[0].original_job_id. | 
| 644 | 
            -
                  failed[1].original_job_id. | 
| 990 | 
            +
                  failed = (Delayed::Job.list_jobs(:failed, 1, 0) + Delayed::Job.list_jobs(:failed, 1, 1)).sort_by(&:id)
         | 
| 991 | 
            +
                  expect(failed.size).to eq(2)
         | 
| 992 | 
            +
                  expect(failed[0].original_job_id).to eq(jobs[0].id)
         | 
| 993 | 
            +
                  expect(failed[1].original_job_id).to eq(jobs[1].id)
         | 
| 645 994 | 
             
                end
         | 
| 646 995 | 
             
              end
         | 
| 647 996 |  | 
| @@ -652,126 +1001,123 @@ shared_examples_for 'a backend' do | |
| 652 1001 | 
             
                    @ignored_jobs = []
         | 
| 653 1002 | 
             
                  end
         | 
| 654 1003 |  | 
| 655 | 
            -
                  it " | 
| 656 | 
            -
                    @affected_jobs.all? | 
| 657 | 
            -
                    @ignored_jobs.any? | 
| 658 | 
            -
                    Delayed::Job.bulk_update( | 
| 1004 | 
            +
                  it "holds and unhold a scope of jobs" do
         | 
| 1005 | 
            +
                    expect(@affected_jobs.all?(&:on_hold?)).to be false
         | 
| 1006 | 
            +
                    expect(@ignored_jobs.any?(&:on_hold?)).to be false
         | 
| 1007 | 
            +
                    expect(Delayed::Job.bulk_update("hold", flavor: @flavor, query: @query)).to eq(@affected_jobs.size)
         | 
| 659 1008 |  | 
| 660 | 
            -
                    @affected_jobs.all? { |j| Delayed::Job.find(j.id).on_hold? }. | 
| 661 | 
            -
                    @ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }. | 
| 1009 | 
            +
                    expect(@affected_jobs.all? { |j| Delayed::Job.find(j.id).on_hold? }).to be true
         | 
| 1010 | 
            +
                    expect(@ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }).to be false
         | 
| 662 1011 |  | 
| 663 | 
            -
                     | 
| 664 | 
            -
                    # to un-hold them
         | 
| 665 | 
            -
                    next if Delayed::Job == Delayed::Backend::Redis::Job
         | 
| 666 | 
            -
                    Delayed::Job.bulk_update('unhold', :flavor => @flavor, :query => @query).should == @affected_jobs.size
         | 
| 1012 | 
            +
                    expect(Delayed::Job.bulk_update("unhold", flavor: @flavor, query: @query)).to eq(@affected_jobs.size)
         | 
| 667 1013 |  | 
| 668 | 
            -
                    @affected_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }. | 
| 669 | 
            -
                    @ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }. | 
| 1014 | 
            +
                    expect(@affected_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }).to be false
         | 
| 1015 | 
            +
                    expect(@ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }).to be false
         | 
| 670 1016 | 
             
                  end
         | 
| 671 1017 |  | 
| 672 | 
            -
                  it " | 
| 673 | 
            -
                    Delayed::Job.bulk_update( | 
| 674 | 
            -
                     | 
| 675 | 
            -
                     | 
| 1018 | 
            +
                  it "deletes a scope of jobs" do
         | 
| 1019 | 
            +
                    expect(Delayed::Job.bulk_update("destroy", flavor: @flavor, query: @query)).to eq(@affected_jobs.size)
         | 
| 1020 | 
            +
                    expect(Delayed::Job.where(id: @affected_jobs.map(&:id))).not_to exist
         | 
| 1021 | 
            +
                    expect(Delayed::Job.where(id: @ignored_jobs.map(&:id)).count).to eq @ignored_jobs.size
         | 
| 676 1022 | 
             
                  end
         | 
| 677 1023 | 
             
                end
         | 
| 678 1024 |  | 
| 679 1025 | 
             
                describe "scope: current" do
         | 
| 680 1026 | 
             
                  include_examples "scope"
         | 
| 681 | 
            -
                  before do
         | 
| 682 | 
            -
                    @flavor =  | 
| 1027 | 
            +
                  before do # rubocop:disable RSpec/HooksBeforeExamples
         | 
| 1028 | 
            +
                    @flavor = "current"
         | 
| 683 1029 | 
             
                    Timecop.freeze(5.minutes.ago) do
         | 
| 684 1030 | 
             
                      3.times { @affected_jobs << create_job }
         | 
| 685 | 
            -
                      @ignored_jobs << create_job(: | 
| 686 | 
            -
                      @ignored_jobs << create_job(: | 
| 1031 | 
            +
                      @ignored_jobs << create_job(run_at: 2.hours.from_now)
         | 
| 1032 | 
            +
                      @ignored_jobs << create_job(queue: "q2")
         | 
| 687 1033 | 
             
                    end
         | 
| 688 1034 | 
             
                  end
         | 
| 689 1035 | 
             
                end
         | 
| 690 1036 |  | 
| 691 1037 | 
             
                describe "scope: future" do
         | 
| 692 1038 | 
             
                  include_examples "scope"
         | 
| 693 | 
            -
                  before do
         | 
| 694 | 
            -
                    @flavor =  | 
| 1039 | 
            +
                  before do # rubocop:disable RSpec/HooksBeforeExamples
         | 
| 1040 | 
            +
                    @flavor = "future"
         | 
| 695 1041 | 
             
                    Timecop.freeze(5.minutes.ago) do
         | 
| 696 | 
            -
                      3.times { @affected_jobs << create_job(: | 
| 1042 | 
            +
                      3.times { @affected_jobs << create_job(run_at: 2.hours.from_now) }
         | 
| 697 1043 | 
             
                      @ignored_jobs << create_job
         | 
| 698 | 
            -
                      @ignored_jobs << create_job(: | 
| 1044 | 
            +
                      @ignored_jobs << create_job(queue: "q2", run_at: 2.hours.from_now)
         | 
| 699 1045 | 
             
                    end
         | 
| 700 1046 | 
             
                  end
         | 
| 701 1047 | 
             
                end
         | 
| 702 1048 |  | 
| 703 1049 | 
             
                describe "scope: strand" do
         | 
| 704 1050 | 
             
                  include_examples "scope"
         | 
| 705 | 
            -
                  before do
         | 
| 706 | 
            -
                    @flavor =  | 
| 707 | 
            -
                    @query =  | 
| 1051 | 
            +
                  before do # rubocop:disable RSpec/HooksBeforeExamples
         | 
| 1052 | 
            +
                    @flavor = "strand"
         | 
| 1053 | 
            +
                    @query = "s1"
         | 
| 708 1054 | 
             
                    Timecop.freeze(5.minutes.ago) do
         | 
| 709 | 
            -
                      @affected_jobs << create_job(: | 
| 710 | 
            -
                      @affected_jobs << create_job(: | 
| 1055 | 
            +
                      @affected_jobs << create_job(strand: "s1")
         | 
| 1056 | 
            +
                      @affected_jobs << create_job(strand: "s1", run_at: 2.hours.from_now)
         | 
| 711 1057 | 
             
                      @ignored_jobs << create_job
         | 
| 712 | 
            -
                      @ignored_jobs << create_job(: | 
| 713 | 
            -
                      @ignored_jobs << create_job(: | 
| 1058 | 
            +
                      @ignored_jobs << create_job(strand: "s2")
         | 
| 1059 | 
            +
                      @ignored_jobs << create_job(strand: "s2", run_at: 2.hours.from_now)
         | 
| 714 1060 | 
             
                    end
         | 
| 715 1061 | 
             
                  end
         | 
| 716 1062 | 
             
                end
         | 
| 717 1063 |  | 
| 718 1064 | 
             
                describe "scope: tag" do
         | 
| 719 1065 | 
             
                  include_examples "scope"
         | 
| 720 | 
            -
                  before do
         | 
| 721 | 
            -
                    @flavor =  | 
| 722 | 
            -
                    @query =  | 
| 1066 | 
            +
                  before do # rubocop:disable RSpec/HooksBeforeExamples
         | 
| 1067 | 
            +
                    @flavor = "tag"
         | 
| 1068 | 
            +
                    @query = "String#to_i"
         | 
| 723 1069 | 
             
                    Timecop.freeze(5.minutes.ago) do
         | 
| 724 1070 | 
             
                      @affected_jobs << "test".delay(ignore_transaction: true).to_i
         | 
| 725 | 
            -
                      @affected_jobs << "test".delay(strand:  | 
| 1071 | 
            +
                      @affected_jobs << "test".delay(strand: "s1", ignore_transaction: true).to_i
         | 
| 726 1072 | 
             
                      @affected_jobs << "test".delay(run_at: 2.hours.from_now, ignore_transaction: true).to_i
         | 
| 727 1073 | 
             
                      @ignored_jobs << create_job
         | 
| 728 | 
            -
                      @ignored_jobs << create_job(: | 
| 1074 | 
            +
                      @ignored_jobs << create_job(run_at: 1.hour.from_now)
         | 
| 729 1075 | 
             
                    end
         | 
| 730 1076 | 
             
                  end
         | 
| 731 1077 | 
             
                end
         | 
| 732 1078 |  | 
| 733 | 
            -
                it " | 
| 1079 | 
            +
                it "holds and un-hold given job ids" do
         | 
| 734 1080 | 
             
                  j1 = "test".delay(ignore_transaction: true).to_i
         | 
| 735 | 
            -
                  j2 = create_job(: | 
| 736 | 
            -
                  j3 = "test".delay(strand:  | 
| 737 | 
            -
                  Delayed::Job.bulk_update( | 
| 738 | 
            -
                  Delayed::Job.find(j1.id).on_hold | 
| 739 | 
            -
                  Delayed::Job.find(j2.id).on_hold | 
| 740 | 
            -
                  Delayed::Job.find(j3.id).on_hold | 
| 1081 | 
            +
                  j2 = create_job(run_at: 2.hours.from_now)
         | 
| 1082 | 
            +
                  j3 = "test".delay(strand: "s1", ignore_transaction: true).to_i
         | 
| 1083 | 
            +
                  expect(Delayed::Job.bulk_update("hold", ids: [j1.id, j2.id])).to eq(2)
         | 
| 1084 | 
            +
                  expect(Delayed::Job.find(j1.id).on_hold?).to be true
         | 
| 1085 | 
            +
                  expect(Delayed::Job.find(j2.id).on_hold?).to be true
         | 
| 1086 | 
            +
                  expect(Delayed::Job.find(j3.id).on_hold?).to be false
         | 
| 741 1087 |  | 
| 742 | 
            -
                  Delayed::Job.bulk_update( | 
| 743 | 
            -
                  Delayed::Job.find(j1.id).on_hold | 
| 744 | 
            -
                  Delayed::Job.find(j2.id).on_hold | 
| 745 | 
            -
                  Delayed::Job.find(j3.id).on_hold | 
| 1088 | 
            +
                  expect(Delayed::Job.bulk_update("unhold", ids: [j2.id, j3.id])).to eq(1)
         | 
| 1089 | 
            +
                  expect(Delayed::Job.find(j1.id).on_hold?).to be true
         | 
| 1090 | 
            +
                  expect(Delayed::Job.find(j2.id).on_hold?).to be false
         | 
| 1091 | 
            +
                  expect(Delayed::Job.find(j3.id).on_hold?).to be false
         | 
| 746 1092 | 
             
                end
         | 
| 747 1093 |  | 
| 748 | 
            -
                it " | 
| 749 | 
            -
                  job1 = Delayed::Job.new(: | 
| 1094 | 
            +
                it "does not hold locked jobs" do
         | 
| 1095 | 
            +
                  job1 = Delayed::Job.new(tag: "tag")
         | 
| 750 1096 | 
             
                  job1.create_and_lock!("worker")
         | 
| 751 | 
            -
                  job1.on_hold | 
| 752 | 
            -
                  Delayed::Job.bulk_update( | 
| 753 | 
            -
                  Delayed::Job.find(job1.id).on_hold | 
| 1097 | 
            +
                  expect(job1.on_hold?).to be false
         | 
| 1098 | 
            +
                  expect(Delayed::Job.bulk_update("hold", ids: [job1.id])).to eq(0)
         | 
| 1099 | 
            +
                  expect(Delayed::Job.find(job1.id).on_hold?).to be false
         | 
| 754 1100 | 
             
                end
         | 
| 755 1101 |  | 
| 756 | 
            -
                it " | 
| 757 | 
            -
                  job1 = Delayed::Job.new(: | 
| 1102 | 
            +
                it "does not unhold locked jobs" do
         | 
| 1103 | 
            +
                  job1 = Delayed::Job.new(tag: "tag")
         | 
| 758 1104 | 
             
                  job1.create_and_lock!("worker")
         | 
| 759 | 
            -
                  Delayed::Job.bulk_update( | 
| 760 | 
            -
                  Delayed::Job.find(job1.id).on_hold | 
| 761 | 
            -
                  Delayed::Job.find(job1.id).locked | 
| 1105 | 
            +
                  expect(Delayed::Job.bulk_update("unhold", ids: [job1.id])).to eq(0)
         | 
| 1106 | 
            +
                  expect(Delayed::Job.find(job1.id).on_hold?).to be false
         | 
| 1107 | 
            +
                  expect(Delayed::Job.find(job1.id).locked?).to be true
         | 
| 762 1108 | 
             
                end
         | 
| 763 1109 |  | 
| 764 | 
            -
                it " | 
| 1110 | 
            +
                it "deletes given job ids" do
         | 
| 765 1111 | 
             
                  jobs = (0..2).map { create_job }
         | 
| 766 | 
            -
                  Delayed::Job.bulk_update( | 
| 767 | 
            -
                   | 
| 1112 | 
            +
                  expect(Delayed::Job.bulk_update("destroy", ids: jobs[0, 2].map(&:id))).to eq(2)
         | 
| 1113 | 
            +
                  expect(Delayed::Job.order(:id).where(id: jobs.map(&:id))).to eq jobs[2, 1]
         | 
| 768 1114 | 
             
                end
         | 
| 769 1115 |  | 
| 770 | 
            -
                it " | 
| 771 | 
            -
                  job1 = Delayed::Job.new(: | 
| 1116 | 
            +
                it "does not delete locked jobs" do
         | 
| 1117 | 
            +
                  job1 = Delayed::Job.new(tag: "tag")
         | 
| 772 1118 | 
             
                  job1.create_and_lock!("worker")
         | 
| 773 | 
            -
                  Delayed::Job.bulk_update( | 
| 774 | 
            -
                  Delayed::Job.find(job1.id).locked | 
| 1119 | 
            +
                  expect(Delayed::Job.bulk_update("destroy", ids: [job1.id])).to eq(0)
         | 
| 1120 | 
            +
                  expect(Delayed::Job.find(job1.id).locked?).to be true
         | 
| 775 1121 | 
             
                end
         | 
| 776 1122 | 
             
              end
         | 
| 777 1123 |  | 
| @@ -779,7 +1125,7 @@ shared_examples_for 'a backend' do | |
| 779 1125 | 
             
                before do
         | 
| 780 1126 | 
             
                  @cur = []
         | 
| 781 1127 | 
             
                  3.times { @cur << "test".delay(ignore_transaction: true).to_s }
         | 
| 782 | 
            -
                  5.times { @cur << "test".delay(ignore_transaction: true).to_i}
         | 
| 1128 | 
            +
                  5.times { @cur << "test".delay(ignore_transaction: true).to_i }
         | 
| 783 1129 | 
             
                  2.times { @cur << "test".delay(ignore_transaction: true).upcase }
         | 
| 784 1130 | 
             
                  "test".delay(ignore_transaction: true).downcase.fail!
         | 
| 785 1131 | 
             
                  @future = []
         | 
| @@ -787,48 +1133,48 @@ shared_examples_for 'a backend' do | |
| 787 1133 | 
             
                  @cur << "test".delay(ignore_transaction: true).downcase
         | 
| 788 1134 | 
             
                end
         | 
| 789 1135 |  | 
| 790 | 
            -
                it " | 
| 791 | 
            -
                  Delayed::Job.tag_counts(:current, 1). | 
| 792 | 
            -
                  Delayed::Job.tag_counts(:current, 1, 1). | 
| 793 | 
            -
                  Delayed::Job.tag_counts(:current, 5). | 
| 794 | 
            -
             | 
| 795 | 
            -
             | 
| 796 | 
            -
             | 
| 797 | 
            -
                  @cur[0,4].each | 
| 1136 | 
            +
                it "returns a sorted list of popular current tags" do
         | 
| 1137 | 
            +
                  expect(Delayed::Job.tag_counts(:current, 1)).to eq([{ tag: "String#to_i", count: 5 }])
         | 
| 1138 | 
            +
                  expect(Delayed::Job.tag_counts(:current, 1, 1)).to eq([{ tag: "String#to_s", count: 3 }])
         | 
| 1139 | 
            +
                  expect(Delayed::Job.tag_counts(:current, 5)).to eq([{ tag: "String#to_i", count: 5 },
         | 
| 1140 | 
            +
                                                                      { tag: "String#to_s", count: 3 },
         | 
| 1141 | 
            +
                                                                      { tag: "String#upcase", count: 2 },
         | 
| 1142 | 
            +
                                                                      { tag: "String#downcase", count: 1 }])
         | 
| 1143 | 
            +
                  @cur[0, 4].each(&:destroy)
         | 
| 798 1144 | 
             
                  @future[0].run_at = @future[1].run_at = 1.hour.ago
         | 
| 799 1145 | 
             
                  @future[0].save!
         | 
| 800 1146 | 
             
                  @future[1].save!
         | 
| 801 1147 |  | 
| 802 | 
            -
                  Delayed::Job.tag_counts(:current, 5). | 
| 803 | 
            -
             | 
| 804 | 
            -
             | 
| 1148 | 
            +
                  expect(Delayed::Job.tag_counts(:current, 5)).to eq([{ tag: "String#to_i", count: 4 },
         | 
| 1149 | 
            +
                                                                      { tag: "String#downcase", count: 3 },
         | 
| 1150 | 
            +
                                                                      { tag: "String#upcase", count: 2 }])
         | 
| 805 1151 | 
             
                end
         | 
| 806 1152 |  | 
| 807 | 
            -
                it " | 
| 808 | 
            -
                  Delayed::Job.tag_counts(:all, 1). | 
| 809 | 
            -
                  Delayed::Job.tag_counts(:all, 1, 1). | 
| 810 | 
            -
                  Delayed::Job.tag_counts(:all, 5). | 
| 811 | 
            -
             | 
| 812 | 
            -
             | 
| 813 | 
            -
             | 
| 1153 | 
            +
                it "returns a sorted list of all popular tags" do
         | 
| 1154 | 
            +
                  expect(Delayed::Job.tag_counts(:all, 1)).to eq([{ tag: "String#downcase", count: 6 }])
         | 
| 1155 | 
            +
                  expect(Delayed::Job.tag_counts(:all, 1, 1)).to eq([{ tag: "String#to_i", count: 5 }])
         | 
| 1156 | 
            +
                  expect(Delayed::Job.tag_counts(:all, 5)).to eq([{ tag: "String#downcase", count: 6 },
         | 
| 1157 | 
            +
                                                                  { tag: "String#to_i", count: 5 },
         | 
| 1158 | 
            +
                                                                  { tag: "String#to_s", count: 3 },
         | 
| 1159 | 
            +
                                                                  { tag: "String#upcase", count: 2 }])
         | 
| 814 1160 |  | 
| 815 | 
            -
                  @cur[0,4].each | 
| 1161 | 
            +
                  @cur[0, 4].each(&:destroy)
         | 
| 816 1162 | 
             
                  @future[0].destroy
         | 
| 817 1163 | 
             
                  @future[1].fail!
         | 
| 818 1164 | 
             
                  @future[2].fail!
         | 
| 819 1165 |  | 
| 820 | 
            -
                  Delayed::Job.tag_counts(:all, 5). | 
| 821 | 
            -
             | 
| 822 | 
            -
             | 
| 1166 | 
            +
                  expect(Delayed::Job.tag_counts(:all, 5)).to eq([{ tag: "String#to_i", count: 4 },
         | 
| 1167 | 
            +
                                                                  { tag: "String#downcase", count: 3 },
         | 
| 1168 | 
            +
                                                                  { tag: "String#upcase", count: 2 }])
         | 
| 823 1169 | 
             
                end
         | 
| 824 1170 | 
             
              end
         | 
| 825 1171 |  | 
| 826 | 
            -
              it " | 
| 1172 | 
            +
              it "unlocks orphaned jobs" do
         | 
| 827 1173 | 
             
                change_setting(Delayed::Settings, :max_attempts, 2) do
         | 
| 828 | 
            -
                  job1 = Delayed::Job.new(: | 
| 829 | 
            -
                  job2 = Delayed::Job.new(: | 
| 830 | 
            -
                  job3 = Delayed::Job.new(: | 
| 831 | 
            -
                  job4 = Delayed::Job.new(: | 
| 1174 | 
            +
                  job1 = Delayed::Job.new(tag: "tag")
         | 
| 1175 | 
            +
                  job2 = Delayed::Job.new(tag: "tag")
         | 
| 1176 | 
            +
                  job3 = Delayed::Job.new(tag: "tag")
         | 
| 1177 | 
            +
                  job4 = Delayed::Job.new(tag: "tag")
         | 
| 832 1178 | 
             
                  job1.create_and_lock!("Jobworker:#{Process.pid}")
         | 
| 833 1179 | 
             
                  `echo ''`
         | 
| 834 1180 | 
             
                  child_pid = $?.pid
         | 
| @@ -836,23 +1182,38 @@ shared_examples_for 'a backend' do | |
| 836 1182 | 
             
                  job3.create_and_lock!("someoneelse:#{Process.pid}")
         | 
| 837 1183 | 
             
                  job4.create_and_lock!("Jobworker:notanumber")
         | 
| 838 1184 |  | 
| 839 | 
            -
                  Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker"). | 
| 1185 | 
            +
                  expect(Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker")).to eq(1)
         | 
| 840 1186 |  | 
| 841 | 
            -
                  Delayed::Job.find(job1.id).locked_by. | 
| 842 | 
            -
                  Delayed::Job.find(job2.id).locked_by. | 
| 843 | 
            -
                  Delayed::Job.find(job3.id).locked_by. | 
| 844 | 
            -
                  Delayed::Job.find(job4.id).locked_by. | 
| 1187 | 
            +
                  expect(Delayed::Job.find(job1.id).locked_by).not_to be_nil
         | 
| 1188 | 
            +
                  expect(Delayed::Job.find(job2.id).locked_by).to be_nil
         | 
| 1189 | 
            +
                  expect(Delayed::Job.find(job3.id).locked_by).not_to be_nil
         | 
| 1190 | 
            +
                  expect(Delayed::Job.find(job4.id).locked_by).not_to be_nil
         | 
| 845 1191 |  | 
| 846 | 
            -
                  Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker"). | 
| 1192 | 
            +
                  expect(Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker")).to eq(0)
         | 
| 1193 | 
            +
                end
         | 
| 1194 | 
            +
              end
         | 
| 1195 | 
            +
             | 
| 1196 | 
            +
              it "removes an un-reschedulable job" do
         | 
| 1197 | 
            +
                change_setting(Delayed::Settings, :max_attempts, -1) do
         | 
| 1198 | 
            +
                  job = Delayed::Job.new(tag: "tag")
         | 
| 1199 | 
            +
                  `echo ''`
         | 
| 1200 | 
            +
                  child_pid = $?.pid
         | 
| 1201 | 
            +
                  job.create_and_lock!("Jobworker:#{child_pid}")
         | 
| 1202 | 
            +
                  Timeout.timeout(1) do
         | 
| 1203 | 
            +
                    # if this takes longer than a second it's hung
         | 
| 1204 | 
            +
                    # in an infinite loop, which would be bad.
         | 
| 1205 | 
            +
                    expect(Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker")).to eq(1)
         | 
| 1206 | 
            +
                  end
         | 
| 1207 | 
            +
                  expect { Delayed::Job.find(job.id) }.to raise_error(ActiveRecord::RecordNotFound)
         | 
| 847 1208 | 
             
                end
         | 
| 848 1209 | 
             
              end
         | 
| 849 1210 |  | 
| 850 | 
            -
              it " | 
| 1211 | 
            +
              it "unlocks orphaned jobs given a pid" do
         | 
| 851 1212 | 
             
                change_setting(Delayed::Settings, :max_attempts, 2) do
         | 
| 852 | 
            -
                  job1 = Delayed::Job.new(: | 
| 853 | 
            -
                  job2 = Delayed::Job.new(: | 
| 854 | 
            -
                  job3 = Delayed::Job.new(: | 
| 855 | 
            -
                  job4 = Delayed::Job.new(: | 
| 1213 | 
            +
                  job1 = Delayed::Job.new(tag: "tag")
         | 
| 1214 | 
            +
                  job2 = Delayed::Job.new(tag: "tag")
         | 
| 1215 | 
            +
                  job3 = Delayed::Job.new(tag: "tag")
         | 
| 1216 | 
            +
                  job4 = Delayed::Job.new(tag: "tag")
         | 
| 856 1217 | 
             
                  job1.create_and_lock!("Jobworker:#{Process.pid}")
         | 
| 857 1218 | 
             
                  `echo ''`
         | 
| 858 1219 | 
             
                  child_pid = $?.pid
         | 
| @@ -862,15 +1223,15 @@ shared_examples_for 'a backend' do | |
| 862 1223 | 
             
                  job3.create_and_lock!("someoneelse:#{Process.pid}")
         | 
| 863 1224 | 
             
                  job4.create_and_lock!("Jobworker:notanumber")
         | 
| 864 1225 |  | 
| 865 | 
            -
                  Delayed::Job.unlock_orphaned_jobs(child_pid2, "Jobworker"). | 
| 866 | 
            -
                  Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker"). | 
| 1226 | 
            +
                  expect(Delayed::Job.unlock_orphaned_jobs(child_pid2, "Jobworker")).to eq(0)
         | 
| 1227 | 
            +
                  expect(Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker")).to eq(1)
         | 
| 867 1228 |  | 
| 868 | 
            -
                  Delayed::Job.find(job1.id).locked_by. | 
| 869 | 
            -
                  Delayed::Job.find(job2.id).locked_by. | 
| 870 | 
            -
                  Delayed::Job.find(job3.id).locked_by. | 
| 871 | 
            -
                  Delayed::Job.find(job4.id).locked_by. | 
| 1229 | 
            +
                  expect(Delayed::Job.find(job1.id).locked_by).not_to be_nil
         | 
| 1230 | 
            +
                  expect(Delayed::Job.find(job2.id).locked_by).to be_nil
         | 
| 1231 | 
            +
                  expect(Delayed::Job.find(job3.id).locked_by).not_to be_nil
         | 
| 1232 | 
            +
                  expect(Delayed::Job.find(job4.id).locked_by).not_to be_nil
         | 
| 872 1233 |  | 
| 873 | 
            -
                  Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker"). | 
| 1234 | 
            +
                  expect(Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker")).to eq(0)
         | 
| 874 1235 | 
             
                end
         | 
| 875 1236 | 
             
              end
         | 
| 876 1237 | 
             
            end
         |