Class: Apartment::Adapters::AbstractAdapter
- Inherits:
-
Object
- Object
- Apartment::Adapters::AbstractAdapter
- Includes:
- ActiveSupport::Callbacks
- Defined in:
- lib/apartment/adapters/abstract_adapter.rb
Overview
rubocop:disable Metrics/ClassLength
Direct Known Subclasses
Mysql2Adapter, PostgresqlDatabaseAdapter, PostgresqlSchemaAdapter, Sqlite3Adapter
Instance Attribute Summary collapse
-
#connection_config ⇒ Object
readonly
The raw database connection configuration hash (from ActiveRecord).
Instance Method Summary collapse
-
#create(tenant) ⇒ Object
Create a new tenant (schema or database).
-
#default_tenant ⇒ Object
Default tenant from config.
-
#drop(tenant) ⇒ Object
Drop a tenant.
-
#environmentify(tenant) ⇒ Object
Environmentify a tenant name based on config.
-
#failsafe_error_classes ⇒ Object
Request-path fail-safe contract.
-
#initialize(connection_config) ⇒ AbstractAdapter
constructor
A new instance of AbstractAdapter.
-
#migrate(tenant, version = nil) ⇒ Object
Run migrations for a tenant.
-
#process_excluded_models ⇒ Object
Deprecated: use process_pinned_models instead.
-
#process_pinned_model(klass) ⇒ Object
Process a single pinned model.
-
#process_pinned_models ⇒ Object
Process all pinned models.
-
#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.
-
#resolve_connection_config(tenant, base_config: nil) ⇒ Object
Resolve a tenant-specific connection config hash.
-
#seed(tenant) ⇒ Object
Run seeds for a tenant.
-
#shared_pinned_connection? ⇒ Boolean
Whether pinned models can share the tenant’s connection pool using qualified table names instead of establish_connection.
-
#tenant_container_gone?(error, tenant) ⇒ Boolean
Whether
error, raised while servingtenant, 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. -
#validated_connection_config(tenant, base_config_override: nil) ⇒ Object
Template method: validates tenant name then delegates to resolve_connection_config.
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_config ⇒ Object (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_tenant ⇒ Object
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.}" end begin deregister_shard_from_ar_handler(pool_key) rescue StandardError => e warn "[Apartment] Shard deregistration failed for '#{pool_key}': #{e.class}: #{e.}" 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_classes ⇒ Object
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_models ⇒ Object
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_models ⇒ Object
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.}") 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.
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.
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.
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
-
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.
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 |