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



43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/apartment/adapters/abstract_adapter.rb', line 43

def create(tenant)
  TenantNameValidator.validate!(
    environmentify(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.



202
203
204
# File 'lib/apartment/adapters/abstract_adapter.rb', line 202

def default_tenant
  Apartment.config.default_tenant
end

#drop(tenant) ⇒ Object

Drop a tenant.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/apartment/adapters/abstract_adapter.rb', line 59

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



187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/apartment/adapters/abstract_adapter.rb', line 187

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.



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

def failsafe_error_classes
  []
end

#migrate(tenant, version = nil) ⇒ Object

Run migrations for a tenant.



78
79
80
81
82
# File 'lib/apartment/adapters/abstract_adapter.rb', line 78

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

#process_excluded_modelsObject

Deprecated: use process_pinned_models instead.



179
180
181
182
183
# File 'lib/apartment/adapters/abstract_adapter.rb', line 179

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



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/apartment/adapters/abstract_adapter.rb', line 159

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.



141
142
143
144
145
146
147
148
149
150
# File 'lib/apartment/adapters/abstract_adapter.rb', line 141

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)


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

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)


38
39
40
# File 'lib/apartment/adapters/abstract_adapter.rb', line 38

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

#seed(tenant) ⇒ Object

Run seeds for a tenant.



85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/apartment/adapters/abstract_adapter.rb', line 85

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)


105
106
107
# File 'lib/apartment/adapters/abstract_adapter.rb', line 105

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

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


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

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.



26
27
28
29
30
31
32
33
34
# File 'lib/apartment/adapters/abstract_adapter.rb', line 26

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