Skip to content

Commit 5848068

Browse files
committed
ShardSelector supports granular database connection switching
This allows for the ShardSelector to be used for granular switching on a specific abstract connection class. Previously switching was done globally on ActiveRecord::Base. Also updates the Multiple Databases guide and improves the ShardSelector documentation.
1 parent cd2f6b1 commit 5848068

File tree

4 files changed

+110
-36
lines changed

4 files changed

+110
-36
lines changed

activerecord/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
* `ActiveRecord::Middleware::ShardSelector` supports granular database connection switching.
2+
3+
A new configuration option, `class_name:`, is introduced to
4+
`config.active_record.shard_selector` to allow an application to specify the abstract connection
5+
class to be switched by the shard selection middleware. The default class is
6+
`ActiveRecord::Base`.
7+
8+
For example, this configuration tells `ShardSelector` to switch shards using
9+
`AnimalsRecord.connected_to`:
10+
11+
```
12+
config.active_record.shard_selector = { class_name: "AnimalsRecord" }
13+
```
14+
15+
*Mike Dalessio*
16+
117
* Reset relations after `insert_all`/`upsert_all`.
218
319
Bulk insert/upsert methods will now call `reset` if used on a relation, matching the behavior of `update_all`.

activerecord/lib/active_record/middleware/shard_selector.rb

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,40 @@ module Middleware
99
# shard to switch to and allows for applications to write custom strategies
1010
# for swapping if needed.
1111
#
12-
# The ShardSelector takes a set of options (currently only +lock+ is supported)
13-
# that can be used by the middleware to alter behavior. +lock+ is
14-
# true by default and will prohibit the request from switching shards once
15-
# inside the block. If +lock+ is false, then shard swapping will be allowed.
16-
# For tenant based sharding, +lock+ should always be true to prevent application
17-
# code from mistakenly switching between tenants.
12+
# == Setup
1813
#
19-
# Options can be set in the config:
14+
# Applications must provide a resolver that will provide application-specific logic for
15+
# selecting the appropriate shard. Setting +config.active_record.shard_resolver+ will cause
16+
# Rails to add ShardSelector to the default middleware stack.
2017
#
21-
# config.active_record.shard_selector = { lock: true }
18+
# The resolver, along with any configuration options, can be set in the application
19+
# configuration using an initializer like so:
2220
#
23-
# Applications must also provide the code for the resolver as it depends on application
24-
# specific models. An example resolver would look like this:
21+
# Rails.application.configure do
22+
# config.active_record.shard_selector = { lock: false, class_name: "AnimalsRecord" }
23+
# config.active_record.shard_resolver = ->(request) {
24+
# subdomain = request.subdomain
25+
# tenant = Tenant.find_by_subdomain!(subdomain)
26+
# tenant.shard
27+
# }
28+
# end
29+
#
30+
# == Configuration
31+
#
32+
# The behavior of ShardSelector can be altered through some configuration options.
33+
#
34+
# [+lock:+]
35+
# +lock+ is true by default and will prohibit the request from switching shards once inside
36+
# the block. If +lock+ is false, then shard switching will be allowed. For tenant based
37+
# sharding, +lock+ should always be true to prevent application code from mistakenly switching
38+
# between tenants.
39+
#
40+
# [+class_name:+]
41+
# +class_name+ is the name of the abstract connection class to switch. By
42+
# default, the ShardSelector will use ActiveRecord::Base, but if the
43+
# application has multiple databases, then this option should be set to
44+
# the name of the sharded database's abstract connection class.
2545
#
26-
# config.active_record.shard_resolver = ->(request) {
27-
# subdomain = request.subdomain
28-
# tenant = Tenant.find_by_subdomain!(subdomain)
29-
# tenant.shard
30-
# }
3146
class ShardSelector
3247
def initialize(app, resolver, options = {})
3348
@app = app
@@ -53,8 +68,10 @@ def selected_shard(request)
5368
end
5469

5570
def set_shard(shard, &block)
56-
ActiveRecord::Base.connected_to(shard: shard.to_sym) do
57-
ActiveRecord::Base.prohibit_shard_swapping(options.fetch(:lock, true), &block)
71+
klass = options[:class_name]&.constantize || ActiveRecord::Base
72+
73+
klass.connected_to(shard: shard.to_sym) do
74+
klass.prohibit_shard_swapping(options.fetch(:lock, true), &block)
5875
end
5976
end
6077
end

activerecord/test/cases/shard_selector_test.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,38 @@ def test_middleware_can_handle_string_shards
4141

4242
assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET")
4343
end
44+
45+
def test_middleware_can_do_granular_database_connection_switching
46+
klass = Class.new(ActiveRecord::Base) do |k|
47+
class << self
48+
attr_reader :connected_to_shard
49+
50+
def connected_to(shard:)
51+
@connected_to_shard = shard
52+
yield
53+
end
54+
55+
def prohibit_shard_swapping(...)
56+
yield
57+
end
58+
59+
def connected_to?(role: nil, shard:)
60+
@connected_to_shard.to_sym == shard.to_sym
61+
end
62+
end
63+
end
64+
Object.const_set :ShardSelectorTestModel, klass
65+
66+
middleware = ActiveRecord::Middleware::ShardSelector.new(lambda { |env|
67+
assert_not ActiveRecord::Base.connected_to?(role: :writing, shard: :shard_one)
68+
assert klass.connected_to?(role: :writing, shard: :shard_one)
69+
[200, {}, ["body"]]
70+
}, ->(*) { :shard_one }, { class_name: "ShardSelectorTestModel" })
71+
72+
assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET")
73+
assert_equal(:shard_one, klass.connected_to_shard)
74+
ensure
75+
Object.send(:remove_const, :ShardSelectorTestModel)
76+
end
4477
end
4578
end

guides/source/active_record_multiple_databases.md

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -493,29 +493,18 @@ end
493493

494494
## Activating Automatic Shard Switching
495495

496-
Applications are able to automatically switch shards per request using the provided
497-
middleware.
496+
Applications are able to automatically switch shards per request using the `ShardSelector`
497+
middleware, which allows an application to provide custom logic for determining the appropriate
498+
shard for each request.
498499

499-
The `ShardSelector` middleware provides a framework for automatically
500-
swapping shards. Rails provides a basic framework to determine which
501-
shard to switch to and allows for applications to write custom strategies
502-
for swapping if needed.
503-
504-
`ShardSelector` takes a set of options (currently only `lock` is supported)
505-
that can be used by the middleware to alter behavior. `lock` is
506-
true by default and will prohibit the request from switching shards once
507-
inside the block. If `lock` is false, then shard swapping will be allowed.
508-
For tenant based sharding, `lock` should always be true to prevent application
509-
code from mistakenly switching between tenants.
510-
511-
The same generator as the database selector can be used to generate the file for
512-
automatic shard swapping:
500+
The same generator used for the database selector above can be used to generate an initializer file
501+
for automatic shard swapping:
513502

514503
```bash
515504
$ bin/rails g active_record:multi_db
516505
```
517506

518-
Then in the generated `config/initializers/multi_db.rb` uncomment the following:
507+
Then in the generated `config/initializers/multi_db.rb` uncomment and modify the following code:
519508

520509
```ruby
521510
Rails.application.configure do
@@ -524,8 +513,8 @@ Rails.application.configure do
524513
end
525514
```
526515

527-
Applications must provide the code for the resolver as it depends on application
528-
specific models. An example resolver would look like this:
516+
Applications must provide a resolver to provide application-specific logic. An example resolver that
517+
uses subdomain to determine the shard might look like this:
529518

530519
```ruby
531520
config.active_record.shard_resolver = ->(request) {
@@ -535,6 +524,25 @@ config.active_record.shard_resolver = ->(request) {
535524
}
536525
```
537526

527+
The behavior of `ShardSelector` can be altered through some configuration options.
528+
529+
`lock` is true by default and will prohibit the request from switching shards during the request. If
530+
`lock` is false, then shard swapping will be allowed. For tenant-based sharding, `lock` should
531+
always be true to prevent application code from mistakenly switching between tenants.
532+
533+
`class_name` is the name of the abstract connection class to switch. By default, the `ShardSelector`
534+
will use `ActiveRecord::Base`, but if the application has multiple databases, then this option
535+
should be set to the name of the sharded database's abstract connection class.
536+
537+
Options may be set in the application configuration. For example, this configuration tells
538+
`ShardSelector` to switch shards using `AnimalsRecord.connected_to`:
539+
540+
541+
``` ruby
542+
config.active_record.shard_selector = { lock: true, class_name: "AnimalsRecord" }
543+
```
544+
545+
538546
## Granular Database Connection Switching
539547

540548
Starting from Rails 6.1, it's possible to switch connections for one database

0 commit comments

Comments
 (0)