Module: Hyperion::AsyncPg::ActiveRecordAdapter
- Defined in:
- lib/hyperion/async_pg/active_record_adapter.rb
Overview
Makes ActiveRecord’s connection pool fiber-aware.
Background: AR 7.2 / 8.1’s ‘ActiveRecord::ConnectionAdapters::ConnectionPool` uses `ActiveSupport::IsolatedExecutionState.context` as the connection- affinity key in `connection_lease` (the lookup behind `with_connection`, `lease_connection`, etc). At Rails boot the isolation level defaults to `:thread`, so `context` returns `Thread.current`. Two fibers running on the same OS thread (e.g. under Hyperion `–async-io` or Falcon) call `with_connection` and BOTH get handed the same `ActiveRecord::ConnectionAdapters::AbstractAdapter` — which wraps a single `PG::Connection`. When the second fiber’s ‘exec_params` arrives, the connection is still busy with the first fiber’s query and ‘pg` raises `PG::UnableToSend: another command is already in progress`.
The fix: flip ‘ActiveSupport::IsolatedExecutionState.isolation_level` to `:fiber`. Then `context` returns `Fiber.current`, the per-fiber `connection_lease` is keyed on the running fiber, and each fiber on the same OS thread gets its own AR connection (subject to pool size). This is the same switch Rails sets internally when you opt into `config.active_support.isolation_level = :fiber`; we expose it as a one-liner so non-Rails callers (or pre-Rails-boot init scripts) can flip it without booting the full railtie.
Outside a fiber scheduler (Sidekiq workers, plain scripts, rake tasks, Rails console), every callable still runs on a unique fiber per thread, so switching to ‘:fiber` has no negative effect — `Fiber.current` is the OS thread’s root fiber, behaviorally identical to keying on the thread itself. The only observable change is in the multi-fiber case under a scheduler, which is exactly what we want.
Usage:
require 'hyperion/async_pg/active_record_adapter'
Hyperion::AsyncPg::ActiveRecordAdapter.install!
Or, with the main shim:
Hyperion::AsyncPg.install!(activerecord: true)
Sizing reminder: under a fiber scheduler each in-flight fiber holds an AR connection. Set ‘pool:` in `database.yml` to peak fiber concurrency, NOT thread count.
Defined Under Namespace
Classes: NotLoadedError
Class Method Summary collapse
- .install! ⇒ Object
- .installed? ⇒ Boolean
-
.isolation_level ⇒ Object
Returns the current AR isolation level, or nil if AR/AS isn’t loaded.
-
.reset_for_tests! ⇒ Object
Test-only — does NOT restore the previous isolation level.
-
.restore_thread_isolation! ⇒ Object
Test-only — flip the isolation level back to :thread.
Class Method Details
.install! ⇒ Object
52 53 54 55 56 57 58 59 60 61 |
# File 'lib/hyperion/async_pg/active_record_adapter.rb', line 52 def install! @install_mutex.synchronize do return false if @installed require_active_record! switch_to_fiber_isolation! @installed = true end true end |
.installed? ⇒ Boolean
63 64 65 |
# File 'lib/hyperion/async_pg/active_record_adapter.rb', line 63 def installed? @installed end |
.isolation_level ⇒ Object
Returns the current AR isolation level, or nil if AR/AS isn’t loaded.
83 84 85 86 87 |
# File 'lib/hyperion/async_pg/active_record_adapter.rb', line 83 def isolation_level return nil unless defined?(::ActiveSupport::IsolatedExecutionState) ::ActiveSupport::IsolatedExecutionState.isolation_level end |
.reset_for_tests! ⇒ Object
Test-only — does NOT restore the previous isolation level. Use ‘restore_thread_isolation!` for that.
69 70 71 |
# File 'lib/hyperion/async_pg/active_record_adapter.rb', line 69 def reset_for_tests! @install_mutex.synchronize { @installed = false } end |
.restore_thread_isolation! ⇒ Object
Test-only — flip the isolation level back to :thread. Equivalent to AR’s pre-install! state.
75 76 77 78 79 80 |
# File 'lib/hyperion/async_pg/active_record_adapter.rb', line 75 def restore_thread_isolation! return false unless defined?(::ActiveSupport::IsolatedExecutionState) ::ActiveSupport::IsolatedExecutionState.isolation_level = :thread true end |