Module: Apartment::Tenant
- Defined in:
- lib/apartment/tenant.rb
Overview
rubocop:disable Metrics/ModuleLength
Class Method Summary collapse
-
.assert_tenant_switched!(message: nil) ⇒ Object
Raise if no tenant has been explicitly entered.
-
.cache_namespace ⇒ Object
Routed cache namespace helper: asserts a real, non-default tenant and returns its normalized name.
-
.create(tenant) ⇒ Object
Delegate lifecycle operations to the adapter.
-
.current ⇒ Object
Current tenant name.
-
.default_tenant ⇒ Object
The configured default tenant name (nil if Apartment is not configured).
- .drop(tenant) ⇒ Object
-
.each(tenants = nil) ⇒ Object
Iterate over all tenants, switching into each for the duration of the block.
-
.in_default_tenant? ⇒ Boolean
Predicate: is the effective tenant the default tenant? (Identity axis.) False when no default_tenant is configured.
-
.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.
-
.init ⇒ Object
Initialize: resolve excluded_models shim, then process pinned models.
- .migrate(tenant, version = nil) ⇒ Object
-
.pool_stats ⇒ Object
Pool stats delegated to pool_manager.
-
.require_default_tenant! ⇒ Object
Guard: raise unless the effective tenant is the default tenant.
-
.require_tenant! ⇒ Object
Guard: raise unless the effective tenant is a real, non-default tenant.
-
.reset ⇒ Object
Reset to default tenant.
- .seed(tenant) ⇒ Object
-
.switch(tenant, &block) ⇒ Object
Switch to a tenant for the duration of the block.
-
.switch!(tenant) ⇒ Object
Direct switch without block.
-
.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.
-
.with_default_tenant ⇒ Object
Establish the default/pinned tenant context for the block.
-
.with_tenants(*names) ⇒ Object
Convenience splat over with_tenants_provider for the common case of an enumerated list of names.
-
.with_tenants_provider(source) ⇒ Object
Block-scoped override of the tenant resolver.
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, || '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_namespace ⇒ Object
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 |
.current ⇒ Object
Current tenant name.
39 40 41 |
# File 'lib/apartment/tenant.rb', line 39 def current Current.tenant || default_tenant end |
.default_tenant ⇒ Object
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.
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.
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.
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 |
.init ⇒ Object
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_stats ⇒ Object
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 |
.reset ⇒ Object
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.
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.
64 65 66 |
# File 'lib/apartment/tenant.rb', line 64 def tenant_switched? !Current.tenant.nil? end |
.with_default_tenant ⇒ Object
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.
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
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 |