Module: Apartment::Patches::ConnectionHandling

Defined in:
lib/apartment/patches/connection_handling.rb

Overview

Prepended on ActiveRecord::Base (singleton class) to intercept connection_pool lookups. When Apartment::Current.tenant is set, returns a tenant-specific pool keyed by “tenant:role”, with config resolved by the adapter using the current role’s base config.

Instance Method Summary collapse

Instance Method Details

#connection_poolObject

rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/apartment/patches/connection_handling.rb', line 12

def connection_pool # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  tenant = Apartment::Current.tenant
  cfg = Apartment.config

  return super if tenant.nil? || cfg.nil?
  return super if tenant.to_s == cfg.default_tenant.to_s
  return super unless Apartment.pool_manager

  # Skip tenant override for Apartment pinned models.
  # Uses explicit registry (not connection_specification_name heuristic)
  # because ApplicationRecord subclasses have a different spec name than
  # ActiveRecord::Base while sharing the same pool.
  return super if self != ActiveRecord::Base && Apartment.pinned_model?(self)

  role = ActiveRecord::Base.current_role
  pool_key = "#{tenant}:#{role}"

  Apartment.pool_manager.fetch_or_create(pool_key) do
    # Resolve base config from the current role's default pool when available.
    # Falls back to nil (adapter uses its own base_config) when the default pool
    # is not accessible — e.g., in worker threads during parallel migration where
    # the ConnectionHandler may not have the pool registered for this context.
    # NOTE: `super` must be called here (not in a helper) because it refers to
    # the original connection_pool method on AR::Base, which only resolves from
    # the prepended method scope.
    base = begin
      default_pool = super
      default_pool.db_config.configuration_hash.stringify_keys
    rescue ActiveRecord::ConnectionNotEstablished
      nil
    end

    config = Apartment.adapter.validated_connection_config(tenant, base_config_override: base)
    prefix = cfg.shard_key_prefix
    shard_key = :"#{prefix}_#{pool_key}"

    db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(
      cfg.rails_env_name,
      "#{prefix}_#{pool_key}",
      config
    )

    pool = ActiveRecord::Base.connection_handler.establish_connection(
      db_config,
      owner_name: ActiveRecord::Base,
      role: role,
      shard: shard_key
    )

    raise(Apartment::PendingMigrationError, tenant) if check_pending_migrations?(pool)

    load_tenant_schema_cache(tenant, pool) if cfg.schema_cache_per_tenant

    pool
  end
rescue Apartment::ApartmentError
  raise
rescue StandardError => e
  raise(Apartment::ApartmentError,
        "Failed to resolve connection pool for tenant '#{tenant}': #{e.class}: #{e.message}")
end