monitor_spec.rb 6.61 KB
Newer Older
1 2 3 4
# frozen_string_literal: true

require 'spec_helper'

5
describe Gitlab::SidekiqDaemon::Monitor do
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
  let(:monitor) { described_class.new }

  describe '#within_job' do
    it 'tracks thread' do
      blk = proc do
        expect(monitor.jobs_thread['jid']).not_to be_nil

        "OK"
      end

      expect(monitor.within_job('jid', 'queue', &blk)).to eq("OK")
    end

    context 'when job is canceled' do
      let(:jid) { SecureRandom.hex }

      before do
        described_class.cancel_job(jid)
      end

      it 'does not execute a block' do
        expect do |blk|
          monitor.within_job(jid, 'queue', &blk)
        rescue described_class::CancelledError
        end.not_to yield_control
      end

      it 'raises exception' do
34 35
        expect { monitor.within_job(jid, 'queue') }.to raise_error(
          described_class::CancelledError)
36 37 38 39 40
      end
    end
  end

  describe '#start_working' do
41
    subject { monitor.send(:start_working) }
42

43 44 45
    before do
      # we want to run at most once cycle
      # we toggle `enabled?` flag after the first call
46
      stub_const('Gitlab::SidekiqDaemon::Monitor::RECONNECT_TIME', 0)
47 48 49 50 51
      allow(monitor).to receive(:enabled?).and_return(true, false)

      allow(Sidekiq.logger).to receive(:info)
      allow(Sidekiq.logger).to receive(:warn)
    end
52

53
    context 'when structured logging is used' do
54 55 56
      it 'logs start message' do
        expect(Sidekiq.logger).to receive(:info)
          .with(
57
            class: described_class.to_s,
58 59 60
            action: 'start',
            message: 'Starting Monitor Daemon')

61 62
        expect(::Gitlab::Redis::SharedState).to receive(:with)

63 64 65 66 67 68
        subject
      end

      it 'logs stop message' do
        expect(Sidekiq.logger).to receive(:warn)
          .with(
69
            class: described_class.to_s,
70 71 72
            action: 'stop',
            message: 'Stopping Monitor Daemon')

73 74
        expect(::Gitlab::Redis::SharedState).to receive(:with)

75 76 77
        subject
      end

78 79 80
      it 'logs StandardError message' do
        expect(Sidekiq.logger).to receive(:warn)
          .with(
81
            class: described_class.to_s,
82 83 84 85 86 87 88 89 90 91
            action: 'exception',
            message: 'My Exception')

        expect(::Gitlab::Redis::SharedState).to receive(:with)
          .and_raise(StandardError, 'My Exception')

        expect { subject }.not_to raise_error
      end

      it 'logs and raises Exception message' do
92 93
        expect(Sidekiq.logger).to receive(:warn)
          .with(
94
            class: described_class.to_s,
95 96 97 98 99 100 101 102 103 104
            action: 'exception',
            message: 'My Exception')

        expect(::Gitlab::Redis::SharedState).to receive(:with)
          .and_raise(Exception, 'My Exception')

        expect { subject }.to raise_error(Exception, 'My Exception')
      end
    end

105 106 107 108 109 110 111 112 113 114 115 116 117 118
    context 'when StandardError is raised' do
      it 'does retry connection' do
        expect(::Gitlab::Redis::SharedState).to receive(:with)
          .and_raise(StandardError, 'My Exception')

        expect(::Gitlab::Redis::SharedState).to receive(:with)

        # we expect to run `process_messages` twice
        expect(monitor).to receive(:enabled?).and_return(true, true, false)

        subject
      end
    end

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
    context 'when message is published' do
      let(:subscribed) { double }

      before do
        expect_any_instance_of(::Redis).to receive(:subscribe)
          .and_yield(subscribed)

        expect(subscribed).to receive(:message)
          .and_yield(
            described_class::NOTIFICATION_CHANNEL,
            payload
          )

        expect(Sidekiq.logger).to receive(:info)
          .with(
134
            class: described_class.to_s,
135 136 137 138 139
            action: 'start',
            message: 'Starting Monitor Daemon')

        expect(Sidekiq.logger).to receive(:info)
          .with(
140
            class: described_class.to_s,
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
            channel: described_class::NOTIFICATION_CHANNEL,
            message: 'Received payload on channel',
            payload: payload
          )
      end

      context 'and message is valid' do
        let(:payload) { '{"action":"cancel","jid":"my-jid"}' }

        it 'processes cancel' do
          expect(monitor).to receive(:process_job_cancel).with('my-jid')

          subject
        end
      end

      context 'and message is not valid json' do
        let(:payload) { '{"action"}' }

        it 'skips processing' do
          expect(monitor).not_to receive(:process_job_cancel)

          subject
        end
      end
    end
  end

169 170 171 172 173 174 175 176 177 178 179 180 181
  describe '#stop' do
    let!(:monitor_thread) { monitor.start }

    it 'does stop the thread' do
      expect(monitor_thread).to be_alive

      expect { monitor.stop }.not_to raise_error

      expect(monitor_thread).not_to be_alive
      expect { monitor_thread.value }.to raise_error(Interrupt)
    end
  end

182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
  describe '#process_job_cancel' do
    subject { monitor.send(:process_job_cancel, jid) }

    context 'when jid is missing' do
      let(:jid) { nil }

      it 'does not run thread' do
        expect(subject).to be_nil
      end
    end

    context 'when jid is provided' do
      let(:jid) { 'my-jid' }

      context 'when jid is not found' do
        it 'does not log cancellation message' do
          expect(Sidekiq.logger).not_to receive(:warn)
199
          expect(subject).to be_nil
200 201 202 203 204 205 206 207 208 209
        end
      end

      context 'when jid is found' do
        let(:thread) { Thread.new { sleep 1000 } }

        before do
          monitor.jobs_thread[jid] = thread
        end

210 211 212 213 214
        after do
          thread.kill
        rescue
        end

215 216 217
        it 'does log cancellation message' do
          expect(Sidekiq.logger).to receive(:warn)
            .with(
218
              class: described_class.to_s,
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
              action: 'cancel',
              message: 'Canceling thread with CancelledError',
              jid: 'my-jid',
              thread_id: thread.object_id)

          expect(subject).to be_a(Thread)

          subject.join
        end

        it 'does cancel the thread' do
          expect(subject).to be_a(Thread)

          subject.join

234 235 236
          # we wait for the thread to be cancelled
          # by `process_job_cancel`
          expect { thread.join(5) }.to raise_error(described_class::CancelledError)
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
        end
      end
    end
  end

  describe '.cancel_job' do
    subject { described_class.cancel_job('my-jid') }

    it 'sets a redis key' do
      expect_any_instance_of(::Redis).to receive(:setex)
        .with('sidekiq:cancel:my-jid', anything, 1)

      subject
    end

    it 'notifies all workers' do
      payload = '{"action":"cancel","jid":"my-jid"}'

      expect_any_instance_of(::Redis).to receive(:publish)
        .with('sidekiq:cancel:notifications', payload)

      subject
    end
  end
end