|
| 1 | +require 'spec_helper' |
| 2 | +require 'delayed_job' |
| 3 | +require 'delayed_job/threaded_worker' |
| 4 | + |
| 5 | +RSpec.describe ThreadedWorker do |
| 6 | + let(:num_threads) { 2 } |
| 7 | + let(:grace_period_seconds) { 2 } |
| 8 | + let(:worker) { ThreadedWorker.new(num_threads, {}, grace_period_seconds) } |
| 9 | + let(:worker_name) { 'instance_name' } |
| 10 | + |
| 11 | + before { worker.name = worker_name } |
| 12 | + |
| 13 | + describe '#initialize' do |
| 14 | + it 'sets up the thread count' do |
| 15 | + expect(worker.instance_variable_get(:@num_threads)).to eq(num_threads) |
| 16 | + end |
| 17 | + |
| 18 | + it 'sets up the grace period' do |
| 19 | + expect(worker.instance_variable_get(:@grace_period_seconds)).to eq(grace_period_seconds) |
| 20 | + end |
| 21 | + |
| 22 | + it 'sets up the grace period to 30 seconds by default' do |
| 23 | + worker = ThreadedWorker.new(num_threads) |
| 24 | + expect(worker.instance_variable_get(:@grace_period_seconds)).to eq(30) |
| 25 | + end |
| 26 | + end |
| 27 | + |
| 28 | + describe '#start' do |
| 29 | + before do |
| 30 | + allow(worker).to receive(:threaded_start) |
| 31 | + end |
| 32 | + |
| 33 | + it 'sets up signal traps for all signals' do |
| 34 | + expect(worker).to receive(:trap).with('TERM') |
| 35 | + expect(worker).to receive(:trap).with('INT') |
| 36 | + expect(worker).to receive(:trap).with('QUIT') |
| 37 | + worker.start |
| 38 | + end |
| 39 | + |
| 40 | + it 'starts the specified number of threads' do |
| 41 | + expect(worker).to receive(:threaded_start).exactly(num_threads).times |
| 42 | + |
| 43 | + expect(worker.instance_variable_get(:@threads).length).to eq(0) |
| 44 | + worker.start |
| 45 | + expect(worker.instance_variable_get(:@threads).length).to eq(num_threads) |
| 46 | + end |
| 47 | + |
| 48 | + it 'logs the start and shutdown messages' do |
| 49 | + expect(worker).to receive(:say).with("Starting threaded delayed worker with #{num_threads} threads") |
| 50 | + worker.start |
| 51 | + end |
| 52 | + |
| 53 | + it 'sets the thread_name variable for each thread' do |
| 54 | + worker.start |
| 55 | + worker.instance_variable_get(:@threads).each_with_index do |thread, index| |
| 56 | + expect(thread[:thread_name]).to eq("thread:#{index + 1}") |
| 57 | + end |
| 58 | + end |
| 59 | + |
| 60 | + it 'logs the error and stops the worker when an unexpected error occurs' do |
| 61 | + allow(worker).to receive(:threaded_start).and_raise(StandardError.new('test error')) |
| 62 | + allow(worker).to receive(:stop) |
| 63 | + expect { worker.start }.to raise_error('Unexpected error occurred in one of the worker threads') |
| 64 | + expect(worker.instance_variable_get(:@unexpected_error)).to be true |
| 65 | + end |
| 66 | + end |
| 67 | + |
| 68 | + describe '#name' do |
| 69 | + it 'returns the instance name if thread name is set' do |
| 70 | + allow(Thread.current).to receive(:[]).with(:thread_name).and_return('some-thread-name') |
| 71 | + expect(worker.name).to eq('instance_name some-thread-name') |
| 72 | + end |
| 73 | + |
| 74 | + it 'returns the instance name if thread name is not set' do |
| 75 | + allow(Thread.current).to receive(:[]).with(:thread_name).and_return(nil) |
| 76 | + expect(worker.name).to eq(worker_name) |
| 77 | + end |
| 78 | + end |
| 79 | + |
| 80 | + describe '#stop' do |
| 81 | + it 'logs the shutdown message' do |
| 82 | + queue = Queue.new |
| 83 | + allow(worker).to(receive(:say)) { |message| queue.push(message) } |
| 84 | + |
| 85 | + worker.stop |
| 86 | + expect(queue.pop).to eq('Shutting down worker threads gracefully...') |
| 87 | + end |
| 88 | + |
| 89 | + it 'sets the exit flag in the parent worker' do |
| 90 | + worker.stop |
| 91 | + sleep 0.1 until worker.instance_variable_defined?(:@exit) |
| 92 | + expect(worker.instance_variable_get(:@exit)).to be true |
| 93 | + end |
| 94 | + |
| 95 | + it 'allows threads to finish their work without being killed prematurely' do |
| 96 | + allow(worker).to receive(:threaded_start) do |
| 97 | + sleep grace_period_seconds / 2 until worker.instance_variable_get(:@exit) == true |
| 98 | + end |
| 99 | + |
| 100 | + worker_thread = Thread.new { worker.start } |
| 101 | + sleep 0.1 until worker.instance_variable_get(:@threads).length == num_threads && worker.instance_variable_get(:@threads).all?(&:alive?) |
| 102 | + worker.instance_variable_get(:@threads).each { |t| allow(t).to receive(:kill).and_call_original } |
| 103 | + |
| 104 | + Thread.new { worker.stop }.join |
| 105 | + worker_thread.join |
| 106 | + worker.instance_variable_get(:@threads).each { |t| expect(t).not_to have_received(:kill) } |
| 107 | + end |
| 108 | + |
| 109 | + it 'kills threads that exceed the grace period during shutdown' do |
| 110 | + allow(worker).to receive(:threaded_start) do |
| 111 | + sleep grace_period_seconds * 2 until worker.instance_variable_get(:@exit) == true |
| 112 | + end |
| 113 | + |
| 114 | + worker_thread = Thread.new { worker.start } |
| 115 | + sleep 0.1 until worker.instance_variable_get(:@threads).length == num_threads && worker.instance_variable_get(:@threads).all?(&:alive?) |
| 116 | + worker.instance_variable_get(:@threads).each { |t| allow(t).to receive(:kill).and_call_original } |
| 117 | + |
| 118 | + Thread.new { worker.stop }.join |
| 119 | + worker_thread.join |
| 120 | + expect(worker.instance_variable_get(:@threads)).to all(have_received(:kill)) |
| 121 | + end |
| 122 | + end |
| 123 | + |
| 124 | + describe '#threaded_start' do |
| 125 | + before do |
| 126 | + allow(worker).to receive(:work_off).and_return([5, 2]) |
| 127 | + allow(worker).to receive(:sleep) |
| 128 | + allow(worker).to receive(:stop?).and_return(false, true) |
| 129 | + allow(worker).to receive(:reload!).and_call_original |
| 130 | + end |
| 131 | + |
| 132 | + it 'runs the work_off loop twice' do |
| 133 | + worker.threaded_start |
| 134 | + expect(worker).to have_received(:work_off).twice |
| 135 | + end |
| 136 | + |
| 137 | + it 'logs the number of jobs processed' do |
| 138 | + expect(worker).to receive(:say).with(%r{7 jobs processed at \d+\.\d+ j/s, 2 failed}).twice |
| 139 | + worker.threaded_start |
| 140 | + end |
| 141 | + |
| 142 | + it 'reloads the worker if stop is not set' do |
| 143 | + allow(worker).to receive(:work_off).and_return([0, 0]) |
| 144 | + worker.threaded_start |
| 145 | + expect(worker).to have_received(:reload!).once |
| 146 | + end |
| 147 | + |
| 148 | + context 'when exit_on_complete is set' do |
| 149 | + before do |
| 150 | + allow(worker.class).to receive(:exit_on_complete).and_return(true) |
| 151 | + allow(worker).to receive(:work_off).and_return([0, 0]) |
| 152 | + end |
| 153 | + |
| 154 | + it 'exits the worker when no more jobs are available' do |
| 155 | + expect(worker).to receive(:say).with('No more jobs available. Exiting') |
| 156 | + worker.threaded_start |
| 157 | + end |
| 158 | + end |
| 159 | + end |
| 160 | +end |
0 commit comments