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

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

Returns:

  • (Boolean)


63
64
65
# File 'lib/hyperion/async_pg/active_record_adapter.rb', line 63

def installed?
  @installed
end

.isolation_levelObject

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