From 6d4b94b4ed0f09e6c6d1c05786f7e92accac6510 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Sun, 25 Aug 2024 17:12:18 +0100
Subject: [PATCH 01/20] Update `Fiber.defer`
the new method postpones releasing the connection to the next iteration of the event loop and only releases it if the file descriptor has changed since the last `io_wait` call. This ensures we are not releasing the connection until the DB query is completed (there can be several `io_wait` calls for one query)
The change is mostly caused by the bug in AR 7.0+ that makes the inital `Fiber.defer` approach unstable.
We also disable the method for AR 7.2+, as it doesn't hold onto connections anymore.
---
lib/rage/cable/channel.rb | 1 +
lib/rage/configuration.rb | 4 ++++
lib/rage/controller/api.rb | 4 ++--
lib/rage/ext/setup.rb | 32 +++++++++++++++++++++++---------
lib/rage/fiber.rb | 3 +++
lib/rage/fiber_scheduler.rb | 2 +-
6 files changed, 34 insertions(+), 12 deletions(-)
diff --git a/lib/rage/cable/channel.rb b/lib/rage/cable/channel.rb
index 5377578c..1eb4dabd 100644
--- a/lib/rage/cable/channel.rb
+++ b/lib/rage/cable/channel.rb
@@ -166,6 +166,7 @@ def __run_#{action_name}(data)
#{if activerecord_loaded
<<~RUBY
ensure
+ # TODO
if ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Base.connection_handler.clear_active_connections!
end
diff --git a/lib/rage/configuration.rb b/lib/rage/configuration.rb
index 50f2c569..cea6a5e9 100644
--- a/lib/rage/configuration.rb
+++ b/lib/rage/configuration.rb
@@ -264,6 +264,10 @@ class PublicFileServer
class Internal
attr_accessor :rails_mode
+ def patch_ar_pool?
+ ENV["RAGE_PATCH_AR_POOL"] && !Rage.env.test?
+ end
+
def inspect
"#<#{self.class.name}>"
end
diff --git a/lib/rage/controller/api.rb b/lib/rage/controller/api.rb
index 9894fe1b..00de4d36 100644
--- a/lib/rage/controller/api.rb
+++ b/lib/rage/controller/api.rb
@@ -122,8 +122,8 @@ def __run_#{action}
#{if activerecord_loaded
<<~RUBY
ActiveRecord::Base.connection_pool.disable_query_cache!
- if ActiveRecord::Base.connection_pool.active_connection?
- ActiveRecord::Base.connection_handler.clear_active_connections!
+ if ActiveRecord::Base.connection_handler.active_connections?(:all)
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
end
RUBY
end}
diff --git a/lib/rage/ext/setup.rb b/lib/rage/ext/setup.rb
index aeac011b..a9aa914b 100644
--- a/lib/rage/ext/setup.rb
+++ b/lib/rage/ext/setup.rb
@@ -4,16 +4,30 @@
end
# release ActiveRecord connections on yield
-if defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("7.1.0")
- class Fiber
- def self.defer
- res = Fiber.yield
+if defined?(ActiveRecord) && Rage.config.internal.patch_ar_pool?
+ if ActiveRecord.version >= Gem::Version.create("7.2.0")
+ # yay! AR 7.2+ uses `with_connection` internaly - no need to use `Fiber.defer`
+ if ENV["RAGE_DISABLE_AR_WEAK_CONNECTIONS"]
+ puts "WARNING: The RAGE_DISABLE_AR_WEAK_CONNECTIONS setting does not have any effect in Rails 7.2+"
+ end
+ elsif !ENV["RAGE_DISABLE_AR_WEAK_CONNECTIONS"]
+ class Fiber
+ def self.defer(fileno)
+ f = Fiber.current
+ f.__awaited_fileno = fileno
- if ActiveRecord::Base.connection_pool.active_connection?
- ActiveRecord::Base.connection_handler.clear_active_connections!
- end
+ res = Fiber.yield
- res
+ if ActiveRecord::Base.connection_handler.active_connections?(:all)
+ Iodine.defer do
+ if fileno != f.__awaited_fileno
+ ActiveRecord::Base.connection_handler.connection_pools(:all).each { |pool| pool.release_connection(f) }
+ end
+ end
+ end
+
+ res
+ end
end
end
end
@@ -31,6 +45,6 @@ def connection_cache_key(_)
end
# patch `ActiveRecord::ConnectionPool`
-if defined?(ActiveRecord) && ENV["RAGE_PATCH_AR_POOL"] && !Rage.env.test?
+if defined?(ActiveRecord) && Rage.config.internal.patch_ar_pool?
Rage.patch_active_record_connection_pool
end
diff --git a/lib/rage/fiber.rb b/lib/rage/fiber.rb
index dda50921..7c7667ed 100644
--- a/lib/rage/fiber.rb
+++ b/lib/rage/fiber.rb
@@ -81,6 +81,9 @@ def __block_channel(force = false)
"block:#{object_id}:#{@__block_channel_i}"
end
+ # @private
+ attr_accessor :__awaited_fileno
+
# @private
# pause a fiber and resume in the next iteration of the event loop
def self.pause
diff --git a/lib/rage/fiber_scheduler.rb b/lib/rage/fiber_scheduler.rb
index b577012d..7d614754 100644
--- a/lib/rage/fiber_scheduler.rb
+++ b/lib/rage/fiber_scheduler.rb
@@ -14,7 +14,7 @@ def io_wait(io, events, timeout = nil)
f = Fiber.current
::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil || 0) { |err| f.resume(err) }
- err = Fiber.defer
+ err = Fiber.defer(io.fileno)
if err == Errno::ETIMEDOUT::Errno
0
else
From 42bae11b0cefd3daf7366d0adf4b6774c870c7e0 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Sun, 25 Aug 2024 17:14:34 +0100
Subject: [PATCH 02/20] Patch AR pool by default
---
lib/rage/configuration.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/rage/configuration.rb b/lib/rage/configuration.rb
index cea6a5e9..b5959f3a 100644
--- a/lib/rage/configuration.rb
+++ b/lib/rage/configuration.rb
@@ -265,7 +265,7 @@ class Internal
attr_accessor :rails_mode
def patch_ar_pool?
- ENV["RAGE_PATCH_AR_POOL"] && !Rage.env.test?
+ !ENV["RAGE_DISABLE_AR_POOL_PATCH"] && !Rage.env.test?
end
def inspect
From 52766f4f32817926adf6c444cf6c7fe46c3b36a9 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Mon, 26 Aug 2024 16:45:33 +0100
Subject: [PATCH 03/20] Add `manually_release_ar_connections?` helper
---
lib/rage/configuration.rb | 6 ++++++
lib/rage/ext/setup.rb | 10 +++++-----
2 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/lib/rage/configuration.rb b/lib/rage/configuration.rb
index b5959f3a..7faad39f 100644
--- a/lib/rage/configuration.rb
+++ b/lib/rage/configuration.rb
@@ -268,6 +268,12 @@ def patch_ar_pool?
!ENV["RAGE_DISABLE_AR_POOL_PATCH"] && !Rage.env.test?
end
+ # whether we should manually release AR connections;
+ # AR 7.2+ uses `with_connection` internaly, so we only need to do this for older versions;
+ def manually_release_ar_connections?
+ defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("7.2.0")
+ end
+
def inspect
"#<#{self.class.name}>"
end
diff --git a/lib/rage/ext/setup.rb b/lib/rage/ext/setup.rb
index a9aa914b..08003239 100644
--- a/lib/rage/ext/setup.rb
+++ b/lib/rage/ext/setup.rb
@@ -5,12 +5,12 @@
# release ActiveRecord connections on yield
if defined?(ActiveRecord) && Rage.config.internal.patch_ar_pool?
- if ActiveRecord.version >= Gem::Version.create("7.2.0")
- # yay! AR 7.2+ uses `with_connection` internaly - no need to use `Fiber.defer`
- if ENV["RAGE_DISABLE_AR_WEAK_CONNECTIONS"]
- puts "WARNING: The RAGE_DISABLE_AR_WEAK_CONNECTIONS setting does not have any effect in Rails 7.2+"
+ if ENV["RAGE_DISABLE_AR_WEAK_CONNECTIONS"]
+ unless Rage.config.internal.manually_release_ar_connections?
+ puts "WARNING: The RAGE_DISABLE_AR_WEAK_CONNECTIONS setting does not have any effect with Active Record 7.2+"
end
- elsif !ENV["RAGE_DISABLE_AR_WEAK_CONNECTIONS"]
+ # no-op
+ elsif Rage.config.internal.manually_release_ar_connections?
class Fiber
def self.defer(fileno)
f = Fiber.current
From e9a814e0e8f8cebe8a700ed3f4ee207437c7e1f6 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Mon, 26 Aug 2024 16:46:05 +0100
Subject: [PATCH 04/20] Stop manually releasing connections in AR 7.2+
---
lib/rage/controller/api.rb | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/lib/rage/controller/api.rb b/lib/rage/controller/api.rb
index 00de4d36..e0c575d8 100644
--- a/lib/rage/controller/api.rb
+++ b/lib/rage/controller/api.rb
@@ -76,8 +76,6 @@ def __register_action(action)
""
end
- activerecord_loaded = defined?(::ActiveRecord)
-
wrap_parameters_chunk = if __wrap_parameters_key
<<~RUBY
wrap_key = self.class.__wrap_parameters_key
@@ -95,9 +93,12 @@ def __register_action(action)
RUBY
end
+ query_cache_enabled = defined?(::ActiveRecord)
+ should_release_connections = Rage.config.internal.manually_release_ar_connections?
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def __run_#{action}
- #{if activerecord_loaded
+ #{if query_cache_enabled
<<~RUBY
ActiveRecord::Base.connection_pool.enable_query_cache!
RUBY
@@ -119,9 +120,14 @@ def __run_#{action}
#{rescue_handlers_chunk}
ensure
- #{if activerecord_loaded
+ #{if query_cache_enabled
<<~RUBY
ActiveRecord::Base.connection_pool.disable_query_cache!
+ RUBY
+ end}
+
+ #{if should_release_connections
+ <<~RUBY
if ActiveRecord::Base.connection_handler.active_connections?(:all)
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
end
From 606855d1109b2e1536e5a51026ac81ffe42b1732 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Mon, 26 Aug 2024 19:00:23 +0100
Subject: [PATCH 05/20] Stop using the :all option
AR 6.0 doesn't support passing the role to the connection handler methods
---
lib/rage/controller/api.rb | 4 ++--
lib/rage/ext/setup.rb | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/lib/rage/controller/api.rb b/lib/rage/controller/api.rb
index e0c575d8..d5e3e4ca 100644
--- a/lib/rage/controller/api.rb
+++ b/lib/rage/controller/api.rb
@@ -128,8 +128,8 @@ def __run_#{action}
#{if should_release_connections
<<~RUBY
- if ActiveRecord::Base.connection_handler.active_connections?(:all)
- ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
+ if ActiveRecord::Base.connection_handler.active_connections?
+ ActiveRecord::Base.connection_handler.clear_active_connections!
end
RUBY
end}
diff --git a/lib/rage/ext/setup.rb b/lib/rage/ext/setup.rb
index 08003239..1b53b696 100644
--- a/lib/rage/ext/setup.rb
+++ b/lib/rage/ext/setup.rb
@@ -18,10 +18,10 @@ def self.defer(fileno)
res = Fiber.yield
- if ActiveRecord::Base.connection_handler.active_connections?(:all)
+ if ActiveRecord::Base.connection_handler.active_connections?
Iodine.defer do
if fileno != f.__awaited_fileno
- ActiveRecord::Base.connection_handler.connection_pools(:all).each { |pool| pool.release_connection(f) }
+ ActiveRecord::Base.connection_handler.connection_pools.each { |pool| pool.release_connection(f) }
end
end
end
From 5a06c89c72bb6ab4570e86f0a0c3f092ac256000 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Tue, 27 Aug 2024 19:44:34 +0100
Subject: [PATCH 06/20] Monitor the health of AR connections
---
lib/rage/configuration.rb | 6 +++
lib/rage/ext/active_record/connection_pool.rb | 40 +++++++++++++++++++
2 files changed, 46 insertions(+)
diff --git a/lib/rage/configuration.rb b/lib/rage/configuration.rb
index 7faad39f..84ff882d 100644
--- a/lib/rage/configuration.rb
+++ b/lib/rage/configuration.rb
@@ -274,6 +274,12 @@ def manually_release_ar_connections?
defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("7.2.0")
end
+ # whether we should manually reconnect closed AR connections;
+ # AR 7.1+ does this automatically while executing the query;
+ def should_manually_restore_ar_connections?
+ defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("7.1.0")
+ end
+
def inspect
"#<#{self.class.name}>"
end
diff --git a/lib/rage/ext/active_record/connection_pool.rb b/lib/rage/ext/active_record/connection_pool.rb
index cb6ed07e..c2cac283 100644
--- a/lib/rage/ext/active_record/connection_pool.rb
+++ b/lib/rage/ext/active_record/connection_pool.rb
@@ -24,11 +24,30 @@ def to_a
end
end
+ # reconnect closed connections on checkout;
+ # only included with `Rage.config.should_manually_restore_ar_connections?`
+ module ConnectionWithVerify
+ def connection
+ conn = super
+
+ if conn.__needs_reconnect
+ conn.reconnect!
+ conn.__needs_reconnect = false
+ end
+
+ conn
+ end
+ end
+ if Rage.config.internal.should_manually_restore_ar_connections?
+ prepend ConnectionWithVerify
+ end
+
def self.extended(instance)
instance.class.alias_method :__checkout__, :checkout
instance.class.alias_method :__remove__, :remove
ActiveRecord::ConnectionAdapters::AbstractAdapter.attr_accessor(:__idle_since)
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.attr_accessor(:__needs_reconnect)
end
def __init_rage_extension
@@ -65,6 +84,26 @@ def __init_rage_extension
end
end
+ # monitor connections health
+ if Rage.config.internal.should_manually_restore_ar_connections?
+ Iodine.run_every(1_000) do
+ i = 0
+ while i < @__connections.length
+ conn = @__connections[i]
+
+ unless conn.__needs_reconnect
+ needs_reconnect = !conn.active? rescue true
+ if needs_reconnect
+ conn.__needs_reconnect = true
+ conn.disconnect!
+ end
+ end
+
+ i += 1
+ end
+ end
+ end
+
# resume blocked fibers once connections become available
Iodine.subscribe("ext:ar-connection-released") do
if @__blocked.length > 0 && @__connections.length > 0
@@ -135,6 +174,7 @@ def flush(minimum_idle = @__idle_timeout)
conn = @__connections[i]
if conn.__idle_since && current_time - conn.__idle_since >= minimum_idle
conn.__idle_since = nil
+ conn.__needs_reconnect = true
conn.disconnect!
end
i += 1
From 8e79c5675873e0ee7b288f4f3e6677993c42c80e Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Sun, 1 Sep 2024 16:51:15 +0100
Subject: [PATCH 07/20] Support multiple errors in `io_wait`
---
lib/rage/fiber_scheduler.rb | 4 ++--
rage.gemspec | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/lib/rage/fiber_scheduler.rb b/lib/rage/fiber_scheduler.rb
index 7d614754..56b37189 100644
--- a/lib/rage/fiber_scheduler.rb
+++ b/lib/rage/fiber_scheduler.rb
@@ -15,8 +15,8 @@ def io_wait(io, events, timeout = nil)
::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil || 0) { |err| f.resume(err) }
err = Fiber.defer(io.fileno)
- if err == Errno::ETIMEDOUT::Errno
- 0
+ if err && err < 0
+ err
else
events
end
diff --git a/rage.gemspec b/rage.gemspec
index e095bb11..fc44c96d 100644
--- a/rage.gemspec
+++ b/rage.gemspec
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "thor", "~> 1.0"
spec.add_dependency "rack", "~> 2.0"
- spec.add_dependency "rage-iodine", "~> 3.0"
+ spec.add_dependency "rage-iodine", "~> 4.0"
spec.add_dependency "zeitwerk", "~> 2.6"
spec.add_dependency "rack-test", "~> 2.1"
end
From 313a249a64a25d55b13922184682761de53d7133 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Thu, 5 Sep 2024 16:39:14 +0100
Subject: [PATCH 08/20] Update method name
---
lib/rage/configuration.rb | 2 +-
lib/rage/controller/api.rb | 2 +-
lib/rage/ext/setup.rb | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/lib/rage/configuration.rb b/lib/rage/configuration.rb
index 84ff882d..4f335bed 100644
--- a/lib/rage/configuration.rb
+++ b/lib/rage/configuration.rb
@@ -270,7 +270,7 @@ def patch_ar_pool?
# whether we should manually release AR connections;
# AR 7.2+ uses `with_connection` internaly, so we only need to do this for older versions;
- def manually_release_ar_connections?
+ def should_manually_release_ar_connections?
defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("7.2.0")
end
diff --git a/lib/rage/controller/api.rb b/lib/rage/controller/api.rb
index d5e3e4ca..d7ecd89d 100644
--- a/lib/rage/controller/api.rb
+++ b/lib/rage/controller/api.rb
@@ -94,7 +94,7 @@ def __register_action(action)
end
query_cache_enabled = defined?(::ActiveRecord)
- should_release_connections = Rage.config.internal.manually_release_ar_connections?
+ should_release_connections = Rage.config.internal.should_manually_release_ar_connections?
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def __run_#{action}
diff --git a/lib/rage/ext/setup.rb b/lib/rage/ext/setup.rb
index 1b53b696..3947731c 100644
--- a/lib/rage/ext/setup.rb
+++ b/lib/rage/ext/setup.rb
@@ -6,11 +6,11 @@
# release ActiveRecord connections on yield
if defined?(ActiveRecord) && Rage.config.internal.patch_ar_pool?
if ENV["RAGE_DISABLE_AR_WEAK_CONNECTIONS"]
- unless Rage.config.internal.manually_release_ar_connections?
+ unless Rage.config.internal.should_manually_release_ar_connections?
puts "WARNING: The RAGE_DISABLE_AR_WEAK_CONNECTIONS setting does not have any effect with Active Record 7.2+"
end
# no-op
- elsif Rage.config.internal.manually_release_ar_connections?
+ elsif Rage.config.internal.should_manually_release_ar_connections?
class Fiber
def self.defer(fileno)
f = Fiber.current
From 6ec3a425c5cb3d0b165db5485a5be87ee003ce1e Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Thu, 5 Sep 2024 16:41:08 +0100
Subject: [PATCH 09/20] Correctly reap active connections
releasing connections inside each raises "can't add a new key into hash during iteration"
---
lib/rage/ext/active_record/connection_pool.rb | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/lib/rage/ext/active_record/connection_pool.rb b/lib/rage/ext/active_record/connection_pool.rb
index c2cac283..fa0f1c21 100644
--- a/lib/rage/ext/active_record/connection_pool.rb
+++ b/lib/rage/ext/active_record/connection_pool.rb
@@ -147,11 +147,13 @@ def release_connection(owner = Fiber.current)
# Recover lost connections for the pool.
def reap
+ crashed_fibers = nil
+
@__in_use.each do |fiber, conn|
unless fiber.alive?
if conn.active?
conn.reset!
- release_connection(fiber)
+ (crashed_fibers ||= []) << fiber
else
@__in_use.delete(fiber)
conn.disconnect!
@@ -161,6 +163,10 @@ def reap
end
end
end
+
+ if crashed_fibers
+ crashed_fibers.each { |fiber| release_connection(fiber) }
+ end
end
# Disconnect all connections that have been idle for at least
From 714ac365ed9560c0004968887724288b75f91610 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Thu, 5 Sep 2024 16:41:45 +0100
Subject: [PATCH 10/20] Correctly determine idle timeout
---
lib/rage/ext/active_record/connection_pool.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/rage/ext/active_record/connection_pool.rb b/lib/rage/ext/active_record/connection_pool.rb
index fa0f1c21..7dbf6eb4 100644
--- a/lib/rage/ext/active_record/connection_pool.rb
+++ b/lib/rage/ext/active_record/connection_pool.rb
@@ -66,7 +66,7 @@ def __init_rage_extension
@__checkout_timeout = checkout_timeout
# how long a connection can be idle for before disconnecting
- @__idle_timeout = reaper.frequency
+ @__idle_timeout = respond_to?(:db_config) ? db_config.idle_timeout : @idle_timeout
# how often should we check for fibers that wait for a connection for too long
@__timeout_worker_frequency = 0.5
From 644c2e4b4973777bdebd9e86cbbfb009515b131d Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Thu, 5 Sep 2024 16:42:18 +0100
Subject: [PATCH 11/20] Release AR connections when calling `Fiber.await`
---
lib/rage/fiber.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/rage/fiber.rb b/lib/rage/fiber.rb
index 7c7667ed..b5e7c1a5 100644
--- a/lib/rage/fiber.rb
+++ b/lib/rage/fiber.rb
@@ -141,7 +141,7 @@ def self.await(fibers)
end
end
- Fiber.yield
+ Fiber.defer(-1)
Iodine.defer { Iodine.unsubscribe("await:#{f.object_id}") }
# if num_wait_for is not 0 means we exited prematurely because of an error
From 70e8f89fd1502b1b8d606438cedc52c80481fa2a Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Thu, 5 Sep 2024 16:42:42 +0100
Subject: [PATCH 12/20] Release AR connections in nested fibers
---
lib/rage/ext/setup.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/rage/ext/setup.rb b/lib/rage/ext/setup.rb
index 3947731c..ce773e8a 100644
--- a/lib/rage/ext/setup.rb
+++ b/lib/rage/ext/setup.rb
@@ -20,7 +20,7 @@ def self.defer(fileno)
if ActiveRecord::Base.connection_handler.active_connections?
Iodine.defer do
- if fileno != f.__awaited_fileno
+ if fileno != f.__awaited_fileno || !f.alive?
ActiveRecord::Base.connection_handler.connection_pools.each { |pool| pool.release_connection(f) }
end
end
From 7047bb70b80c5f02df983e918179cd3abe0d908e Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Wed, 11 Sep 2024 17:30:14 +0100
Subject: [PATCH 13/20] Patch all available connection pools
---
lib/rage-rb.rb | 6 ++++--
lib/rage/ext/active_record/connection_pool.rb | 12 +++++++-----
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/lib/rage-rb.rb b/lib/rage-rb.rb
index 5f9094c4..59bc7aec 100644
--- a/lib/rage-rb.rb
+++ b/lib/rage-rb.rb
@@ -65,8 +65,10 @@ def self.patch_active_record_connection_pool
if is_connected
puts "INFO: Patching ActiveRecord::ConnectionPool"
Iodine.on_state(:on_start) do
- ActiveRecord::Base.connection_pool.extend(Rage::Ext::ActiveRecord::ConnectionPool)
- ActiveRecord::Base.connection_pool.__init_rage_extension
+ ActiveRecord::Base.connection_handler.connection_pool_list(:all).each do |pool|
+ pool.extend(Rage::Ext::ActiveRecord::ConnectionPool)
+ pool.__init_rage_extension
+ end
end
else
puts "WARNING: DB connection is not established - can't patch ActiveRecord::ConnectionPool"
diff --git a/lib/rage/ext/active_record/connection_pool.rb b/lib/rage/ext/active_record/connection_pool.rb
index 7dbf6eb4..2acda092 100644
--- a/lib/rage/ext/active_record/connection_pool.rb
+++ b/lib/rage/ext/active_record/connection_pool.rb
@@ -104,8 +104,10 @@ def __init_rage_extension
end
end
+ @release_connection_channel = "ext:ar-connection-released:#{object_id}"
+
# resume blocked fibers once connections become available
- Iodine.subscribe("ext:ar-connection-released") do
+ Iodine.subscribe(@release_connection_channel) do
if @__blocked.length > 0 && @__connections.length > 0
f, _ = @__blocked.shift
f.resume
@@ -114,7 +116,7 @@ def __init_rage_extension
# unsubscribe on shutdown
Iodine.on_state(:on_finish) do
- Iodine.unsubscribe("ext:ar-connection-released")
+ Iodine.unsubscribe(@release_connection_channel)
end
end
@@ -139,7 +141,7 @@ def release_connection(owner = Fiber.current)
if (conn = @__in_use.delete(owner))
conn.__idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@__connections << conn
- Iodine.publish("ext:ar-connection-released", "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
+ Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
end
conn
@@ -159,7 +161,7 @@ def reap
conn.disconnect!
__remove__(conn)
@__connections += build_new_connections(1)
- Iodine.publish("ext:ar-connection-released", "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
+ Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
end
end
end
@@ -262,7 +264,7 @@ def disconnect(raise_on_acquisition_timeout = true, disconnect_attempts = 0)
# notify blocked fibers that there are new connections available
[@__blocked.length, @__connections.length].min.times do
- Iodine.publish("ext:ar-connection-released", "", Iodine::PubSub::PROCESS)
+ Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS)
end
end
From f697ff1697929e0e21c8c9ba61935f5fb3e97462 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Wed, 11 Sep 2024 17:33:34 +0100
Subject: [PATCH 14/20] Explicitly access all connection pools
`:all` option makes it explicit and removes deprecation warning on AR 7.1.
AR 6.0 doesn't support this option, so we add the "polyfill"
---
lib/rage/controller/api.rb | 4 +---
lib/rage/ext/setup.rb | 13 +++++++++++--
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/lib/rage/controller/api.rb b/lib/rage/controller/api.rb
index d7ecd89d..31385793 100644
--- a/lib/rage/controller/api.rb
+++ b/lib/rage/controller/api.rb
@@ -128,9 +128,7 @@ def __run_#{action}
#{if should_release_connections
<<~RUBY
- if ActiveRecord::Base.connection_handler.active_connections?
- ActiveRecord::Base.connection_handler.clear_active_connections!
- end
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
RUBY
end}
diff --git a/lib/rage/ext/setup.rb b/lib/rage/ext/setup.rb
index ce773e8a..0ac97ae9 100644
--- a/lib/rage/ext/setup.rb
+++ b/lib/rage/ext/setup.rb
@@ -3,6 +3,15 @@
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
end
+# patch Active Record 6.0 to accept the role argument
+if defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("6.1")
+ %i(active_connections? connection_pool_list clear_active_connections!).each do |m|
+ ActiveRecord::Base.connection_handler.define_singleton_method(m) do |_ = nil|
+ super()
+ end
+ end
+end
+
# release ActiveRecord connections on yield
if defined?(ActiveRecord) && Rage.config.internal.patch_ar_pool?
if ENV["RAGE_DISABLE_AR_WEAK_CONNECTIONS"]
@@ -18,10 +27,10 @@ def self.defer(fileno)
res = Fiber.yield
- if ActiveRecord::Base.connection_handler.active_connections?
+ if ActiveRecord::Base.connection_handler.active_connections?(:all)
Iodine.defer do
if fileno != f.__awaited_fileno || !f.alive?
- ActiveRecord::Base.connection_handler.connection_pools.each { |pool| pool.release_connection(f) }
+ ActiveRecord::Base.connection_handler.connection_pool_list(:all).each { |pool| pool.release_connection(Fiber.current) }
end
end
end
From bc2fb2edc90804890e074ddbdca2aab263afd634 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Wed, 11 Sep 2024 17:36:14 +0100
Subject: [PATCH 15/20] Add a version guard clause
---
lib/rage/ext/setup.rb | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/lib/rage/ext/setup.rb b/lib/rage/ext/setup.rb
index 0ac97ae9..e1259af7 100644
--- a/lib/rage/ext/setup.rb
+++ b/lib/rage/ext/setup.rb
@@ -1,3 +1,7 @@
+if defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("6")
+ fail "Rage is only compatible with Active Record 6+. Detected Active Record version: #{ActiveRecord.version}."
+end
+
# set ActiveSupport isolation level
if defined?(ActiveSupport::IsolatedExecutionState)
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
From 143499f5313530aa6b7d4553aae0c6bb549e0656 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Wed, 11 Sep 2024 17:38:35 +0100
Subject: [PATCH 16/20] Correctly release connections in cable apps
---
lib/rage/cable/channel.rb | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/lib/rage/cable/channel.rb b/lib/rage/cable/channel.rb
index 1eb4dabd..3947364a 100644
--- a/lib/rage/cable/channel.rb
+++ b/lib/rage/cable/channel.rb
@@ -135,7 +135,7 @@ def __register_action_proc(action_name)
end
is_subscribing = action_name == :subscribed
- activerecord_loaded = defined?(::ActiveRecord)
+ should_release_connections = Rage.config.internal.should_manually_release_ar_connections?
method_name = class_eval <<~RUBY, __FILE__, __LINE__ + 1
def __run_#{action_name}(data)
@@ -163,13 +163,10 @@ def __run_#{action_name}(data)
#{periodic_timers_chunk}
#{rescue_handlers_chunk}
- #{if activerecord_loaded
+ #{if should_release_connections
<<~RUBY
ensure
- # TODO
- if ActiveRecord::Base.connection_pool.active_connection?
- ActiveRecord::Base.connection_handler.clear_active_connections!
- end
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
RUBY
end}
end
From cae80a28e651ef371307071fb53009198151c750 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Thu, 12 Sep 2024 19:35:41 +0100
Subject: [PATCH 17/20] Correctly release connections from nested fibers
---
lib/rage/ext/setup.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/rage/ext/setup.rb b/lib/rage/ext/setup.rb
index e1259af7..17a03e0b 100644
--- a/lib/rage/ext/setup.rb
+++ b/lib/rage/ext/setup.rb
@@ -34,7 +34,7 @@ def self.defer(fileno)
if ActiveRecord::Base.connection_handler.active_connections?(:all)
Iodine.defer do
if fileno != f.__awaited_fileno || !f.alive?
- ActiveRecord::Base.connection_handler.connection_pool_list(:all).each { |pool| pool.release_connection(Fiber.current) }
+ ActiveRecord::Base.connection_handler.connection_pool_list(:all).each { |pool| pool.release_connection(f) }
end
end
end
From 7e14fc4d40c8f4c7059030f38a795cf169beaf55 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Thu, 12 Sep 2024 19:37:30 +0100
Subject: [PATCH 18/20] Set `automatic_reconnect` flag
setting this flag allows to correctly rebuild connections in dev env with multiple DBs
---
lib/rage/ext/active_record/connection_pool.rb | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lib/rage/ext/active_record/connection_pool.rb b/lib/rage/ext/active_record/connection_pool.rb
index 2acda092..225cd691 100644
--- a/lib/rage/ext/active_record/connection_pool.rb
+++ b/lib/rage/ext/active_record/connection_pool.rb
@@ -160,6 +160,7 @@ def reap
@__in_use.delete(fiber)
conn.disconnect!
__remove__(conn)
+ self.automatic_reconnect = true
@__connections += build_new_connections(1)
Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
end
@@ -260,6 +261,7 @@ def disconnect(raise_on_acquisition_timeout = true, disconnect_attempts = 0)
end
# create a new pool
+ self.automatic_reconnect = true
@__connections = build_new_connections
# notify blocked fibers that there are new connections available
From a35040d5288b115daf170408a634f116ff6fed0b Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Fri, 13 Sep 2024 15:49:15 +0100
Subject: [PATCH 19/20] Update docs
---
lib/rage/configuration.rb | 8 ++++++--
lib/rage/fiber.rb | 37 +++++++++++++++++++++++++++++++++++++
2 files changed, 43 insertions(+), 2 deletions(-)
diff --git a/lib/rage/configuration.rb b/lib/rage/configuration.rb
index 4f335bed..c6f6b09c 100644
--- a/lib/rage/configuration.rb
+++ b/lib/rage/configuration.rb
@@ -130,9 +130,13 @@
#
# > Disables the `io_write` hook to fix the ["zero-length iov"](https://bugs.ruby-lang.org/issues/19640) error on Ruby < 3.3.
#
-# • _RAGE_PATCH_AR_POOL_
+# • _RAGE_DISABLE_AR_POOL_PATCH_
#
-# > Enables the `ActiveRecord::ConnectionPool` patch to optimize database connection management. Use it to increase throughput under high load.
+# > Disables the `ActiveRecord::ConnectionPool` patch and makes Rage use the original ActiveRecord implementation.
+#
+# • _RAGE_DISABLE_AR_WEAK_CONNECTIONS_
+#
+# > Instructs Rage to not reuse Active Record connections between different fibers.
#
class Rage::Configuration
attr_accessor :logger
diff --git a/lib/rage/fiber.rb b/lib/rage/fiber.rb
index b5e7c1a5..2220bacd 100644
--- a/lib/rage/fiber.rb
+++ b/lib/rage/fiber.rb
@@ -39,6 +39,43 @@
# Many developers see fibers as "lightweight threads" that should be used in conjunction with fiber pools, the same way we use thread pools for threads.
# Instead, it makes sense to think of fibers as regular Ruby objects. We don't use a pool of arrays when we need to create an array - we create a new object and let Ruby and the GC do their job.
# Same applies to fibers. Feel free to create as many fibers as you need on demand.
+#
+# ## Active Record Connections
+#
+# Let's consider the following controller, where we update a record in the database:
+#
+# ```ruby
+# class UsersController < RageController::API
+# def update
+# User.update!(params[:id], email: params[:email])
+# render status: :ok
+# end
+# end
+# ```
+#
+# The `User.update!` call here checks out an Active Record connection, and Rage will automatically check it back in once the action is completed. So far so good!
+#
+# Let's consider another example:
+#
+# ```ruby
+# require "net/http"
+#
+# class UsersController < RageController::API
+# def update
+# User.update!(params[:id], email: params[:email]) # takes 5ms
+# Net::HTTP.post_form(URI("https://mailing.service/update"), { user_id: params[:id] }) # takes 50ms
+# render status: :ok
+# end
+# end
+# ```
+#
+# Here, we've added another step: once the record is updated, we will send a request to update the user's data in the mailing list service.
+#
+# However, in this case, we want to release the Active Record connection before the action is completed. You can see that we need the connection only for the `User.update!` call.
+# The next 50ms the code will spend waiting for the HTTP request to finish, and if we don't release the Active Record connection right away, other fibers won't be able to use it.
+#
+# Active Record 7.2 handles this case by using [#with_connection](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html#method-i-with_connection) internally.
+# With older Active Record versions, Rage handles this case on its own by keeping track of blocking calls and releasing Active Record connections between them.
class Fiber
# @private
AWAIT_ERROR_MESSAGE = "err"
From 24ebe9480f5f45368c431e789b7269cbea37e307 Mon Sep 17 00:00:00 2001
From: Roman Samoilov <2270393+rsamoilov@users.noreply.github.com>
Date: Fri, 13 Sep 2024 15:53:42 +0100
Subject: [PATCH 20/20] Correctly generate YARD docs for Fiber
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
🤨
---
lib/rage/ext/setup.rb | 1 -
1 file changed, 1 deletion(-)
diff --git a/lib/rage/ext/setup.rb b/lib/rage/ext/setup.rb
index 17a03e0b..3aa386b3 100644
--- a/lib/rage/ext/setup.rb
+++ b/lib/rage/ext/setup.rb
@@ -22,7 +22,6 @@
unless Rage.config.internal.should_manually_release_ar_connections?
puts "WARNING: The RAGE_DISABLE_AR_WEAK_CONNECTIONS setting does not have any effect with Active Record 7.2+"
end
- # no-op
elsif Rage.config.internal.should_manually_release_ar_connections?
class Fiber
def self.defer(fileno)