Skip to content

Support Rails 6 multiple database connections. #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

masato-hi
Copy link

EN)
Fixed to disconnect all connection_handlers in ActiveRecord 6 and later versions.

JA)
ActiveRecord 6では ActiveRecord::Base#clear_all_connections! 及び ActiveRecord::Base#clear_active_connections!ActiveRecord::Base#default_connection_handler に処理が移譲されるため、
単一のデータベース接続を扱う場合は正常に動作しますが、複数のデータベース接続を扱う場合はdefault_connection_handler以外のconnection_handlerの接続が切れません。
(多くの場合default_connection_handlerはPrimaryデータベース, それ以外のconnection_handlerはReplicaデータベースへ接続されています)

ActiveRecord::Base#connection_handlers はActiveRecord 6で追加されたため、バージョン6以降とそれ以前で処理を分岐し、複数のデータベース接続においてもコネクションを切断出来るよう修正しました。

ActiveRecord::Base.clear_all_connections!
if ActiveRecord::VERSION::MAJOR >= 6
if should_clear_all_connections?
ActiveRecord::Base.connection_handlers.each_value do |connection_handler|
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your review.
Probably fixed to work with Rails 6.1.

Do you know a better way to get a list of roles?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -30,6 +33,60 @@ def call(env)
private

def clear_connections
if @ar_version >= AR_VERSION_6_1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can check the condition in definition time rather than runtime, checking it in runtime have a little overhead for the clear_connections method call.


def legacy_connection_handling?
begin
ActiveRecord::Base.legacy_connection_handling
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No raise NoMethodError since Rails 6.1 have ActiveRecord::Base.legacy_connection_handling.

@masato-hi
Copy link
Author

@kamipo
Thanks for the review and reference.

Removed the process when legacy_connection_handling is true because the connection to the replica will continue to remain.

@masato-hi masato-hi force-pushed the support-rails6-multi-db branch from 073114d to 0ece604 Compare June 4, 2021 09:58
if should_clear_all_connections?
ActiveRecord::Base.connection_handler.all_connection_pools.each(&:disconnect!)
else
ActiveRecord::Base.connection_handler.all_connection_pools.each do |pool|
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct for legacy_connection_handling = false but is not always correct for legacy_connection_handling = true.

@masato-hi
Copy link
Author

@kamipo
I'm sorry my verification wasn't enough.

For example, if you set ActiveRecord::Base.legacy_connection_handling to true and execute the following code will create a connection to primary in ActiveRecord::Base.connection_handler.

ActiveRecord::Base.connection.execute('SELECT 1 FROM DUAL;')

The connection is out of the management of ActiveRecord::Base.connection_handlers, so running the following code will not disconnect the connection.

ActiveRecord::Base.connection_handlers.each_value do |handler|
  handler.connection_pool_list.each(&:disconnect!)
end

I also need to run ActiveRecord::Base.connection_handler.all_connection_pools.each(&:disconnect!). Are there any concerns about that?

@masato-hi
Copy link
Author

@kamipo
The problem I'm having trouble with at the moment is something like the sample code below.

The problem is that one of the three cases is wrong or ActiveRecord::Base#connection is not returning the connection_pool in connection_handlers.

(1): Multiple databases should not be configured when legacy_connection_handling is enabled.
(2): You should not call connection outside the connected_to block when legacy_connection_handling is enabled.
(3): ActionRecord::Base#connected_to should not be called (ApplicationRecord # connected_to should be used).

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'

  gem 'rspec', require: 'rspec/autorun'
  gem 'rails', '6.1.3.2'
  gem 'sqlite3'
end

require 'active_record'

ActiveRecord::Base.legacy_connection_handling = true

# Point (1)
ActiveRecord::Base.configurations = (YAML.load(
<<-YAML
development:
  primary: &development
    adapter: sqlite3
    database: development.sqlite3
    pool: 5
    timeout: 5000
  primary_replica:
    <<: *development
    replica: true
YAML
))

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  # Point (1)
  connects_to database: {writing: :primary, reading: :primary_replica}
end

RSpec.describe 'activerecord-refresh_connection' do
  before do
    ActiveRecord::Base.establish_connection(:development)
    ActiveRecord::Base.connection_handlers.each_value do |handler|
      handler.connection_pool_list.each do |pool|
        raise 'expect to disconnected!' if pool.connected?
      end
    end
    raise 'expect to disconnected!' if ApplicationRecord.connected?
  end

  after do
    ActiveRecord::Base.connection_handlers.each_value do |handler|
      handler.connection_pool_list.each(&:disconnect!)
    end
    ActiveRecord::Base.clear_all_connections!
  end

  before 'Connect all connections' do
    # Point (2)
    ApplicationRecord.connection.execute('SELECT 1')

    # Point (3)
    ActiveRecord::Base.connected_to(role: :writing) do
      ApplicationRecord.connection.execute('SELECT 1')
    end
    ActiveRecord::Base.connected_to(role: :reading) do
      ApplicationRecord.connection.execute('SELECT 1')
    end
  end

  context 'Check connected to all connections' do
    it do
      ActiveRecord::Base.connection_handlers.each_value do |handler|
        expect(handler.connection_pool_list(:writing).count(&:connected?)).to eq(1)
        expect(handler.connection_pool_list(:reading).count(&:connected?)).to eq(1)
      end
      expect(ApplicationRecord.connected?).to be_truthy
    end
  end

  context 'When only call ActiveRecord::ConnectionAdapters::ConnectionPool#disconnect!' do
    before do
      ActiveRecord::Base.connection_handlers.each_value do |handler|
        handler.connection_pool_list.each(&:disconnect!)
      end
    end

    it do
      ActiveRecord::Base.connection_handlers.each_value do |handler|
        expect(handler.connection_pool_list(:writing).count(&:connected?)).to eq(0)
        expect(handler.connection_pool_list(:reading).count(&:connected?)).to eq(0)
      end
      expect(ApplicationRecord.connected?).to be_truthy
    end
  end

  context 'When only call ActiveRecord::Base#clear_all_connections!' do
    before do
      ActiveRecord::Base.clear_all_connections!
    end

    it do
      ActiveRecord::Base.connection_handlers.each_value do |handler|
        expect(handler.connection_pool_list(:writing).count(&:connected?)).to eq(1)
        expect(handler.connection_pool_list(:reading).count(&:connected?)).to eq(1)
      end
      expect(ApplicationRecord.connected?).to be_falsey
    end
  end

  context 'When call all disconnect methods.' do
    before do
      ActiveRecord::Base.connection_handlers.each_value do |handler|
        handler.connection_pool_list.each(&:disconnect!)
      end
      ActiveRecord::Base.clear_all_connections!
    end

    it do
      ActiveRecord::Base.connection_handlers.each_value do |handler|
        expect(handler.connection_pool_list(:writing).count(&:connected?)).to eq(0)
        expect(handler.connection_pool_list(:reading).count(&:connected?)).to eq(0)
      end
      expect(ApplicationRecord.connected?).to be_falsey
    end
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants