/spec/support/redis/redis_shared_examples.rb
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