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/privileges.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/migration_role.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/privileges/context.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/connection_registry.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, MigrationRole, Model, Patches, Privileges, 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
- BUILDING_ADAPTER =
Fiber-local latch guarding the lazy build below. Thread#[] is fiber-local, so a fiber-based server gets its own, and a nested build cannot see a sibling's.
:apartment_building_adapter- 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.alpha13'
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 ActiveRecord patches pool-per-tenant depends on.
-
.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
-
.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.
-
.pool_in_use?(pool) ⇒ Boolean
private
True when at least one of +pool+'s connections is leased or holds an open transaction (a long migration, a batch job, an unpinned fixture transaction).
-
.pool_key(tenant, role) ⇒ Object
private
Discard one tenant pool, whole: deregister its shard from AR's ConnectionHandler and forget it in PoolManager, disconnecting it either way.
- .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=.
The latch is a backstop, not the fix. @adapter is not assigned until the build returns, so anything inside the build that resolves a connection through the tenant-aware pool lookup asks for the adapter again and recurses to SystemStackError — a stack dump naming no cause a reader can act on. default_connection_db_config closes the one such circle the gem had; this turns any future one into a sentence.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/apartment.rb', line 72 def adapter return @adapter if @adapter if Thread.current[BUILDING_ADAPTER] raise(ConfigurationError, 'Apartment.adapter was re-entered while the adapter was still being ' \ 'built, which means something in the build resolved a connection ' \ 'through the tenant-aware pool lookup. Build the adapter before ' \ 'switching tenants — Apartment.adapter with no tenant current, or an ' \ 'explicit Apartment.adapter= — and report this, because the gem should ' \ 'not need you to.') end Thread.current[BUILDING_ADAPTER] = true begin @adapter ||= build_adapter ensure Thread.current[BUILDING_ADAPTER] = nil end 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 ActiveRecord patches pool-per-tenant depends on. Idempotent — prepend on an already-prepended module is a no-op.
ConnectionHandling routes AR::Base.connection_pool to the current tenant's pool; ConnectionRegistry makes AR's own pool registry safe for the concurrent shard registration that routing produces. The second is not optional given the first: without it, a cold tenant switch can fail whenever any thread happens to be iterating AR's pools — which Rails does through ConnectionHandler#each_connection_pool at the start of every request or job (ActiveRecord::QueryCache.run), at the end of every one (ConnectionPool::ExecutorHooks.complete), and after writes (clear_query_caches_for_current_thread). NOT from AR's ConnectionPool::Reaper, which reads a private WeakRef list rather than the registry.
235 236 237 238 239 240 241 |
# File 'lib/apartment.rb', line 235 def activate! require_relative('apartment/patches/connection_handling') require_relative('apartment/patches/connection_registry') ActiveRecord::Base.singleton_class.prepend(Patches::ConnectionHandling) Patches::ConnectionRegistry.apply! @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.
246 247 248 249 250 251 252 253 254 255 |
# File 'lib/apartment.rb', line 246 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
133 134 135 |
# File 'lib/apartment.rb', line 133 def activated? @activated == true end |
.clear_config ⇒ Object
Reset all configuration and stop background tasks.
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/apartment.rb', line 204 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
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/apartment.rb', line 185 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
330 331 332 333 334 335 336 337 338 |
# File 'lib/apartment.rb', line 330 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.
350 351 352 353 354 355 356 |
# File 'lib/apartment.rb', line 350 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).
162 163 164 165 166 |
# File 'lib/apartment.rb', line 162 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.
125 126 127 128 129 130 131 |
# File 'lib/apartment.rb', line 125 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).
113 114 115 |
# File 'lib/apartment.rb', line 113 def pinned_models @pinned_models ||= Concurrent::Set.new end |
.pool_in_use?(pool) ⇒ Boolean
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.
True when at least one of +pool+'s connections is leased or holds an open transaction (a long migration, a batch job, an unpinned fixture transaction). Discarding such a pool orphans that work, so every caller that removes a pool outside the reaper's own cycle checks this first. nil counts as not in use: an untracked pool has nothing to orphan.
321 322 323 324 325 326 327 328 |
# File 'lib/apartment.rb', line 321 def pool_in_use?(pool) return false unless pool.respond_to?(:connections) pool.connections.any? do |conn| (conn.respond_to?(:in_use?) && conn.in_use?) || (conn.respond_to?(:open_transactions) && conn.open_transactions.positive?) end end |
.pool_key(tenant, role) ⇒ 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.
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. The PoolManager key for one tenant under one connection role. Pools are keyed by both because a tenant migrated under an elevated role and served under the writing role are different pools with different credentials. deregister_ar_shard parses the role back off the tail, so the separator is part of the contract, not cosmetic.
311 312 313 |
# File 'lib/apartment.rb', line 311 def pool_key(tenant, role) "#{tenant}:#{role}" end |
.process_pinned_model(klass) ⇒ Object
168 169 170 171 172 173 174 175 |
# File 'lib/apartment.rb', line 168 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
117 118 119 |
# File 'lib/apartment.rb', line 117 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.
373 374 375 376 377 |
# File 'lib/apartment.rb', line 373 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.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/apartment.rb', line 145 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.
103 104 105 106 107 108 109 |
# File 'lib/apartment.rb', line 103 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 |