makara icon indicating copy to clipboard operation
makara copied to clipboard

how to send (explicitly) a query to slave?

Open michelson opened this issue 9 years ago • 4 comments

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

michelson avatar Dec 19 '16 17:12 michelson

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.

bleonard avatar Dec 20 '16 21:12 bleonard

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

bf4 avatar Jan 06 '17 02:01 bf4

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).

jeremy avatar Mar 02 '17 19:03 jeremy

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

raosev avatar Mar 01 '18 04:03 raosev