Skip to content

Commit 1e92cb3

Browse files
authored
Introduce callbacks for tenant change methods (#247)
to allow applications to extend tenant change behavior. I'm using this functionality in my current project to cascade a _second_ tenant change in another database, something like this: ```ruby module ApplicationRecordWithTenantZoneExtension extend ActiveSupport::Concern class_methods do def with_zone(&block) ZonedRecord.with_tenant(tenant_zone, &block) end def set_queue_zone ZonedRecord.current_tenant = tenant_zone end def tenant_zone # ... business logic to determine the "zone" for ApplicationRecord.current_tenant end end included do set_callback :with_tenant, :around, :with_zone set_callback :set_current_tenant, :after, :set_zone end end Rails.application.config.to_prepare do ApplicationRecord.include ApplicationRecordWithTenantZoneExtension end ``` so that the application only needs to call `ApplicationTenant.with_tenant` and the tenant is automatically set on the secondary database as well.
2 parents e18f83f + eaf012a commit 1e92cb3

File tree

4 files changed

+69
-5
lines changed

4 files changed

+69
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ Read the [Rails Guide documentation on `config.active_record.query_log_tags`](ht
5959

6060
### Added
6161

62+
- Add callbacks for `:with_tenant` which are invoked when `.with_tenant` is called.
63+
- Add callbacks for `:set_current_tenant` which are invoked when `.current_tenant=` is called.
6264
- `UntenantedConnectionPool#size` returns the database configuration's `max_connections` value, so that code (like Solid Queue) can inspect config params without a tenant context.
6365

6466

GUIDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,11 @@ TODO:
396396

397397
Documentation outline:
398398

399-
- invalid characters in a tenant name (which is database-dependent)
399+
- setting the tenant
400+
- `.with_tenant` and `.current_tenant=`
401+
- and the callbacks for each, `:with_tenant` and `:set_current_tenant`
402+
- validation
403+
- invalid characters in a tenant name (which is database-dependent)
400404
- and how the application may want to do additional validation (e.g. ICANN subdomain restrictions)
401405
- `#tenant` is a readonly attribute on all tenanted model instances
402406
- `.current_tenant` returns the execution context for the model connection class

lib/active_record/tenanted/tenant.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ def current_tenant=(tenant_name)
104104
tenant_name = tenant_name.to_s
105105
end
106106

107-
connection_class_for_self.connecting_to(shard: tenant_name, role: ActiveRecord.writing_role)
107+
run_callbacks :set_current_tenant do
108+
connection_class_for_self.connecting_to(shard: tenant_name, role: ActiveRecord.writing_role)
109+
end
108110
end
109111

110112
def tenant_exist?(tenant_name)
@@ -115,11 +117,13 @@ def with_tenant(tenant_name, prohibit_shard_swapping: true, &block)
115117
tenant_name = tenant_name.to_s unless tenant_name == UNTENANTED_SENTINEL
116118

117119
if tenant_name == current_tenant
118-
yield
120+
run_callbacks :with_tenant, &block
119121
else
120122
connection_class_for_self.connected_to(shard: tenant_name, role: ActiveRecord.writing_role) do
121-
prohibit_shard_swapping(prohibit_shard_swapping) do
122-
log_tenant_tag(tenant_name, &block)
123+
run_callbacks :with_tenant do
124+
prohibit_shard_swapping(prohibit_shard_swapping) do
125+
log_tenant_tag(tenant_name, &block)
126+
end
123127
end
124128
end
125129
end
@@ -262,9 +266,13 @@ def log_tenant_tag(tenant_name, &block)
262266
self.default_shard = ActiveRecord::Tenanted::Tenant::UNTENANTED_SENTINEL
263267

264268
prepend TenantCommon
269+
extend ActiveSupport::Callbacks
265270

266271
cattr_accessor :tenanted_config_name
267272
cattr_accessor(:tenanted_connection_pools) { LRU.new }
273+
274+
define_callbacks :with_tenant
275+
define_callbacks :set_current_tenant
268276
end
269277

270278
def tenanted?

test/unit/tenant_test.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,22 @@
115115
assert_equal("bar", TenantedApplicationRecord.current_tenant)
116116
end
117117

118+
test ".current_tenant= fires callbacks" do
119+
before_callback_fired = false
120+
after_callback_fired = false
121+
TenantedApplicationRecord.set_callback :set_current_tenant, :before do
122+
before_callback_fired = true
123+
end
124+
TenantedApplicationRecord.set_callback :set_current_tenant, :after do
125+
after_callback_fired = true
126+
end
127+
128+
TenantedApplicationRecord.current_tenant = "foo"
129+
130+
assert(before_callback_fired, "Before callback should be fired")
131+
assert(after_callback_fired, "After callback should be fired")
132+
end
133+
118134
test "using a record after changing tenant raises WrongTenantError" do
119135
TenantedApplicationRecord.create_tenant("foo")
120136
TenantedApplicationRecord.create_tenant("bar")
@@ -281,6 +297,40 @@
281297
TenantedApplicationRecord.with_tenant("baz") { User.count }
282298
end
283299
end
300+
301+
test ".with_tenant fires callbacks" do
302+
around_callback_fired = false
303+
block_called = false
304+
TenantedApplicationRecord.set_callback :with_tenant, :around do |_, block|
305+
around_callback_fired = true
306+
block.call
307+
end
308+
309+
TenantedApplicationRecord.with_tenant("foo") do
310+
block_called = true
311+
end
312+
313+
assert(around_callback_fired, "Around callback should be fired")
314+
assert(block_called, "Block should be called")
315+
end
316+
317+
test ".with_tenant fires callbacks even when tenant doesn't change" do
318+
around_callback_fired = 0
319+
block_called = false
320+
TenantedApplicationRecord.set_callback :with_tenant, :around do |_, block|
321+
around_callback_fired += 1
322+
block.call
323+
end
324+
325+
TenantedApplicationRecord.with_tenant("foo") do
326+
TenantedApplicationRecord.with_tenant("foo") do
327+
block_called = true
328+
end
329+
end
330+
331+
assert_equal(2, around_callback_fired, "Around callback should be fired for each call")
332+
assert(block_called, "Block should be called")
333+
end
284334
end
285335

286336
for_each_scenario(except: { primary_db: [ :subtenant_record ] }) do

0 commit comments

Comments
 (0)