PageRenderTime 47ms CodeModel.GetById 22ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/support/redis/redis_shared_examples.rb

https://gitlab.com/realsatomic/gitlab
Ruby | 407 lines | 325 code | 79 blank | 3 comment | 0 complexity | d5a968c370e49c1d8f829b82089e053f MD5 | raw file
  1# frozen_string_literal: true
  2
  3RSpec.shared_examples "redis_shared_examples" do
  4  include StubENV
  5
  6  let(:test_redis_url) { "redis://redishost:#{redis_port}"}
  7  let(:config_file_name) { instance_specific_config_file }
  8  let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" }
  9  let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
 10  let(:old_socket_path) {"/path/to/old/redis.sock" }
 11  let(:new_socket_path) {"/path/to/redis.sock" }
 12  let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" }
 13  let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
 14  let(:redis_port) { 6379 }
 15  let(:redis_database) { 99 }
 16  let(:sentinel_port) { 26379 }
 17  let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_config_with_env.yml"}
 18  let(:config_env_variable_url) {"TEST_GITLAB_REDIS_URL"}
 19  let(:rails_root) { Dir.mktmpdir('redis_shared_examples') }
 20
 21  before do
 22    allow(described_class).to receive(:config_file_name).and_return(Rails.root.join(config_file_name).to_s)
 23    redis_clear_raw_config!(described_class)
 24  end
 25
 26  after do
 27    redis_clear_raw_config!(described_class)
 28  end
 29
 30  describe '.config_file_name' do
 31    subject { described_class.config_file_name }
 32
 33    before do
 34      # Undo top-level stub of config_file_name because we are testing that method now.
 35      allow(described_class).to receive(:config_file_name).and_call_original
 36
 37      allow(described_class).to receive(:rails_root).and_return(rails_root)
 38      FileUtils.mkdir_p(File.join(rails_root, 'config'))
 39    end
 40
 41    after do
 42      FileUtils.rm_rf(rails_root)
 43    end
 44
 45    context 'when there is no config file anywhere' do
 46      it { expect(subject).to be_nil }
 47
 48      context 'but resque.yml exists' do
 49        before do
 50          FileUtils.touch(File.join(rails_root, 'config', 'resque.yml'))
 51        end
 52
 53        it { expect(subject).to eq("#{rails_root}/config/resque.yml") }
 54
 55        it 'returns a path that exists' do
 56          expect(File.file?(subject)).to eq(true)
 57        end
 58
 59        context 'and there is a global env override' do
 60          before do
 61            stub_env('GITLAB_REDIS_CONFIG_FILE', 'global override')
 62          end
 63
 64          it { expect(subject).to eq('global override') }
 65
 66          context 'and there is an instance specific config file' do
 67            before do
 68              FileUtils.touch(File.join(rails_root, instance_specific_config_file))
 69            end
 70
 71            it { expect(subject).to eq("#{rails_root}/#{instance_specific_config_file}") }
 72
 73            it 'returns a path that exists' do
 74              expect(File.file?(subject)).to eq(true)
 75            end
 76
 77            context 'and there is a specific env override' do
 78              before do
 79                stub_env(environment_config_file_name, 'instance specific override')
 80              end
 81
 82              it { expect(subject).to eq('instance specific override') }
 83            end
 84          end
 85        end
 86      end
 87    end
 88  end
 89
 90  describe '.store' do
 91    let(:rails_env) { 'development' }
 92
 93    subject { described_class.new(rails_env).store }
 94
 95    shared_examples 'redis store' do
 96      let(:redis_store) { ::Redis::Store }
 97      let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database}" }
 98
 99      it 'instantiates Redis::Store' do
100        is_expected.to be_a(redis_store)
101
102        expect(subject.to_s).to eq(redis_store_to_s)
103      end
104
105      context 'with the namespace' do
106        let(:namespace) { 'namespace_name' }
107        let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}" }
108
109        subject { described_class.new(rails_env).store(namespace: namespace) }
110
111        it "uses specified namespace" do
112          expect(subject.to_s).to eq(redis_store_to_s)
113        end
114      end
115    end
116
117    context 'with old format' do
118      it_behaves_like 'redis store' do
119        let(:config_file_name) { config_old_format_host }
120        let(:host) { "localhost:#{redis_port}" }
121      end
122    end
123
124    context 'with new format' do
125      it_behaves_like 'redis store' do
126        let(:config_file_name) { config_new_format_host }
127        let(:host) { "development-host:#{redis_port}" }
128      end
129    end
130  end
131
132  describe '.params' do
133    subject { described_class.new(rails_env).params }
134
135    let(:rails_env) { 'development' }
136    let(:config_file_name) { config_old_format_socket }
137
138    it 'withstands mutation' do
139      params1 = described_class.params
140      params2 = described_class.params
141      params1[:foo] = :bar
142
143      expect(params2).not_to have_key(:foo)
144    end
145
146    context 'when url contains unix socket reference' do
147      context 'with old format' do
148        let(:config_file_name) { config_old_format_socket }
149
150        it 'returns path key instead' do
151          is_expected.to include(path: old_socket_path)
152          is_expected.not_to have_key(:url)
153        end
154      end
155
156      context 'with new format' do
157        let(:config_file_name) { config_new_format_socket }
158
159        it 'returns path key instead' do
160          is_expected.to include(path: new_socket_path)
161          is_expected.not_to have_key(:url)
162        end
163      end
164    end
165
166    context 'when url is host based' do
167      context 'with old format' do
168        let(:config_file_name) { config_old_format_host }
169
170        it 'returns hash with host, port, db, and password' do
171          is_expected.to include(host: 'localhost', password: 'mypassword', port: redis_port, db: redis_database)
172          is_expected.not_to have_key(:url)
173        end
174      end
175
176      context 'with new format' do
177        let(:config_file_name) { config_new_format_host }
178
179        where(:rails_env, :host) do
180          [
181            %w[development development-host],
182            %w[test test-host],
183            %w[production production-host]
184          ]
185        end
186
187        with_them do
188          it 'returns hash with host, port, db, and password' do
189            is_expected.to include(host: host, password: 'mynewpassword', port: redis_port, db: redis_database)
190            is_expected.not_to have_key(:url)
191          end
192        end
193      end
194    end
195  end
196
197  describe '.url' do
198    let(:config_file_name) { config_old_format_socket }
199
200    it 'withstands mutation' do
201      url1 = described_class.url
202      url2 = described_class.url
203      url1 << 'foobar' unless url1.frozen?
204
205      expect(url2).not_to end_with('foobar')
206    end
207
208    context 'when yml file with env variable' do
209      let(:config_file_name) { config_with_environment_variable_inside }
210
211      before do
212        stub_env(config_env_variable_url, test_redis_url)
213      end
214
215      it 'reads redis url from env variable' do
216        expect(described_class.url).to eq test_redis_url
217      end
218    end
219  end
220
221  describe '.version' do
222    it 'returns a version' do
223      expect(described_class.version).to be_present
224    end
225  end
226
227  describe '._raw_config' do
228    subject { described_class._raw_config }
229
230    let(:config_file_name) { '/var/empty/doesnotexist' }
231
232    it 'is frozen' do
233      expect(subject).to be_frozen
234    end
235
236    it 'returns false when the file does not exist' do
237      expect(subject).to eq(false)
238    end
239
240    it "returns false when the filename can't be determined" do
241      expect(described_class).to receive(:config_file_name).and_return(nil)
242
243      expect(subject).to eq(false)
244    end
245  end
246
247  describe '.with' do
248    let(:config_file_name) { config_old_format_socket }
249
250    before do
251      clear_pool
252    end
253    after do
254      clear_pool
255    end
256
257    context 'when running on single-threaded runtime' do
258      before do
259        allow(Gitlab::Runtime).to receive(:multi_threaded?).and_return(false)
260      end
261
262      it 'instantiates a connection pool with size 5' do
263        expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original
264
265        described_class.with { |_redis_shared_example| true }
266      end
267    end
268
269    context 'when running on multi-threaded runtime' do
270      before do
271        allow(Gitlab::Runtime).to receive(:multi_threaded?).and_return(true)
272        allow(Gitlab::Runtime).to receive(:max_threads).and_return(18)
273      end
274
275      it 'instantiates a connection pool with a size based on the concurrency of the worker' do
276        expect(ConnectionPool).to receive(:new).with(size: 18 + 5).and_call_original
277
278        described_class.with { |_redis_shared_example| true }
279      end
280    end
281
282    context 'when there is no config at all' do
283      before do
284        # Undo top-level stub of config_file_name because we are testing that method now.
285        allow(described_class).to receive(:config_file_name).and_call_original
286
287        allow(described_class).to receive(:rails_root).and_return(rails_root)
288      end
289
290      after do
291        FileUtils.rm_rf(rails_root)
292      end
293
294      it 'can run an empty block' do
295        expect { described_class.with { nil } }.not_to raise_error
296      end
297    end
298  end
299
300  describe '#db' do
301    let(:rails_env) { 'development' }
302
303    subject { described_class.new(rails_env).db }
304
305    context 'with old format' do
306      let(:config_file_name) { config_old_format_host }
307
308      it 'returns the correct db' do
309        expect(subject).to eq(redis_database)
310      end
311    end
312
313    context 'with new format' do
314      let(:config_file_name) { config_new_format_host }
315
316      it 'returns the correct db' do
317        expect(subject).to eq(redis_database)
318      end
319    end
320  end
321
322  describe '#sentinels' do
323    subject { described_class.new(rails_env).sentinels }
324
325    let(:rails_env) { 'development' }
326
327    context 'when sentinels are defined' do
328      let(:config_file_name) { config_new_format_host }
329
330      where(:rails_env, :hosts) do
331        [
332          ['development', %w[development-replica1 development-replica2]],
333          ['test', %w[test-replica1 test-replica2]],
334          ['production', %w[production-replica1 production-replica2]]
335        ]
336      end
337
338      with_them do
339        it 'returns an array of hashes with host and port keys' do
340          is_expected.to include(host: hosts[0], port: sentinel_port)
341          is_expected.to include(host: hosts[1], port: sentinel_port)
342        end
343      end
344    end
345
346    context 'when sentinels are not defined' do
347      let(:config_file_name) { config_old_format_host }
348
349      it 'returns nil' do
350        is_expected.to be_nil
351      end
352    end
353  end
354
355  describe '#sentinels?' do
356    subject { described_class.new(Rails.env).sentinels? }
357
358    context 'when sentinels are defined' do
359      let(:config_file_name) { config_new_format_host }
360
361      it 'returns true' do
362        is_expected.to be_truthy
363      end
364    end
365
366    context 'when sentinels are not defined' do
367      let(:config_file_name) { config_old_format_host }
368
369      it 'returns false' do
370        is_expected.to be_falsey
371      end
372    end
373  end
374
375  describe '#raw_config_hash' do
376    it 'returns old-style single url config in a hash' do
377      expect(subject).to receive(:fetch_config) { test_redis_url }
378      expect(subject.send(:raw_config_hash)).to eq(url: test_redis_url)
379    end
380  end
381
382  describe '#fetch_config' do
383    it 'returns false when no config file is present' do
384      allow(described_class).to receive(:_raw_config) { false }
385
386      expect(subject.send(:fetch_config)).to eq false
387    end
388
389    it 'returns false when config file is present but has invalid YAML' do
390      allow(described_class).to receive(:_raw_config) { "# development: true" }
391
392      expect(subject.send(:fetch_config)).to eq false
393    end
394
395    it 'has a value for the legacy default URL' do
396      allow(subject).to receive(:fetch_config) { false }
397
398      expect(subject.send(:raw_config_hash)).to include(url: a_string_matching(%r{\Aredis://localhost:638[012]\Z}))
399    end
400  end
401
402  def clear_pool
403    described_class.remove_instance_variable(:@pool)
404  rescue NameError
405    # raised if @pool was not set; ignore
406  end
407end