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