Module: Apartment::Tenant

Defined in:
lib/apartment/tenant.rb

Overview

rubocop:disable Metrics/ModuleLength

Class Method Summary collapse

Class Method Details

.assert_tenant_switched!(message: nil) ⇒ Object

Raise if no tenant has been explicitly entered. (Explicitness axis.) Test-time discipline for suites that want to fail loudly when ambient writes would land in the default tenant. No-op when a tenant is active.



71
72
73
74
75
76
77
78
79
# File 'lib/apartment/tenant.rb', line 71

def assert_tenant_switched!(message: nil)
  return if tenant_switched?

  raise(Apartment::ApartmentError,
        message ||
        'Expected an explicit tenant context, but Apartment::Current.tenant is nil. ' \
        'Wrap the call in Apartment::Tenant.switch(tenant) { ... } or call ' \
        'Apartment::Tenant.switch!(tenant).')
end

.cache_namespaceObject

Routed cache namespace helper: asserts a real, non-default tenant and returns its normalized name. Intended as a fail-closed cache namespace proc — ‘namespace: -> { Apartment::Tenant.cache_namespace }`.



120
121
122
# File 'lib/apartment/tenant.rb', line 120

def cache_namespace
  require_tenant!
end

.create(tenant) ⇒ Object

Delegate lifecycle operations to the adapter.



168
169
170
# File 'lib/apartment/tenant.rb', line 168

def create(tenant)
  adapter.create(tenant)
end

.currentObject

Current tenant name.



39
40
41
# File 'lib/apartment/tenant.rb', line 39

def current
  Current.tenant || default_tenant
end

.default_tenantObject

The configured default tenant name (nil if Apartment is not configured). v4 relocated this value to Apartment.config.default_tenant; this reader restores the v3 Apartment::Tenant.default_tenant accessor so consumers that read the default tenant need not branch on the apartment version.



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

def default_tenant
  Apartment.config&.default_tenant
end

.drop(tenant) ⇒ Object



172
173
174
# File 'lib/apartment/tenant.rb', line 172

def drop(tenant)
  adapter.drop(tenant)
end

.each(tenants = nil) ⇒ Object

Iterate over all tenants, switching into each for the duration of the block. Accepts an optional tenant list; defaults to tenants_provider. Fail-fast: raises immediately if a block raises for any tenant; tenants after the failing one are not visited.

Raises:

  • (ArgumentError)


188
189
190
191
192
193
# File 'lib/apartment/tenant.rb', line 188

def each(tenants = nil)
  raise(ArgumentError, 'Apartment::Tenant.each requires a block') unless block_given?

  tenants ||= Apartment.tenant_names
  tenants.each { |tenant| switch(tenant) { yield(tenant) } }
end

.in_default_tenant?Boolean

Predicate: is the effective tenant the default tenant? (Identity axis.) False when no default_tenant is configured.

Returns:

  • (Boolean)


92
93
94
95
# File 'lib/apartment/tenant.rb', line 92

def in_default_tenant?
  default = default_tenant
  !default.nil? && current.to_s == default.to_s
end

.in_tenant?Boolean

Predicate: is the effective tenant a real, NON-default tenant? (Identity axis — reads Tenant.current, default fallback included.) False for nil/empty current (e.g. switch!(“”) bypasses name validation) and false when no default_tenant is configured and no tenant is active.

Returns:

  • (Boolean)


85
86
87
88
# File 'lib/apartment/tenant.rb', line 85

def in_tenant?
  c = current.to_s
  !c.empty? && c != default_tenant.to_s
end

.initObject

Initialize: resolve excluded_models shim, then process pinned models.



162
163
164
165
# File 'lib/apartment/tenant.rb', line 162

def init
  resolve_excluded_models_shim
  adapter.process_pinned_models
end

.migrate(tenant, version = nil) ⇒ Object



176
177
178
# File 'lib/apartment/tenant.rb', line 176

def migrate(tenant, version = nil)
  adapter.migrate(tenant, version)
end

.pool_statsObject

Pool stats delegated to pool_manager.



258
259
260
# File 'lib/apartment/tenant.rb', line 258

def pool_stats
  Apartment.pool_manager&.stats || {}
end

.require_default_tenant!Object

Guard: raise unless the effective tenant is the default tenant. Returns the normalized default name on success. Raises DefaultTenantNotConfigured when no default_tenant is configured (a nil keyspace is a silent leak).



109
110
111
112
113
114
115
# File 'lib/apartment/tenant.rb', line 109

def require_default_tenant!
  default = default_tenant
  raise(Apartment::DefaultTenantNotConfigured) if default.nil?
  return default.to_s if current.to_s == default.to_s

  raise(Apartment::DefaultTenantRequired.new(current, default))
end

.require_tenant!Object

Guard: raise unless the effective tenant is a real, non-default tenant. Returns the normalized tenant name on success (a documented convenience; the cache recipe uses cache_namespace, not this return, for the proc).



100
101
102
103
104
# File 'lib/apartment/tenant.rb', line 100

def require_tenant!
  return current.to_s if in_tenant?

  raise(Apartment::TenantRequired, current)
end

.resetObject

Reset to default tenant.



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

def reset
  switch!(default_tenant)
end

.seed(tenant) ⇒ Object



180
181
182
# File 'lib/apartment/tenant.rb', line 180

def seed(tenant)
  adapter.seed(tenant)
end

.switch(tenant, &block) ⇒ Object

Switch to a tenant for the duration of the block. Guaranteed cleanup via ensure — tenant context is always restored.

Note: previous_tenant reflects only the immediately preceding tenant for the current switch scope. It is not stacked across nesting levels —after an inner switch completes, previous_tenant resets to nil.

Raises:

  • (ArgumentError)


12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/apartment/tenant.rb', line 12

def switch(tenant, &block)
  raise(ArgumentError, 'Apartment::Tenant.switch requires a block') unless block

  guard_default_tenant_switch!(tenant)

  previous = Current.tenant
  begin
    Current.tenant = tenant
    Current.previous_tenant = previous
    if tagged_logging?
      Rails.logger.tagged("tenant=#{tenant}", &block)
    else
      yield
    end
  ensure
    Current.tenant = previous
    Current.previous_tenant = nil
  end
end

.switch!(tenant) ⇒ Object

Direct switch without block. Discouraged — prefer switch with block.



33
34
35
36
# File 'lib/apartment/tenant.rb', line 33

def switch!(tenant)
  Current.previous_tenant = Current.tenant
  Current.tenant = tenant
end

.tenant_switched?Boolean

Predicate: was a tenant explicitly entered? (Explicitness axis.) Reads Current.tenant directly (not Tenant.current) so it does NOT consider the default_tenant fallback. Use this when “did this code explicitly enter a tenant?” matters more than “what tenant is effectively active?” — typically test setup and assertion code.

Note: after Tenant.reset, tenant_switched? returns true. reset enters the default tenant via switch!, which is an explicit entry.

Returns:

  • (Boolean)


64
65
66
# File 'lib/apartment/tenant.rb', line 64

def tenant_switched?
  !Current.tenant.nil?
end

.with_default_tenantObject

Establish the default/pinned tenant context for the block. On exit or raise, restores the prior Current.tenant (including nil) and resets Current.previous_tenant to nil — same single-level (non-stacking) contract as switch/switch! (except a call already in the default context, which is the no-op fast path below). Enters default via a direct Current.tenant assignment that bypasses guard_default_tenant_switch!, so it is NOT blocked by default_tenant_switch_allowed = false. Use for pinned/global work (e.g. writing app-wide cache keys).

Raises DefaultTenantNotConfigured when no default_tenant is configured, mirroring require_default_tenant! — entering a nil keyspace for pinned work is a silent leak, not a valid global context.

Raises:

  • (ArgumentError)


136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/apartment/tenant.rb', line 136

def with_default_tenant
  raise(ArgumentError, 'Apartment::Tenant.with_default_tenant requires a block') unless block_given?

  default = default_tenant
  raise(Apartment::DefaultTenantNotConfigured) if default.nil?

  # Fast path: already in the default context, so entering it is a no-op —
  # skip the assign/restore, leaving previous_tenant untouched. to_s matches
  # the sibling guards (symbol/string default); ambient nil ('') never matches
  # a real default, so it takes the full path below. No ensure here: relies on
  # the block restoring its own context, true for block-form switch. See
  # docs/designs/with-default-tenant-short-circuit.md.
  return yield if Current.tenant.to_s == default.to_s

  previous = Current.tenant
  begin
    Current.tenant = default
    Current.previous_tenant = previous
    yield
  ensure
    Current.tenant = previous
    Current.previous_tenant = nil
  end
end

.with_tenants(*names) ⇒ Object

Convenience splat over with_tenants_provider for the common case of an enumerated list of names.

Apartment::Tenant.with_tenants('acme', 'widgets') do
  Apartment::Tenant.each { |t| ... }
end


253
254
255
# File 'lib/apartment/tenant.rb', line 253

def with_tenants(*names, &)
  with_tenants_provider(names, &)
end

.with_tenants_provider(source) ⇒ Object

Block-scoped override of the tenant resolver. For the duration of the block, every “what tenants do we have?” call site (Apartment.tenant_names, Tenant.each, Migrator, SchemaCache, CLI commands) reads from source instead of config.tenants_provider.

The override is in-process, fiber-safe, and block-local. It does NOT automatically propagate to ActiveJob workers, child threads, or other processes — pass tenant names as job arguments if cross-process scoping is required.

Accepted shapes for source:

* A callable (responds to +:call+) — re-evaluated on every
  +Apartment.tenant_names+ access inside the block. Use a frozen Array
  instead if you need a stable snapshot.
* A String or Symbol — coerced to a single-element Array of strings.
* An Array of String/Symbol — coerced to an Array of strings.

Anything else (nil, Hash, arbitrary object) raises ArgumentError. Empty arrays are honored — Tenant.each yields zero times. Nesting fully replaces the outer override; the previous value is restored on block exit (including via raise).

The accepted shapes are intentionally broader than config.tenants_provider, which requires a callable. The block override targets test-suite ergonomics where a literal list is the natural call shape; the configured provider stays callable-only because production tenant lists are nearly always backed by a query that should resolve at access time. The contract that internal callers see — what Apartment.tenant_names returns — is identical: an object that responds to :each, validated at resolution.

Apartment::Tenant.with_tenants_provider(['acme', 'widgets']) do
  Apartment::Tenant.each { |t| ... }       # yields acme, widgets only
end

Apartment::Tenant.with_tenants_provider(-> { Account.recent.pluck(:id) }) do
  Apartment::Tenant.each { |t| ... }
end

Raises:

  • (ArgumentError)


233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/apartment/tenant.rb', line 233

def with_tenants_provider(source)
  raise(ArgumentError, 'Apartment::Tenant.with_tenants_provider requires a block') unless block_given?

  override = coerce_tenant_override(source)

  previous = Current.tenant_override
  Current.tenant_override = override
  begin
    yield
  ensure
    Current.tenant_override = previous
  end
end