Class: Apartment::Adapters::AbstractAdapter

Inherits:
Object
  • Object
show all
Includes:
ActiveSupport::Callbacks
Defined in:
lib/apartment/adapters/abstract_adapter.rb

Overview

rubocop:disable Metrics/ClassLength

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection_config) ⇒ AbstractAdapter

Returns a new instance of AbstractAdapter.



18
19
20
# File 'lib/apartment/adapters/abstract_adapter.rb', line 18

def initialize(connection_config)
  @connection_config = connection_config
end

Instance Attribute Details

#connection_configObject (readonly)

The raw database connection configuration hash (from ActiveRecord). Not to be confused with Apartment.config (the Apartment::Config object).



16
17
18
# File 'lib/apartment/adapters/abstract_adapter.rb', line 16

def connection_config
  @connection_config
end

Instance Method Details

#aborted_transaction?(_conn) ⇒ Boolean

Whether conn sits in an aborted-transaction state that every subsequent statement will fail against until the transaction ends. PostgreSQL is the only supported engine with such a state (PQTRANS_INERROR); MySQL fails the statement and leaves the transaction usable, and its raw connection has no transaction_status at all. Base is conservative: never reclassify. See docs/designs/transaction-taint-detection.md (Evidence E).

Returns:

  • (Boolean)


125
126
127
# File 'lib/apartment/adapters/abstract_adapter.rb', line 125

def aborted_transaction?(_conn)
  false
end

#create(tenant) ⇒ Object

Create a new tenant (schema or database). Validates the physical identifier create_tenant actually addresses (raw schema name for :schema, environmentified database name otherwise), so create and the pool-resolution path validate the same name.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/apartment/adapters/abstract_adapter.rb', line 54

def create(tenant)
  validate_pool_key_safety!(tenant)
  TenantNameValidator.validate!(
    physical_tenant_name(tenant),
    strategy: Apartment.config.tenant_strategy,
    adapter_name: base_config['adapter']
  )
  run_callbacks(:create) do
    create_tenant(tenant)
    grant_tenant_privileges(tenant)
    import_schema(tenant) if Apartment.config.schema_load_strategy
    seed(tenant) if Apartment.config.seed_after_create
    Instrumentation.instrument(:create, tenant: tenant)
  end
end

#default_tenantObject

Default tenant from config.



231
232
233
# File 'lib/apartment/adapters/abstract_adapter.rb', line 231

def default_tenant
  Apartment.config.default_tenant
end

#drop(tenant) ⇒ Object

Drop a tenant.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/apartment/adapters/abstract_adapter.rb', line 71

def drop(tenant)
  drop_tenant(tenant)
  removed_pools = Apartment.pool_manager&.remove_tenant(tenant) || []
  removed_pools.each do |pool_key, pool|
    # remove_tenant already took these out of the manager, so deregister_shard's
    # own removal returns nil and cannot disconnect them — we close them here.
    Apartment.disconnect_removed_pool(pool, pool_key)
    begin
      deregister_shard_from_ar_handler(pool_key)
    rescue StandardError => e
      warn "[Apartment] Shard deregistration failed for '#{pool_key}': #{e.class}: #{e.message}"
    end
  end
  Instrumentation.instrument(:drop, tenant: tenant)
end

#environmentify(tenant) ⇒ Object

Environmentify a tenant name based on config. :prepend/:append require Rails to be defined (for Rails.env).



207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/apartment/adapters/abstract_adapter.rb', line 207

def environmentify(tenant)
  case Apartment.config.environmentify_strategy
  when :prepend
    "#{rails_env}_#{tenant}"
  when :append
    "#{tenant}_#{rails_env}"
  when nil
    tenant.to_s
  else
    # Callable
    Apartment.config.environmentify_strategy.call(tenant)
  end
end

#failsafe_error_classesObject

Request-path fail-safe contract. The elevator wraps the tenant switch; on one of these error classes it asks #tenant_container_gone? whether the tenant's storage actually vanished (a cross-process drop) rather than an app-level failure. An empty list disables the rescue, so an adapter that does not implement the seams never converts an error into a 404.



135
136
137
# File 'lib/apartment/adapters/abstract_adapter.rb', line 135

def failsafe_error_classes
  []
end

#migrate(tenant, version = nil) ⇒ Object

Run migrations for a tenant.



88
89
90
91
92
# File 'lib/apartment/adapters/abstract_adapter.rb', line 88

def migrate(tenant, version = nil)
  Apartment::Tenant.switch(tenant) do
    ActiveRecord::Base.connection_pool.migration_context.migrate(version)
  end
end

#physical_tenant_name(tenant) ⇒ Object

The physical identifier used to address this tenant at connection time: the database name for database-per-tenant strategies (environmentified). validated_connection_config validates THIS name so the pool-resolution path agrees with what the connection actually targets. Schema-per-tenant overrides this to the raw tenant (schemas are named directly).



226
227
228
# File 'lib/apartment/adapters/abstract_adapter.rb', line 226

def physical_tenant_name(tenant)
  environmentify(tenant)
end

#process_excluded_modelsObject

Deprecated: use process_pinned_models instead.



199
200
201
202
203
# File 'lib/apartment/adapters/abstract_adapter.rb', line 199

def process_excluded_models
  warn '[Apartment] DEPRECATION: process_excluded_models is deprecated. ' \
       'Use Apartment::Model with pin_tenant instead.'
  process_pinned_models
end

#process_pinned_model(klass) ⇒ Object

Process a single pinned model. Called by process_pinned_models (batch) and by Apartment::Model.pin_tenant (when activated? is true).

When shared_pinned_connection? is true, qualifies the table name so the model uses the tenant's pool (preserving transactional integrity). Otherwise, establishes a separate connection pool (required when cross-database queries are impossible).



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/apartment/adapters/abstract_adapter.rb', line 179

def process_pinned_model(klass)
  # Ensure the concern is included — models registered via the
  # excluded_models shim may not have it yet. Uses apartment_mark_pinned!
  # (not pin_tenant) to avoid recursion back into process_pinned_model.
  unless klass.respond_to?(:apartment_pinned_processed?)
    klass.include(Apartment::Model)
    klass.apartment_mark_pinned!
  end

  return if klass.apartment_pinned_processed?

  if shared_pinned_connection?
    qualify_pinned_table_name(klass)
  else
    klass.establish_connection(pinned_model_config)
    klass.apartment_mark_processed!
  end
end

#process_pinned_modelsObject

Process all pinned models. When shared_pinned_connection? is true, qualifies table names for shared pool routing. Otherwise, establishes separate connections.



161
162
163
164
165
166
167
168
169
170
# File 'lib/apartment/adapters/abstract_adapter.rb', line 161

def process_pinned_models
  return if Apartment.pinned_models.empty?

  Apartment.pinned_models.each do |klass|
    process_pinned_model(klass)
  rescue StandardError => e
    raise(Apartment::ConfigurationError,
          "Failed to process pinned model #{klass.name}: #{e.class}: #{e.message}")
  end
end

#qualify_pinned_table_name(_klass) ⇒ Object

Qualify a pinned model's table_name so it targets the default tenant's tables from any tenant connection. Subclasses must implement when shared_pinned_connection? returns true.

Raises:

  • (NotImplementedError)


154
155
156
157
# File 'lib/apartment/adapters/abstract_adapter.rb', line 154

def qualify_pinned_table_name(_klass)
  raise(NotImplementedError,
        "#{self.class}#qualify_pinned_table_name must be implemented when shared_pinned_connection? is true")
end

#resolve_connection_config(tenant, base_config: nil) ⇒ Object

Resolve a tenant-specific connection config hash. Subclasses override to set strategy-specific keys.

Raises:

  • (NotImplementedError)


46
47
48
# File 'lib/apartment/adapters/abstract_adapter.rb', line 46

def resolve_connection_config(tenant, base_config: nil)
  raise(NotImplementedError)
end

#seed(tenant) ⇒ Object

Run seeds for a tenant.



95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/apartment/adapters/abstract_adapter.rb', line 95

def seed(tenant)
  Apartment::Tenant.switch(tenant) do
    seed_file = Apartment.config.seed_data_file
    return unless seed_file

    unless File.exist?(seed_file)
      raise(Apartment::ConfigurationError,
            "Seed file '#{seed_file}' does not exist")
    end

    load(seed_file)
  end
end

#shared_pinned_connection?Boolean

Whether pinned models can share the tenant's connection pool using qualified table names instead of establish_connection.

Returns false by default (separate pool). Subclasses override to return true when the engine supports cross-schema/database queries, gated by config.force_separate_pinned_pool.

Returns:

  • (Boolean)


115
116
117
# File 'lib/apartment/adapters/abstract_adapter.rb', line 115

def shared_pinned_connection?
  false
end

#tenant_container_gone?(error, tenant) ⇒ Boolean

Whether error, raised while serving tenant, means the tenant's container (schema/database/file) no longer exists — so the validator should evict the name and the request should 404 instead of surfacing a 500. Composed from a cheap error-shape check and an authoritative existence probe, both conservative by default so the base adapter never reclassifies. Subclasses override the seams.

Returns:

  • (Boolean)


145
146
147
148
149
# File 'lib/apartment/adapters/abstract_adapter.rb', line 145

def tenant_container_gone?(error, tenant)
  return false unless container_error?(unwrap_db_error(error))

  !tenant_container_exists?(tenant)
end

#validated_connection_config(tenant, base_config_override: nil) ⇒ Object

Template method: validates tenant name then delegates to resolve_connection_config. Called by ConnectionHandling — subclasses should NOT override this. base_config_override: when supplied (e.g. a role-specific config from ConnectionHandling), the adapter builds the tenant config on top of it instead of its own base_config.

This validates only the PHYSICAL identifier (engine rules). Raw pool-key safety (colon/whitespace/NUL that would corrupt "tenant:role") is enforced by the sole production caller, ConnectionHandling#connection_pool, before it builds the pool key — and independently by #create. A future caller that invokes this directly, bypassing connection_pool, must validate the raw tenant itself (TenantNameValidator.validate_common!).



33
34
35
36
37
38
39
40
41
42
# File 'lib/apartment/adapters/abstract_adapter.rb', line 33

def validated_connection_config(tenant, base_config_override: nil)
  effective_base = base_config_override || base_config
  TenantNameValidator.validate!(
    physical_tenant_name(tenant),
    strategy: Apartment.config.tenant_strategy,
    adapter_name: effective_base['adapter']
  )
  config = resolve_connection_config(tenant, base_config: effective_base)
  apply_tenant_pool_size(config)
end