Module: Apartment
- Defined in:
- lib/apartment.rb,
lib/apartment/cli.rb,
lib/apartment/config.rb,
lib/apartment/errors.rb,
lib/apartment/tenant.rb,
lib/apartment/current.rb,
lib/apartment/railtie.rb,
lib/apartment/version.rb,
lib/apartment/cli/pool.rb,
lib/apartment/migrator.rb,
lib/apartment/cli/seeds.rb,
lib/apartment/lifecycle.rb,
lib/apartment/cli/tenants.rb,
lib/apartment/pool_reaper.rb,
lib/apartment/pool_manager.rb,
lib/apartment/schema_cache.rb,
lib/apartment/pool_observer.rb,
lib/apartment/test_fixtures.rb,
lib/apartment/cli/migrations.rb,
lib/apartment/concerns/model.rb,
lib/apartment/elevators/host.rb,
lib/apartment/instrumentation.rb,
lib/apartment/elevators/domain.rb,
lib/apartment/elevators/header.rb,
lib/apartment/tenant_validator.rb,
lib/apartment/elevators/generic.rb,
lib/apartment/transaction_taint.rb,
lib/apartment/elevators/host_hash.rb,
lib/apartment/elevators/subdomain.rb,
lib/apartment/schema_dumper_patch.rb,
lib/apartment/configs/mysql_config.rb,
lib/apartment/tenant_name_validator.rb,
lib/apartment/adapters/mysql2_adapter.rb,
lib/apartment/adapters/sqlite3_adapter.rb,
lib/apartment/adapters/trilogy_adapter.rb,
lib/apartment/adapters/abstract_adapter.rb,
lib/apartment/configs/postgresql_config.rb,
lib/apartment/elevators/first_subdomain.rb,
lib/apartment/patches/connection_handling.rb,
lib/apartment/patches/live_tenant_propagation.rb,
lib/apartment/patches/postgresql_sequence_name.rb,
lib/apartment/adapters/postgresql_schema_adapter.rb,
lib/apartment/adapters/postgresql_database_adapter.rb,
lib/generators/apartment/install/install_generator.rb,
lib/apartment/adapters/postgresql_transaction_state.rb
Overview
rubocop:disable Metrics/ModuleLength
Defined Under Namespace
Modules: Adapters, Configs, Elevators, Instrumentation, Lifecycle, Model, Patches, SchemaCache, SchemaDumperPatch, Tenant, TenantNameValidator, TestFixtures, TransactionTaint Classes: AdapterNotFound, ApartmentError, CLI, Config, ConfigurationError, Current, DefaultTenantNotConfigured, DefaultTenantRequired, FixtureLifecycleViolation, InstallGenerator, Migrator, PendingMigrationError, PoolCapacityReached, PoolExhausted, PoolManager, PoolObserver, PoolReaper, Railtie, SchemaLoadError, TenantExists, TenantNotFound, TenantRequired, TenantValidator
Constant Summary collapse
- ALWAYS_VALID_TENANT =
An always-valid validator, used when config.tenant_validator is false.
->(_name) { true }
- BUILT_IN_VALIDATOR_MUTEX =
Guards lazy construction of the built-in validator. A constant (not an ivar) so it survives clear_config, which nils @built_in_tenant_validator.
Mutex.new
- VERSION =
'4.0.0.alpha9'
Class Attribute Summary collapse
-
.adapter ⇒ Object
Lazy-loading adapter.
-
.config ⇒ Object
readonly
Returns the value of attribute config.
-
.pool_manager ⇒ Object
readonly
Returns the value of attribute pool_manager.
-
.pool_reaper ⇒ Object
readonly
Returns the value of attribute pool_reaper.
Class Method Summary collapse
-
.activate! ⇒ Object
Activate the ConnectionHandling patch on ActiveRecord::Base.
-
.activate_sql_query_tags! ⇒ Object
Register a :tenant tag with ActiveRecord::QueryLogs so SQL queries include a /* tenant='name' */ comment.
- .activated? ⇒ Boolean
-
.clear_config ⇒ Object
Reset all configuration and stop background tasks.
-
.configure {|new_config| ... } ⇒ Object
Configure Apartment v4.
-
.deregister_shard(pool_key) ⇒ Object
Discard one tenant pool, whole: deregister its shard from AR's ConnectionHandler and forget it in PoolManager, disconnecting it either way.
-
.disconnect_removed_pool(pool, pool_key) ⇒ Object
private
Disconnect a pool that has been removed from PoolManager.
-
.excluded_models ⇒ Object
v3 compatibility: Apartment.excluded_models returns the excluded models list.
-
.pinned_model?(klass) ⇒ Boolean
Check if a class (or any of its ancestors) is a pinned model.
-
.pinned_models ⇒ Object
Registry of models that declared pin_tenant.
- .process_pinned_model(klass) ⇒ Object
- .register_pinned_model(klass) ⇒ Object
-
.reset_tenant_pools! ⇒ void
Deregister all tenant pools from AR's ConnectionHandler and clear the pool manager cache.
-
.tenant_names ⇒ Object
Returns the current tenant list.
-
.tenant_validator ⇒ Object
Resolves config.tenant_validator to a callable: false -> always valid, nil -> the process's built-in TenantValidator (memoized), a callable -> itself.
Class Attribute Details
.adapter ⇒ Object
Lazy-loading adapter. Built on first access via build_adapter. Can be set manually (e.g., in tests) via Apartment.adapter=.
61 62 63 |
# File 'lib/apartment.rb', line 61 def adapter @adapter ||= build_adapter end |
.config ⇒ Object (readonly)
Returns the value of attribute config.
56 57 58 |
# File 'lib/apartment.rb', line 56 def config @config end |
.pool_manager ⇒ Object (readonly)
Returns the value of attribute pool_manager.
56 57 58 |
# File 'lib/apartment.rb', line 56 def pool_manager @pool_manager end |
.pool_reaper ⇒ Object (readonly)
Returns the value of attribute pool_reaper.
56 57 58 |
# File 'lib/apartment.rb', line 56 def pool_reaper @pool_reaper end |
Class Method Details
.activate! ⇒ Object
Activate the ConnectionHandling patch on ActiveRecord::Base. Idempotent — prepend on an already-prepended module is a no-op.
196 197 198 199 200 |
# File 'lib/apartment.rb', line 196 def activate! require_relative('apartment/patches/connection_handling') ActiveRecord::Base.singleton_class.prepend(Patches::ConnectionHandling) @activated = true end |
.activate_sql_query_tags! ⇒ Object
Register a :tenant tag with ActiveRecord::QueryLogs so SQL queries include a /* tenant='name' */ comment. No-op when sql_query_tags is false or ActiveRecord::QueryLogs is not available.
205 206 207 208 209 210 211 212 213 214 |
# File 'lib/apartment.rb', line 205 def return unless @config&. return unless defined?(ActiveRecord::QueryLogs) return if ActiveRecord::QueryLogs..include?(:tenant) ActiveRecord::QueryLogs.taggings = ActiveRecord::QueryLogs.taggings.merge( tenant: -> { Apartment::Current.tenant } ) ActiveRecord::QueryLogs. = ActiveRecord::QueryLogs. + [:tenant] end |
.activated? ⇒ Boolean
105 106 107 |
# File 'lib/apartment.rb', line 105 def activated? @activated == true end |
.clear_config ⇒ Object
Reset all configuration and stop background tasks.
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/apartment.rb', line 176 def clear_config teardown_old_state # Restore (un-qualify) pinned models, but keep them registered. pin_tenant # runs once when a model's class body loads and never re-runs, so the # registry is the only record of which models are pinned. Discarding it # would strand every pinned model unprocessed after the next configure. # The registry is bounded in production (pinned models are named # constants); a test process that pins anonymous classes accumulates them # here — acceptable, but count-sensitive specs must isolate it themselves. @pinned_models&.each { |klass| klass.apartment_restore! if klass.respond_to?(:apartment_restore!) } @built_in_tenant_validator&.shutdown @built_in_tenant_validator = nil @config = nil @pool_manager = nil @pool_reaper = nil @activated = false end |
.configure {|new_config| ... } ⇒ Object
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/apartment.rb', line 157 def configure raise(ConfigurationError, 'Apartment.configure requires a block') unless block_given? new_config = Config.new yield(new_config) new_config.apply_defaults! new_config.validate! new_config.freeze! # Validation passed — tear down old state and swap in new. teardown_old_state @built_in_tenant_validator&.shutdown @built_in_tenant_validator = nil @config = new_config setup_pools!(new_config) @config end |
.deregister_shard(pool_key) ⇒ Object
Discard one tenant pool, whole: deregister its shard from AR's ConnectionHandler and forget it in PoolManager, disconnecting it either way. Safe to call when AR is not loaded or config is not set (no-op). Used by PoolReaper eviction, AbstractAdapter#drop, and teardown.
Both registries are updated because either one alone is wrong: deregistering from AR while the manager still holds the pool wedges the tenant permanently (the manager keeps handing back a pool AR has forgotten), and forgetting it in the manager alone leaks the AR registration and a live backend when the tenant is never re-accessed. Every internal caller already removes from the manager first, so the removal here is a no-op for them.
The pool is disconnected here rather than left to AR, which disconnects only a
pool it actually finds registered (ConnectionHandler#disconnect_pool_from_pool_manager
guards pool_config.disconnect! behind if pool_config). A manager-held pool
with no matching AR registration is reachable — the integration suite swaps the
ConnectionHandler per example — and since we have just removed it from the
manager, no later PoolManager#clear will disconnect it either. Mirrors
AbstractAdapter#drop, which already removes, disconnects, then deregisters.
(Callers that remove the pool from the manager THEMSELVES must disconnect it
themselves too — the removal here returns nil for them, so there is nothing left
for us to disconnect. PoolReaper#evict_tenant and Migrator#evict_migration_pools
do exactly that.)
See docs/designs/out-of-band-tenant-ddl.md.
NOT SAFE inside a PoolManager create block — use deregister_ar_shard there.
PoolManager's @pools is a Concurrent::Map whose MRI backend guards
compute_if_absent and delete with the SAME non-reentrant mutex, so removing
a pool from inside the create block raises ThreadError ("deadlock; recursive
locking"). The manager removal below is exactly that call.
ORDER: AR first, manager removal in an ensure. The manager removal is an
in-memory delete that cannot meaningfully fail, so it is the step that may run
unconditionally; AR's removal does IO and is the one that can raise. Running the
fallible step first, with the infallible one guaranteed after, means the call
always ends with BOTH registries clear.
Manager-first would be a race: between the manager delete and AR's removal, a concurrent tenant switch misses the manager, calls establish_connection — which RETURNS THE STILL-REGISTERED OLD POOL (ConnectionHandler#establish_connection reuses a pool whose db_config is equal) — and stores that doomed pool back in the manager. AR then unregisters and disconnects it, leaving the manager holding a dead pool: the permanent wedge this method exists to prevent, reintroduced. In this order the same interleaving costs at most one failed request, and the ensure clears the manager so the next switch rebuilds cleanly.
Deliberately un-rescued at this level: both steps rescue their own failures, and swallowing everything here is what once hid a ThreadError, silently orphaning the pool the caller asked us to discard. Misuse should be loud.
265 266 267 268 269 270 271 272 273 |
# File 'lib/apartment.rb', line 265 def deregister_shard(pool_key) return unless @config && defined?(ActiveRecord::Base) begin deregister_ar_shard(pool_key) ensure disconnect_removed_pool(@pool_manager&.remove(pool_key), pool_key) end end |
.disconnect_removed_pool(pool, pool_key) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Disconnect a pool that has been removed from PoolManager. Idempotent with AR's own disconnect on the happy path (ConnectionPool#disconnect! empties @connections, so a second call has nothing left to close); load-bearing only when AR has no matching registration to disconnect, in which case nothing else will close it.
Public because the callers that remove a pool from the manager THEMSELVES — and therefore get nil back from deregister_shard's own removal — must disconnect what they removed: PoolReaper#evict_tenant, Migrator#evict_migration_pools, AbstractAdapter#drop. Rescued so one broken pool cannot abort the caller.
285 286 287 288 289 290 291 |
# File 'lib/apartment.rb', line 285 def disconnect_removed_pool(pool, pool_key) return unless pool.respond_to?(:disconnect!) pool.disconnect! rescue StandardError => e warn "[Apartment] Failed to disconnect pool for #{pool_key}: #{e.class}: #{e.}" end |
.excluded_models ⇒ Object
v3 compatibility: Apartment.excluded_models returns the excluded models list. Deprecated in v4 (use Apartment::Model + pin_tenant instead).
134 135 136 137 138 |
# File 'lib/apartment.rb', line 134 def excluded_models raise(ConfigurationError, 'Apartment not configured. Call Apartment.configure first.') unless @config @config.excluded_models end |
.pinned_model?(klass) ⇒ Boolean
Check if a class (or any of its ancestors) is a pinned model. Delegates to the class's own apartment_pinned? (defined by the Apartment::Model concern). Falls back to registry lookup for models registered via the excluded_models shim without the concern.
97 98 99 100 101 102 103 |
# File 'lib/apartment.rb', line 97 def pinned_model?(klass) if klass.respond_to?(:apartment_pinned?) klass.apartment_pinned? else klass.ancestors.any? { |a| a.is_a?(Class) && pinned_models.include?(a) } end end |
.pinned_models ⇒ Object
Registry of models that declared pin_tenant. Uses Concurrent::Set for thread safety (Zeitwerk autoload in threaded servers).
85 86 87 |
# File 'lib/apartment.rb', line 85 def pinned_models @pinned_models ||= Concurrent::Set.new end |
.process_pinned_model(klass) ⇒ Object
140 141 142 143 144 145 146 147 |
# File 'lib/apartment.rb', line 140 def process_pinned_model(klass) unless adapter warn "[Apartment] Cannot process pinned model #{klass.name || klass.inspect}: " \ 'adapter not initialized. Model registered but unprocessed.' return end adapter.process_pinned_model(klass) end |
.register_pinned_model(klass) ⇒ Object
89 90 91 |
# File 'lib/apartment.rb', line 89 def register_pinned_model(klass) pinned_models.add(klass) end |
.reset_tenant_pools! ⇒ void
This method returns an undefined value.
Deregister all tenant pools from AR's ConnectionHandler and clear the
pool manager cache. Pools rebuild lazily on the next connection_pool
call.
Execution context (+Apartment::Current+: tenant, tenant_override, etc.)
is left untouched — pool lifecycle and tenant context are separate
concerns. A caller that also wants to drop tenant context resets it
explicitly via Apartment::Tenant.reset.
Called automatically by Apartment::TestFixtures before Rails' fixture
setup iterates shards. Can also be called manually in custom test
harnesses that cycle tenant pools between examples.
308 309 310 311 312 |
# File 'lib/apartment.rb', line 308 def reset_tenant_pools! guard_pinned_pools_during_fixtures! deregister_all_tenant_pools @pool_manager&.clear end |
.tenant_names ⇒ Object
Returns the current tenant list. Single resolver used by Tenant.each, Migrator, SchemaCache, and the CLI commands. Honors the per-block override set by Tenant.with_tenants_provider / with_tenants when present; otherwise resolves through @config.tenants_provider.
The override (or the configured provider) may itself be a callable, in which case it is invoked on every access. Whatever the source, the resolved value must respond to :each.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/apartment.rb', line 117 def tenant_names raise(ConfigurationError, 'Apartment not configured. Call Apartment.configure first.') unless @config override = Current.tenant_override source = override || @config.tenants_provider result = source.respond_to?(:call) ? source.call : source unless result.respond_to?(:each) source_label = override ? 'tenant_override' : 'tenants_provider' raise(ConfigurationError, "#{source_label} must return an Enumerable, got #{result.class}") end result end |
.tenant_validator ⇒ Object
Resolves config.tenant_validator to a callable: false -> always valid, nil -> the process's built-in TenantValidator (memoized), a callable -> itself.
75 76 77 78 79 80 81 |
# File 'lib/apartment.rb', line 75 def tenant_validator case (configured = @config&.tenant_validator) when false then ALWAYS_VALID_TENANT when nil then built_in_tenant_validator else configured end end |