Skip to content

Commit

Permalink
DEV: Make code compatible with Rails 7.1+
Browse files Browse the repository at this point in the history
Currently, we rely on `ActiveRecord::Base.connection_handlers` which has
been deprecated for some time now. It will break with Rails 7.1.

This patch migrates the code to rely on the “new” AR APIs.

To simplify things, this patch also drops Rails 6.0 & Ruby 2.7 support.
  • Loading branch information
Flink committed May 16, 2023
1 parent 041a7c3 commit 8de155d
Show file tree
Hide file tree
Showing 20 changed files with 140 additions and 166 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: ['2.7', '3.0', '3.1', '3.2']
ruby: ['3.0', '3.1', '3.2']

steps:
- uses: actions/checkout@v3
Expand All @@ -59,13 +59,13 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: ['3.2', '3.1', '3.0', '2.7']
ruby: ['3.2', '3.1', '3.0']
rails: ['7.0.0']
include:
- ruby: '3.2'
rails: '6.1.0'
- ruby: '3.2'
rails: '6.0.0'
rails: 'edge'

steps:
- uses: actions/checkout@v3
Expand Down
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
--color
--require spec_helper
--order rand
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.0.0] - 2023-05-16

- DEV: Compatibility with Rails 7.1+ (drop support for Rails 6.0 & ruby 2.7)

## [1.0.0] - 2023-04-07

- DEV: Remove the support for Ruby < 2.7
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ production:
replica_port: <replica db server port>
```

The gem will automatically create an `ActiveRecord::ConnectionAdapters::ConnectionHandler` with the `ActiveRecord.reading_role` as the `handler_key`.
The gem will automatically create a role (using `ActiveRecord.reading_role`) on
the default `ActiveRecord` connection handler.

#### Failover/Fallback Hooks

Expand All @@ -53,8 +54,6 @@ end

#### Multiple connection handlers

Note: This API is unstable and is likely to change when Rails 6.1 is released with sharding support.

```yml
# config/database.yml

Expand All @@ -72,7 +71,7 @@ production:

# In your ActiveRecord base model or model.

connects_to database: { writing: :primary, second_database_writing: :second_database_writing
connects_to database: { writing: :primary, second_database_writing: :second_database_writing }
```

### Redis
Expand Down
102 changes: 49 additions & 53 deletions lib/rails_failover/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,77 +7,73 @@
require_relative "active_record/middleware"
require_relative "active_record/handler"

AR =
(
if ::ActiveRecord.respond_to?(:reading_role)
::ActiveRecord
else
::ActiveRecord::Base
end
)

module RailsFailover
module ActiveRecord
def self.logger=(logger)
@logger = logger
end
class << self
def config
::ActiveRecord::Base.connection_db_config.configuration_hash
end

def self.logger
@logger || Rails.logger
end
def logger=(logger)
@logger = logger
end

def self.verify_primary_frequency_seconds=(seconds)
@verify_primary_frequency_seconds = seconds
end
def logger
@logger || Rails.logger
end

def self.verify_primary_frequency_seconds
@verify_primary_frequency_seconds || 5
end
def verify_primary_frequency_seconds=(seconds)
@verify_primary_frequency_seconds = seconds
end

def self.establish_reading_connection(handler, config)
if config[:replica_host] && config[:replica_port]
def verify_primary_frequency_seconds
@verify_primary_frequency_seconds || 5
end

def establish_reading_connection(handler, config, role: reading_role)
return unless config[:replica_host] && config[:replica_port]
replica_config = config.dup
replica_config[:host] = replica_config.delete(:replica_host)
replica_config[:port] = replica_config.delete(:replica_port)
replica_config[:replica] = true
handler.establish_connection(replica_config)
handler.establish_connection(replica_config, role: role)
end
end

def self.register_force_reading_role_callback(&block)
Middleware.force_reading_role_callback = block
end
def register_force_reading_role_callback(&block)
Middleware.force_reading_role_callback = block
end

def self.on_failover(&block)
@on_failover_callback = block
end
def on_failover(&block)
@on_failover_callback = block
end

def self.on_failover_callback!(key)
@on_failover_callback&.call(key)
rescue => e
logger.warn(
"RailsFailover::ActiveRecord.on_failover failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}",
)
end
def on_failover_callback!(key)
@on_failover_callback&.call(key)
rescue => e
logger.warn(
"RailsFailover::ActiveRecord.on_failover failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}",
)
end

def self.on_fallback(&block)
@on_fallback_callback = block
end
def on_fallback(&block)
@on_fallback_callback = block
end

def self.on_fallback_callback!(key)
@on_fallback_callback&.call(key)
rescue => e
logger.warn(
"RailsFailover::ActiveRecord.on_fallback failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}",
)
end
def on_fallback_callback!(key)
@on_fallback_callback&.call(key)
rescue => e
logger.warn(
"RailsFailover::ActiveRecord.on_fallback failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}",
)
end

def self.reading_role
AR.reading_role
end
def reading_role
::ActiveRecord.try(:reading_role) || ::ActiveRecord::Base.reading_role
end

def self.writing_role
AR.writing_role
def writing_role
::ActiveRecord.try(:writing_role) || ::ActiveRecord::Base.writing_role
end
end
end
end
16 changes: 5 additions & 11 deletions lib/rails_failover/active_record/handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,15 @@ def initiate_fallback_to_primary
active_handler_keys = []

primaries_down.keys.each do |handler_key|
connection_handler = ::ActiveRecord::Base.connection_handlers[handler_key]

connection_pool = connection_handler.retrieve_connection_pool(spec_name)
if connection_pool.respond_to?(:db_config)
config = connection_pool.db_config.configuration_hash
adapter_method = connection_pool.db_config.adapter_method
else
config = connection_pool.spec.config
adapter_method = connection_pool.spec.adapter_method
end
logger.debug "#{Process.pid} Checking server for '#{handler_key} #{spec_name}'..."
connection_active = false

begin
connection = ::ActiveRecord::Base.public_send(adapter_method, config)
connection =
::ActiveRecord::Base.connection_handler.retrieve_connection(
spec_name,
role: handler_key,
)
connection_active = connection.active?
rescue => e
logger.debug "#{Process.pid} Connection to server for '#{handler_key} #{spec_name}' failed with '#{e.message}'"
Expand Down
66 changes: 28 additions & 38 deletions lib/rails_failover/active_record/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ def call(env)
writing_role = resolve_writing_role(current_role, is_writing_role)

role =
if primary_down =
self.class.force_reading_role_callback&.call(env) ||
Handler.instance.primary_down?(writing_role)
if self.class.force_reading_role_callback&.call(env) ||
Handler.instance.primary_down?(writing_role)
reading_role = resolve_reading_role(current_role, is_writing_role)
ensure_reading_connection_established!(
writing_role: writing_role,
Expand All @@ -75,48 +74,39 @@ def call(env)
private

def ensure_reading_connection_established!(writing_role:, reading_role:)
::ActiveRecord::Base.connection_handlers[reading_role] ||= begin
handler = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new

::ActiveRecord::Base.connection_handlers[writing_role].connection_pools.each do |pool|
if pool.respond_to?(:db_config)
config = pool.db_config.configuration_hash
else
config = pool.spec.config
end
::RailsFailover::ActiveRecord.establish_reading_connection(handler, config)
connection_handler = ::ActiveRecord::Base.connection_handler
connection_handler
.connection_pools(writing_role)
.each do |connection_pool|
config = connection_pool.db_config.configuration_hash
RailsFailover::ActiveRecord.establish_reading_connection(
connection_handler,
config,
role: reading_role,
)
end

handler
end
end

def resolve_writing_role(current_role, is_writing_role)
if is_writing_role
current_role
else
current_role
.to_s
.sub(
/#{RailsFailover::ActiveRecord.reading_role}$/,
RailsFailover::ActiveRecord.writing_role.to_s,
)
.to_sym
end
return current_role if is_writing_role
current_role
.to_s
.sub(
/#{RailsFailover::ActiveRecord.reading_role}$/,
RailsFailover::ActiveRecord.writing_role.to_s,
)
.to_sym
end

def resolve_reading_role(current_role, is_writing_role)
if is_writing_role
current_role
.to_s
.sub(
/#{RailsFailover::ActiveRecord.writing_role}$/,
RailsFailover::ActiveRecord.reading_role.to_s,
)
.to_sym
else
current_role
end
return current_role unless is_writing_role
current_role
.to_s
.sub(
/#{RailsFailover::ActiveRecord.writing_role}$/,
RailsFailover::ActiveRecord.reading_role.to_s,
)
.to_sym
end
end
end
Expand Down
60 changes: 13 additions & 47 deletions lib/rails_failover/active_record/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,18 @@ module RailsFailover
module ActiveRecord
class Railtie < ::Rails::Railtie
initializer "rails_failover.init", after: "active_record.initialize_database" do |app|
# AR 6.0 / 6.1 compat
config =
if ::ActiveRecord::Base.respond_to? :connection_db_config
::ActiveRecord::Base.connection_db_config.configuration_hash
else
::ActiveRecord::Base.connection_config
end

app.config.active_record_rails_failover = false

if !!(config[:replica_host] && config[:replica_port])
app.config.active_record_rails_failover = true

::ActiveSupport.on_load(:active_record) do
Handler.instance

# We are doing this manually for now since we're awaiting Rails 6.1 to be released which will
# have more stable ActiveRecord APIs for handling multiple databases with different roles.
::ActiveRecord::Base.connection_handlers[
RailsFailover::ActiveRecord.reading_role
] = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new

::ActiveRecord::Base.connection_handlers[RailsFailover::ActiveRecord.writing_role]
.connection_pools
.each do |connection_pool|
if connection_pool.respond_to?(:db_config)
config = connection_pool.db_config.configuration_hash
else
config = connection_pool.spec.config
end
RailsFailover::ActiveRecord.establish_reading_connection(
::ActiveRecord::Base.connection_handlers[RailsFailover::ActiveRecord.reading_role],
config,
)
end

begin
::ActiveRecord::Base.connection
rescue ::ActiveRecord::NoDatabaseError
# Do nothing since database hasn't been created
rescue ::PG::Error, ::ActiveRecord::ConnectionNotEstablished
Handler.instance.verify_primary(RailsFailover::ActiveRecord.writing_role)
::ActiveRecord::Base.connection_handler =
::ActiveRecord::Base.lookup_connection_handler(:reading)
end
config = RailsFailover::ActiveRecord.config
break unless config[:replica_host] && config[:replica_port]

app.config.active_record_rails_failover = true
::ActiveSupport.on_load(:active_record) do
begin
::ActiveRecord::Base.connection
rescue ::ActiveRecord::NoDatabaseError
# Do nothing since database hasn't been created
rescue ::PG::Error, ::ActiveRecord::ConnectionNotEstablished
Handler.instance.verify_primary(RailsFailover::ActiveRecord.writing_role)
end
end
end
Expand All @@ -60,14 +27,13 @@ class Railtie < ::Rails::Railtie
end

if !skip_middleware?(app.config)
app.middleware.unshift(::RailsFailover::ActiveRecord::Middleware)
app.middleware.unshift(RailsFailover::ActiveRecord::Middleware)
end
end
end

def skip_middleware?(config)
return false if !config.respond_to?(:skip_rails_failover_active_record_middleware)
config.skip_rails_failover_active_record_middleware
config.try(:skip_rails_failover_active_record_middleware)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rails_failover/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module RailsFailover
VERSION = "1.0.0"
VERSION = "2.0.0"
end

0 comments on commit 8de155d

Please sign in to comment.