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

Class Method Summary collapse

Class Attribute Details

.adapterObject

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

.configObject (readonly)

Returns the value of attribute config.



56
57
58
# File 'lib/apartment.rb', line 56

def config
  @config
end

.pool_managerObject (readonly)

Returns the value of attribute pool_manager.



56
57
58
# File 'lib/apartment.rb', line 56

def pool_manager
  @pool_manager
end

.pool_reaperObject (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 activate_sql_query_tags!
  return unless @config&.sql_query_tags
  return unless defined?(ActiveRecord::QueryLogs)
  return if ActiveRecord::QueryLogs.tags.include?(:tenant)

  ActiveRecord::QueryLogs.taggings = ActiveRecord::QueryLogs.taggings.merge(
    tenant: -> { Apartment::Current.tenant }
  )
  ActiveRecord::QueryLogs.tags = ActiveRecord::QueryLogs.tags + [:tenant]
end

.activated?Boolean

Returns:

  • (Boolean)


133
134
135
# File 'lib/apartment.rb', line 133

def activated?
  @activated == true
end

.clear_configObject

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

Configure Apartment v4. Yields a Config instance, validates it, and prepares the module for use.

Apartment.configure do |config|
config.tenant_strategy = :schema
config.tenants_provider = -> { Tenant.pluck(:name) }
end

Yields:

  • (new_config)

Raises:



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.message}"
end

.excluded_modelsObject

v3 compatibility: Apartment.excluded_models returns the excluded models list. Deprecated in v4 (use Apartment::Model + pin_tenant instead).

Raises:



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.

Returns:

  • (Boolean)


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_modelsObject

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.

Returns:

  • (Boolean)


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.

See Also:



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_namesObject

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.

Raises:



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_validatorObject

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