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/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/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/adapters/postgresql_schema_adapter.rb,
lib/apartment/adapters/postgresql_database_adapter.rb,
lib/generators/apartment/install/install_generator.rb

Overview

rubocop:disable Metrics/ModuleLength

Defined Under Namespace

Modules: Adapters, Configs, Elevators, Instrumentation, Lifecycle, Model, Patches, SchemaCache, SchemaDumperPatch, Tenant, TenantNameValidator, TestFixtures Classes: AdapterNotFound, ApartmentError, CLI, Config, ConfigurationError, Current, DefaultTenantNotConfigured, DefaultTenantRequired, FixtureLifecycleViolation, InstallGenerator, Migrator, PendingMigrationError, PoolExhausted, PoolManager, 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.alpha2'

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



51
52
53
# File 'lib/apartment.rb', line 51

def adapter
  @adapter ||= build_adapter
end

.configObject (readonly)

Returns the value of attribute config.



46
47
48
# File 'lib/apartment.rb', line 46

def config
  @config
end

.pool_managerObject (readonly)

Returns the value of attribute pool_manager.



46
47
48
# File 'lib/apartment.rb', line 46

def pool_manager
  @pool_manager
end

.pool_reaperObject (readonly)

Returns the value of attribute pool_reaper.



46
47
48
# File 'lib/apartment.rb', line 46

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.



195
196
197
198
199
# File 'lib/apartment.rb', line 195

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.



204
205
206
207
208
209
210
211
212
213
# File 'lib/apartment.rb', line 204

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)


95
96
97
# File 'lib/apartment.rb', line 95

def activated?
  @activated == true
end

.clear_configObject

Reset all configuration and stop background tasks.



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/apartment.rb', line 175

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:



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/apartment.rb', line 147

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
  @pool_manager = PoolManager.new
  @pool_reaper = PoolReaper.new(
    pool_manager: @pool_manager,
    interval: new_config.pool_idle_timeout,
    idle_timeout: new_config.pool_idle_timeout,
    max_total: new_config.max_total_connections,
    default_tenant: new_config.default_tenant,
    shard_key_prefix: new_config.shard_key_prefix
  )
  @pool_reaper.start
  @config
end

.deregister_shard(pool_key) ⇒ Object

Deregister a single tenant’s shard from AR’s ConnectionHandler. Safe to call when AR is not loaded or config is not set (no-op). Used by PoolReaper eviction, AbstractAdapter#drop, and teardown.



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/apartment.rb', line 218

def deregister_shard(pool_key)
  return unless @config && defined?(ActiveRecord::Base)

  _, separator, role_str = pool_key.to_s.rpartition(':')
  role = separator.empty? || role_str.empty? ? ActiveRecord.writing_role : role_str.to_sym

  shard_key = :"#{@config.shard_key_prefix}_#{pool_key}"
  ActiveRecord::Base.connection_handler.remove_connection_pool(
    'ActiveRecord::Base',
    role: role,
    shard: shard_key
  )
rescue StandardError => e
  warn "[Apartment] Failed to deregister AR 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:



124
125
126
127
128
# File 'lib/apartment.rb', line 124

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)


87
88
89
90
91
92
93
# File 'lib/apartment.rb', line 87

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



75
76
77
# File 'lib/apartment.rb', line 75

def pinned_models
  @pinned_models ||= Concurrent::Set.new
end

.process_pinned_model(klass) ⇒ Object



130
131
132
133
134
135
136
137
# File 'lib/apartment.rb', line 130

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



79
80
81
# File 'lib/apartment.rb', line 79

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:



249
250
251
252
253
# File 'lib/apartment.rb', line 249

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:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/apartment.rb', line 107

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.



65
66
67
68
69
70
71
# File 'lib/apartment.rb', line 65

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