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.
-
#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).
-
#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). 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_tenant ⇒ Object
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.}" 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).
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_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.
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_models ⇒ Object
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_models ⇒ Object
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.}") 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.
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.
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.
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.
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 |