how to send (explicitly) a query to slave?
Hello , we are just started to use makara and is working very well on our production services.
we have migrated from Octopus. Just wondering how can we explicitly send queries to slave, is there a method or a block to achieve that ? in octupus you could do Model.find(foo).to_slave(:slave1)
is there something similar in makara ?
best
I don't believe there is a way to talk to a specific slave. Maybe something you could do under the hood.
But there is
ActiveRecord::Base.connection.without_sticking do
# stuff here
end
This should not force master and also no make it stick to master afterwards.
You can do something stupid like:
module MakaraPoolExtensions
def slave_connection
slave_connections.first
end
def slave_connections
@_slave_connections = nil
sql = "select * from foo".freeze
connection.send(:appropriate_pool, :execute, [sql]) { |pool| @_slave_connections = pool.connections }
@_slave_connections or fail "Slave connection expected but none found".freeze
end
def master_connections
@_master_connections = nil
sql = "insert into foo values (1,2)".freeze
connection.send(:appropriate_pool, :execute, [sql]) { |pool| @_master_connections = pool.connections }
@_master_connections or fail "Master connection expected but none found".freeze
end
end
ActiveSupport.on_load(:active_record) do extend MakaraPoolExtensions end
So that you can User.slave_connection if you wanted to get the raw connection.
I've also tried the below extension at one point, but it only works for requests:
module AlternativeMakaraPoolExtensions
def with_readonly_connection
current_context = Makara::Context.get_current
# https://github.com/taskrabbit/makara/blob/f2dfab140aaa7b182228b673acf578d04a9fce12/lib/makara/proxy.rb#L188-L196
# change current context in case it's the master context
Makara::Context.set_current Makara::Context.generate
yield
ensure
Makara::Context.set_current current_context
end
end
ActiveSupport.on_load(:active_record) do extend AlternativeMakaraPoolExtensions end
tested via
context 'database connection' do
let(:queries) { [] }
before do
@subscribe = ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, details|
if (connection_object_id = details[:connection_id])
adapter = ObjectSpace._id2ref(connection_object_id)
if adapter.respond_to?(:_makara_name)
sql = details[:sql]
queries << adapter._makara_name if sql =~ /SOME EXPECTED MATCHER/
end
end
end
end
after do
ActiveSupport::Notifications.unsubscribe(@subscriber)
end
it 'forces queries to read from the slave' do
api_request
expect(queries).to match [
"slave/1"
]
end
end
Model.find(foo).to_slave(:slave1)
This will be a SELECT query that'll go to the replica db in any case.
Another hack to send a non-SELECT query to the replica is to do a SELECT first, causing the subsequent query to stick to the same conn.
Some facility to use specific kind of connection would be desirable for sure (and really should be in AR proper).
For those wanting to force the usage of a slave (not a specific one). I ended up monkey patching the SQL_SLAVE_MATCHERS constant within an initializer. The code looks like this:
ActiveRecord::ConnectionAdapters::MakaraAbstractAdapter::SQL_SLAVE_MATCHERS = [/\A\s*select\s/i, /\/\*force slave\*\//i].map(&:freeze).freeze
we have several complex queries on our code and it looks like some of them were triggering the master node. With the previous modification, you just need to add a comment like this anywhere in your query:
/*force slave*/
plus, executing it within a ActiveRecord::Base.connection.without_sticking block like
ActiveRecord::Base.connection.without_sticking do
ActiveRecord::Base.connection.execute(some_forced_slave_query_string)
end
Not sure if that was the best idea but it's working just fine for us