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

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=.



61
62
63
# File 'lib/apartment.rb', line 61

def adapter
  @adapter ||= build_adapter
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 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 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)


105
106
107
# File 'lib/apartment.rb', line 105

def activated?
  @activated == true
end

.clear_configObject

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

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:



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

.excluded_modelsObject

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

Raises:



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.

Returns:

  • (Boolean)


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_modelsObject

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.

See Also:



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_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:



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_validatorObject

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