/ee/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb

https://gitlab.com/klml/gitlab-ee · Ruby · 316 lines · 243 code · 69 blank · 4 comment · 0 complexity · d2ec407288b9f509d168731ce9bc7b34 MD5 · raw file

  1. # frozen_string_literal: true
  2. require 'spec_helper'
  3. RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
  4. let(:proxy) { described_class.new }
  5. describe '#select' do
  6. it 'performs a read' do
  7. expect(proxy).to receive(:read_using_load_balancer).with(:select, ['foo'])
  8. proxy.select('foo')
  9. end
  10. end
  11. describe '#select_all' do
  12. let(:override_proxy) { ActiveRecord::Base.connection.class }
  13. # We can't use :Gitlab::Utils::Override because this method is dynamically prepended
  14. it 'method signatures match' do
  15. expect(proxy.method(:select_all).parameters).to eq(override_proxy.instance_method(:select_all).parameters)
  16. end
  17. describe 'using a SELECT query' do
  18. it 'runs the query on a secondary' do
  19. arel = double(:arel)
  20. expect(proxy).to receive(:read_using_load_balancer)
  21. .with(:select_all, [arel, 'foo', []])
  22. proxy.select_all(arel, 'foo')
  23. end
  24. end
  25. describe 'using a SELECT FOR UPDATE query' do
  26. it 'runs the query on the primary and sticks to it' do
  27. arel = double(:arel, locked: true)
  28. expect(proxy).to receive(:write_using_load_balancer)
  29. .with(:select_all, [arel, 'foo', []], sticky: true)
  30. proxy.select_all(arel, 'foo')
  31. end
  32. end
  33. end
  34. Gitlab::Database::LoadBalancing::ConnectionProxy::NON_STICKY_READS.each do |name|
  35. describe "#{name}" do
  36. it 'runs the query on the replica' do
  37. expect(proxy).to receive(:read_using_load_balancer)
  38. .with(name, ['foo'])
  39. proxy.send(name, 'foo')
  40. end
  41. end
  42. end
  43. Gitlab::Database::LoadBalancing::ConnectionProxy::STICKY_WRITES.each do |name|
  44. describe "#{name}" do
  45. it 'runs the query on the primary and sticks to it' do
  46. expect(proxy).to receive(:write_using_load_balancer)
  47. .with(name, ['foo'], sticky: true)
  48. proxy.send(name, 'foo')
  49. end
  50. end
  51. end
  52. describe '.insert_all!' do
  53. before do
  54. ActiveRecord::Schema.define do
  55. create_table :connection_proxy_bulk_insert, force: true do |t|
  56. t.string :name, null: true
  57. end
  58. end
  59. end
  60. after do
  61. ActiveRecord::Schema.define do
  62. drop_table :connection_proxy_bulk_insert, force: true
  63. end
  64. end
  65. let(:model_class) do
  66. Class.new(ApplicationRecord) do
  67. self.table_name = "connection_proxy_bulk_insert"
  68. end
  69. end
  70. it 'inserts data in bulk' do
  71. expect(model_class).to receive(:connection)
  72. .at_least(:once)
  73. .and_return(proxy)
  74. expect(proxy).to receive(:write_using_load_balancer)
  75. .at_least(:once)
  76. .and_call_original
  77. expect do
  78. model_class.insert_all! [
  79. { name: "item1" },
  80. { name: "item2" }
  81. ]
  82. end.to change { model_class.count }.by(2)
  83. end
  84. end
  85. # We have an extra test for #transaction here to make sure that nested queries
  86. # are also sent to a primary.
  87. describe '#transaction' do
  88. let(:session) { double(:session) }
  89. before do
  90. allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
  91. .and_return(session)
  92. end
  93. context 'session fallbacks ambiguous queries to replicas' do
  94. let(:replica) { double(:connection) }
  95. before do
  96. allow(session).to receive(:fallback_to_replicas_for_ambiguous_queries?).and_return(true)
  97. allow(session).to receive(:use_primary?).and_return(false)
  98. allow(replica).to receive(:transaction).and_yield
  99. allow(replica).to receive(:select)
  100. end
  101. context 'with a read query' do
  102. it 'runs the transaction and any nested queries on the replica' do
  103. expect(proxy.load_balancer).to receive(:read)
  104. .twice.and_yield(replica)
  105. expect(proxy.load_balancer).not_to receive(:read_write)
  106. expect(session).not_to receive(:write!)
  107. proxy.transaction { proxy.select('true') }
  108. end
  109. end
  110. context 'with a write query' do
  111. it 'raises an exception' do
  112. allow(proxy.load_balancer).to receive(:read).and_yield(replica)
  113. allow(proxy.load_balancer).to receive(:read_write).and_yield(replica)
  114. expect do
  115. proxy.transaction { proxy.insert('something') }
  116. end.to raise_error(Gitlab::Database::LoadBalancing::ConnectionProxy::WriteInsideReadOnlyTransactionError)
  117. end
  118. end
  119. end
  120. context 'session does not fallback to replicas for ambiguous queries' do
  121. let(:primary) { double(:connection) }
  122. before do
  123. allow(session).to receive(:fallback_to_replicas_for_ambiguous_queries?).and_return(false)
  124. allow(session).to receive(:use_replicas_for_read_queries?).and_return(false)
  125. allow(session).to receive(:use_primary?).and_return(true)
  126. allow(primary).to receive(:transaction).and_yield
  127. allow(primary).to receive(:select)
  128. allow(primary).to receive(:insert)
  129. end
  130. context 'with a read query' do
  131. it 'runs the transaction and any nested queries on the primary and stick to it' do
  132. expect(proxy.load_balancer).to receive(:read_write)
  133. .twice.and_yield(primary)
  134. expect(proxy.load_balancer).not_to receive(:read)
  135. expect(session).to receive(:write!)
  136. proxy.transaction { proxy.select('true') }
  137. end
  138. end
  139. context 'with a write query' do
  140. it 'runs the transaction and any nested queries on the primary and stick to it' do
  141. expect(proxy.load_balancer).to receive(:read_write)
  142. .twice.and_yield(primary)
  143. expect(proxy.load_balancer).not_to receive(:read)
  144. expect(session).to receive(:write!).twice
  145. proxy.transaction { proxy.insert('something') }
  146. end
  147. end
  148. end
  149. end
  150. describe '#method_missing' do
  151. it 'runs the query on the primary without sticking to it' do
  152. expect(proxy).to receive(:write_using_load_balancer)
  153. .with(:foo, ['foo'])
  154. proxy.foo('foo')
  155. end
  156. it 'properly forwards trailing hash arguments' do
  157. allow(proxy.load_balancer).to receive(:read_write)
  158. expect(proxy).to receive(:write_using_load_balancer).and_call_original
  159. expect { proxy.case_sensitive_comparison(:table, :attribute, :column, { value: :value, format: :format }) }
  160. .not_to raise_error
  161. end
  162. context 'current session prefers to fallback ambiguous queries to replicas' do
  163. let(:session) { double(:session) }
  164. before do
  165. allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
  166. .and_return(session)
  167. allow(session).to receive(:fallback_to_replicas_for_ambiguous_queries?).and_return(true)
  168. allow(session).to receive(:use_primary?).and_return(false)
  169. end
  170. it 'runs the query on the replica' do
  171. expect(proxy).to receive(:read_using_load_balancer).with(:foo, ['foo'])
  172. proxy.foo('foo')
  173. end
  174. it 'properly forwards trailing hash arguments' do
  175. allow(proxy.load_balancer).to receive(:read)
  176. expect(proxy).to receive(:read_using_load_balancer).and_call_original
  177. expect { proxy.case_sensitive_comparison(:table, :attribute, :column, { value: :value, format: :format }) }
  178. .not_to raise_error
  179. end
  180. end
  181. end
  182. describe '#read_using_load_balancer' do
  183. let(:session) { double(:session) }
  184. let(:connection) { double(:connection) }
  185. before do
  186. allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
  187. .and_return(session)
  188. end
  189. context 'with a regular session' do
  190. it 'uses a secondary' do
  191. allow(session).to receive(:use_primary?).and_return(false)
  192. allow(session).to receive(:use_replicas_for_read_queries?).and_return(false)
  193. expect(connection).to receive(:foo).with('foo')
  194. expect(proxy.load_balancer).to receive(:read).and_yield(connection)
  195. proxy.read_using_load_balancer(:foo, ['foo'])
  196. end
  197. end
  198. context 'with a regular session and forcing all reads to replicas' do
  199. it 'uses a secondary' do
  200. allow(session).to receive(:use_primary?).and_return(false)
  201. allow(session).to receive(:use_replicas_for_read_queries?).and_return(true)
  202. expect(connection).to receive(:foo).with('foo')
  203. expect(proxy.load_balancer).to receive(:read).and_yield(connection)
  204. proxy.read_using_load_balancer(:foo, ['foo'])
  205. end
  206. end
  207. context 'with a session using the primary but forcing all reads to replicas' do
  208. it 'uses a secondary' do
  209. allow(session).to receive(:use_primary?).and_return(true)
  210. allow(session).to receive(:use_replicas_for_read_queries?).and_return(true)
  211. expect(connection).to receive(:foo).with('foo')
  212. expect(proxy.load_balancer).to receive(:read).and_yield(connection)
  213. proxy.read_using_load_balancer(:foo, ['foo'])
  214. end
  215. end
  216. describe 'with a session using the primary' do
  217. it 'uses the primary' do
  218. allow(session).to receive(:use_primary?).and_return(true)
  219. allow(session).to receive(:use_replicas_for_read_queries?).and_return(false)
  220. expect(connection).to receive(:foo).with('foo')
  221. expect(proxy.load_balancer).to receive(:read_write)
  222. .and_yield(connection)
  223. proxy.read_using_load_balancer(:foo, ['foo'])
  224. end
  225. end
  226. end
  227. describe '#write_using_load_balancer' do
  228. let(:session) { double(:session) }
  229. let(:connection) { double(:connection) }
  230. before do
  231. allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
  232. .and_return(session)
  233. end
  234. it 'uses but does not stick to the primary when sticking is disabled' do
  235. expect(proxy.load_balancer).to receive(:read_write).and_yield(connection)
  236. expect(connection).to receive(:foo).with('foo')
  237. expect(session).not_to receive(:write!)
  238. proxy.write_using_load_balancer(:foo, ['foo'])
  239. end
  240. it 'sticks to the primary when sticking is enabled' do
  241. expect(proxy.load_balancer).to receive(:read_write).and_yield(connection)
  242. expect(connection).to receive(:foo).with('foo')
  243. expect(session).to receive(:write!)
  244. proxy.write_using_load_balancer(:foo, ['foo'], sticky: true)
  245. end
  246. end
  247. end