Skip to content

Commit fe67fc8

Browse files
committed
Test connection pools and schema migrations
1 parent f3d74bd commit fe67fc8

File tree

13 files changed

+138
-17
lines changed

13 files changed

+138
-17
lines changed

GUIDE.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,21 @@ TODO:
6969
- [ ] should error if self is not an abstract base class or if target is not tenanted abstract base class
7070
- [x] `tenant_config_name` and `.tenanted?`
7171
- [ ] `.tenanted_class` nil or the abstract base class
72-
- [ ] all the creation and schema migration complications (we have existing tests for this)
73-
- think about race conditions here, maybe use a file lock to figure it out
74-
- running migrations (they are done in a transaction, but the second thread's migration may fail resulting in a 500?)
75-
- loading schemas (if the first thread loads the schema and inserts data, can the second thread accidentally drop/load causing data loss?)
72+
- [x] shared connection pools
73+
- [x] all the creation and schema migration complications (we have existing tests for this)
74+
accidentally drop/load causing data loss?)
7675
- [ ] feature to turn off automatic creation/migration
7776
- make sure we pay attention to Rails.config.active_record.migration_error when we turn off auto-migrating
7877
- thinking
7978
- should there be a global singleton `Tenant`? I'm not sure we need it or the limitations of a global.
8079
- if we do, though, then `.tenanted` should set `Tenant.base_class=`
8180
- and we need to add checks that `.tenanted` is called only ONCE in the application
8281

82+
- [ ] think about race conditions with database creation and schema migrations
83+
- maybe use a file lock to figure it out
84+
- running migrations (they are done in a transaction, but the second thread's migration may fail resulting in a 500?)
85+
- loading schemas (if the first thread loads the schema and inserts data, can the second thread
86+
8387
- database tasks
8488
- [ ] RootConfig should conditionally re-enable database tasks ... when AR_TENANT is present?
8589
- [ ] make `db:migrate:tenants` iterate over all the tenants on disk

lib/active_record/tenanted/database_configurations.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ class RootConfig < ActiveRecord::DatabaseConfigurations::HashConfig
99
def database_tasks?
1010
false
1111
end
12+
13+
def database_path_for(tenant_name)
14+
format_specifiers = {
15+
tenant: tenant_name,
16+
}
17+
database % format_specifiers
18+
end
19+
end
20+
21+
class TenantConfig < RootConfig
1222
end
1323
end
1424
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveRecord
4+
module Tenanted
5+
module Patches
6+
# TODO: I think this is needed because there was no followup to rails/rails#46270.
7+
# See rails/rails@901828f2 from that PR for background.
8+
module DatabaseTasks
9+
private def with_temporary_pool(db_config, clobber: false)
10+
original_db_config = begin
11+
migration_class.connection_db_config
12+
rescue ActiveRecord::ConnectionNotDefined
13+
nil
14+
end
15+
16+
begin
17+
pool = migration_class.connection_handler.establish_connection(db_config, clobber: clobber)
18+
19+
yield pool
20+
ensure
21+
migration_class.connection_handler.establish_connection(original_db_config, clobber: clobber) if original_db_config
22+
end
23+
end
24+
end
25+
end
26+
end
27+
end

lib/active_record/tenanted/railtie.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ class Railtie < ::Rails::Railtie
1717
prepend ActiveRecord::Tenanted::Base
1818
end
1919
end
20+
21+
initializer "active_record-tenanted.monkey_patches.active_record" do
22+
ActiveSupport.on_load(:active_record) do
23+
# require "rails/generators/active_record/migration.rb"
24+
# ActiveRecord::Generators::Migration.prepend(ActiveRecord::Tenanted::Patches::Migration)
25+
ActiveRecord::Tasks::DatabaseTasks.prepend(ActiveRecord::Tenanted::Patches::DatabaseTasks)
26+
end
27+
end
2028
end
2129
end
2230
end

lib/active_record/tenanted/tenant.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,42 @@ def while_tenanted(tenant_name, &block)
3030

3131
def connection_pool
3232
raise NoTenantError unless current_tenant
33+
34+
pool = connection_handler.retrieve_connection_pool(connection_specification_name, role: current_role, shard: current_tenant, strict: false)
35+
36+
if pool.nil?
37+
create_tenanted_pool
38+
pool = connection_handler.retrieve_connection_pool(connection_specification_name, role: current_role, shard: current_tenant, strict: true)
39+
end
40+
41+
pool
42+
end
43+
44+
def create_tenanted_pool # :nodoc:
45+
# ensure all classes use the same connection pool
46+
return superclass.create_tenanted_pool unless connection_class?
47+
48+
tenant = current_tenant
49+
base_config = ActiveRecord::Base.configurations.resolve(tenanted_config_name.to_sym)
50+
tenant_name = "#{tenanted_config_name}_#{tenant}"
51+
config_hash = base_config.configuration_hash.dup.tap do |hash|
52+
hash[:database] = base_config.database_path_for(tenant)
53+
end
54+
config = Tenanted::DatabaseConfigurations::TenantConfig.new(base_config.env_name, tenant_name, config_hash)
55+
56+
establish_connection(config)
57+
ensure_schema_migrations(config)
58+
end
59+
60+
def ensure_schema_migrations(config) # :nodoc:
61+
ActiveRecord::Tasks::DatabaseTasks.with_temporary_connection(config) do |conn|
62+
pool = conn.pool
63+
64+
if pool.migration_context.pending_migration_versions.present?
65+
ActiveRecord::Tasks::DatabaseTasks.migrate(nil)
66+
# ActiveRecord::Tasks::DatabaseTasks.dump_schema(config) if Rails.env.development?
67+
end
68+
end
3369
end
3470
end
3571
end

test/scenarios/vanilla/database.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ test:
66
tenanted: true
77
adapter: "sqlite3"
88
database: "%{storage}/test/primary/%%{tenant}/main.sqlite3"
9-
migrations_paths: "%{__dir__}/primary_migrations"
9+
migrations_paths: "%{scenario}/primary_migrations"
1010
shared:
1111
adapter: "sqlite3"
1212
database: "%{storage}/test/shared.sqlite3"
13-
migrations_paths: "%{__dir__}/shared_migrations"
13+
migrations_paths: "%{scenario}/shared_migrations"

test/scenarios/vanilla/tenanted_primary.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class TenantedApplicationRecord < ActiveRecord::Base
99
class User < TenantedApplicationRecord
1010
end
1111

12+
class Post < TenantedApplicationRecord
13+
end
14+
1215
class SharedApplicationRecord < ActiveRecord::Base
1316
self.abstract_class = true
1417

test/scenarios/vanilla/tenanted_secondary.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class TenantedApplicationRecord < ActiveRecord::Base
99
class User < TenantedApplicationRecord
1010
end
1111

12+
class Post < TenantedApplicationRecord
13+
end
14+
1215
class SharedApplicationRecord < ActiveRecord::Base
1316
primary_abstract_class
1417

test/scenarios/vanilla_named_primary/database.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ test:
66
tenanted: true
77
adapter: "sqlite3"
88
database: "%{storage}/test/tenanted/%%{tenant}/main.sqlite3"
9-
migrations_paths: "%{__dir__}/tenanted_migrations"
9+
migrations_paths: "%{scenario}/tenanted_migrations"
1010
shared:
1111
adapter: "sqlite3"
1212
database: "%{storage}/test/shared.sqlite3"
13-
migrations_paths: "%{__dir__}/shared_migrations"
13+
migrations_paths: "%{scenario}/shared_migrations"

test/scenarios/vanilla_named_primary/tenanted_primary.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class TenantedApplicationRecord < ActiveRecord::Base
99
class User < TenantedApplicationRecord
1010
end
1111

12+
class Post < TenantedApplicationRecord
13+
end
14+
1215
class SharedApplicationRecord < ActiveRecord::Base
1316
self.abstract_class = true
1417

0 commit comments

Comments
 (0)