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

#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.



223
224
225
# File 'lib/apartment/adapters/abstract_adapter.rb', line 223

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
86
87
# File 'lib/apartment/adapters/abstract_adapter.rb', line 71

def drop(tenant) # rubocop:disable Metrics/CyclomaticComplexity
  drop_tenant(tenant)
  removed_pools = Apartment.pool_manager&.remove_tenant(tenant) || []
  removed_pools.each do |pool_key, pool|
    begin
      pool&.disconnect! if pool.respond_to?(:disconnect!)
    rescue StandardError => e
      warn "[Apartment] Pool disconnect failed for '#{pool_key}': #{e.class}: #{e.message}"
    end
    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).



199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/apartment/adapters/abstract_adapter.rb', line 199

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.



127
128
129
# File 'lib/apartment/adapters/abstract_adapter.rb', line 127

def failsafe_error_classes
  []
end

#migrate(tenant, version = nil) ⇒ Object

Run migrations for a tenant.



90
91
92
93
94
# File 'lib/apartment/adapters/abstract_adapter.rb', line 90

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).



218
219
220
# File 'lib/apartment/adapters/abstract_adapter.rb', line 218

def physical_tenant_name(tenant)
  environmentify(tenant)
end

#process_excluded_modelsObject

Deprecated: use process_pinned_models instead.



191
192
193
194
195
# File 'lib/apartment/adapters/abstract_adapter.rb', line 191

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).



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/apartment/adapters/abstract_adapter.rb', line 171

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.



153
154
155
156
157
158
159
160
161
162
# File 'lib/apartment/adapters/abstract_adapter.rb', line 153

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)


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

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.



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

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)


117
118
119
# File 'lib/apartment/adapters/abstract_adapter.rb', line 117

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)


137
138
139
140
141
# File 'lib/apartment/adapters/abstract_adapter.rb', line 137

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