Commit 4e80a5fe authored by Sean McGivern's avatar Sean McGivern

Merge branch 'db-load-balancing-hijack-connection' into 'master'

Hijack ActiveRecord::Base.connection in the DB load balancer

Closes #3191

See merge request !2707
parents 60e42dd9 96685fc1
---
title: >
Ensure all database queries are routed through the database load balancer when
load balancing is enabled
merge_request: 2707
author:
type: changed
...@@ -61,15 +61,10 @@ module Gitlab ...@@ -61,15 +61,10 @@ module Gitlab
def self.configure_proxy def self.configure_proxy
self.proxy = ConnectionProxy.new(hosts) self.proxy = ConnectionProxy.new(hosts)
# ActiveRecordProxy's methods are made available as class methods in # This hijacks the "connection" method to ensure both
# ActiveRecord::Base, while still allowing the use of `super`. # `ActiveRecord::Base.connection` and all models use the same load
# balancing proxy.
ActiveRecord::Base.singleton_class.prepend(ActiveRecordProxy) ActiveRecord::Base.singleton_class.prepend(ActiveRecordProxy)
# The above will only patch newly defined models, so we also need to
# patch existing ones.
active_record_models.each do |model|
model.singleton_class.prepend(ModelProxy)
end
end end
def self.active_record_models def self.active_record_models
......
module Gitlab module Gitlab
module Database module Database
module LoadBalancing module LoadBalancing
# Module injected into ActiveRecord::Base to allow proxying of subclasses. # Module injected into ActiveRecord::Base to allow hijacking of the
# "connection" method.
module ActiveRecordProxy module ActiveRecordProxy
def inherited(by) def connection
super(by) LoadBalancing.proxy
# The methods in ModelProxy will become available as class methods for
# the class defined in `by`.
by.singleton_class.prepend(ModelProxy)
end end
end end
end end
......
...@@ -6,8 +6,8 @@ module Gitlab ...@@ -6,8 +6,8 @@ module Gitlab
# Each host in the load balancer uses the same credentials as the primary # Each host in the load balancer uses the same credentials as the primary
# database. # database.
# #
# This class *requires* that `ActiveRecord::Base.connection` always # This class *requires* that `ActiveRecord::Base.retrieve_connection`
# returns a connection to the primary. # always returns a connection to the primary.
class LoadBalancer class LoadBalancer
CACHE_KEY = :gitlab_load_balancer_host CACHE_KEY = :gitlab_load_balancer_host
...@@ -63,7 +63,7 @@ module Gitlab ...@@ -63,7 +63,7 @@ module Gitlab
# Instead of immediately grinding to a halt we'll retry the operation # Instead of immediately grinding to a halt we'll retry the operation
# a few times. # a few times.
retry_with_backoff do retry_with_backoff do
yield ActiveRecord::Base.connection yield ActiveRecord::Base.retrieve_connection
end end
end end
......
module Gitlab
module Database
module LoadBalancing
# Modle injected into models in order to redirect connections to a
# ConnectionProxy.
module ModelProxy
def connection
LoadBalancing.proxy
end
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Database::LoadBalancing::ActiveRecordProxy do describe Gitlab::Database::LoadBalancing::ActiveRecordProxy do
describe '#inherited' do describe '#connection' do
it 'adds the ModelProxy module to the singleton class' do it 'returns a connection proxy' do
base = Class.new do dummy = Class.new do
include Gitlab::Database::LoadBalancing::ActiveRecordProxy include Gitlab::Database::LoadBalancing::ActiveRecordProxy
end end
model = Class.new(base) proxy = double(:proxy)
expect(model.included_modules).to include(described_class) expect(Gitlab::Database::LoadBalancing).to receive(:proxy)
.and_return(proxy)
expect(dummy.new.connection).to eq(proxy)
end end
end end
end end
...@@ -86,14 +86,14 @@ describe Gitlab::Database::LoadBalancing::LoadBalancer do ...@@ -86,14 +86,14 @@ describe Gitlab::Database::LoadBalancing::LoadBalancer do
expect(lb).to receive(:read_write).and_call_original expect(lb).to receive(:read_write).and_call_original
expect { |b| lb.read(&b) } expect { |b| lb.read(&b) }
.to yield_with_args(ActiveRecord::Base.connection) .to yield_with_args(ActiveRecord::Base.retrieve_connection)
end end
end end
describe '#read_write' do describe '#read_write' do
it 'yields a connection for a write' do it 'yields a connection for a write' do
expect { |b| lb.read_write(&b) } expect { |b| lb.read_write(&b) }
.to yield_with_args(ActiveRecord::Base.connection) .to yield_with_args(ActiveRecord::Base.retrieve_connection)
end end
it 'uses a retry with exponential backoffs' do it 'uses a retry with exponential backoffs' do
......
require 'spec_helper'
describe Gitlab::Database::LoadBalancing::ModelProxy do
describe '#connection' do
it 'returns a connection proxy' do
dummy = Class.new do
include Gitlab::Database::LoadBalancing::ModelProxy
end
proxy = double(:proxy)
expect(Gitlab::Database::LoadBalancing).to receive(:proxy)
.and_return(proxy)
expect(dummy.new.connection).to eq(proxy)
end
end
end
...@@ -106,17 +106,9 @@ describe Gitlab::Database::LoadBalancing do ...@@ -106,17 +106,9 @@ describe Gitlab::Database::LoadBalancing do
end end
it 'configures the connection proxy' do it 'configures the connection proxy' do
model = double(:model)
expect(ActiveRecord::Base.singleton_class).to receive(:prepend) expect(ActiveRecord::Base.singleton_class).to receive(:prepend)
.with(Gitlab::Database::LoadBalancing::ActiveRecordProxy) .with(Gitlab::Database::LoadBalancing::ActiveRecordProxy)
expect(described_class).to receive(:active_record_models)
.and_return([model])
expect(model.singleton_class).to receive(:prepend)
.with(Gitlab::Database::LoadBalancing::ModelProxy)
described_class.configure_proxy described_class.configure_proxy
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment