forked from rails/rails
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add automatic shard swapping middleware
This PR adds a middleware that can be used for automatic shard swapping. The design is similar to the database selector middleware in that the resolver is provided by the application to determine which shard to switch to. The selector also takes options to change the default behavior of the middleware. The only supported option is `lock` at the moment which will allow shard swapping in a request, otherwise it defaults to true and prevents shard swapping. This will help protect mistakenly switching shards inside of application code in a multi-tenant application. The resolver can be designed however the application wants but the basic idea is that the resolver accesses the `request` headers and uses that to lookup the subdomain which then looks up the tenant shard name stored in that table. The tenant table is the "router" for the entire application and an example resolver looks like this: ```ruby config.active_record.shard_resolver = ->(request) { subdomain = request.subdomain tenant = Tenant.find_by_subdomain!(subdomain) tenant.shard } ``` The `Tenant` table in this example would have `subdomain` and `shard` attributes that are inserted into the database. These are used to route to the shard in `connected_to`. Ie if we had a `Tenant` with the subdomain `github` and the shard name `github_shard` we'd lookup the connection with `ActiveRecord::Base.connected_to(shard: :github_shard) {}` and all queries within that block (request) would be scoped to the github shard. Co-authored-by: John Crepezzi <[email protected]>
- Loading branch information
1 parent
a0e14a8
commit 6f02a2a
Showing
10 changed files
with
195 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
activerecord/lib/active_record/middleware/shard_selector.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# frozen_string_literal: true | ||
|
||
require "active_record/middleware/database_selector/resolver" | ||
|
||
module ActiveRecord | ||
module Middleware | ||
# The ShardSelector Middleware provides a framework for automatically | ||
# swapping shards. Rails provides a basic framework to determine which | ||
# shard to switch to and allows for applications to write custom strategies | ||
# for swapping if needed. | ||
# | ||
# The ShardSelector takes a set of options (currently only `lock` is supported) | ||
# that can be used by the middleware to alter behavior. `lock` is | ||
# true by default and will prohibit the request from switching shards once | ||
# inside the block. If `lock` is false, then shard swapping will be allowed. | ||
# For tenant based sharding, `lock` should always be true to prevent application | ||
# code from mistakenly switching between tenants. | ||
# | ||
# Options can be set in the config: | ||
# | ||
# config.active_record.shard_selector = { lock: true } | ||
# | ||
# Applications must also provide the code for the resolver as it depends on application | ||
# specific models. An example resolver would look like this: | ||
# | ||
# config.active_record.shard_resolver = ->(request) { | ||
# subdomain = request.subdomain | ||
# tenant = Tenant.find_by_subdomain!(subdomain) | ||
# tenant.shard | ||
# } | ||
class ShardSelector | ||
def initialize(app, resolver, options = {}) | ||
@app = app | ||
@resolver = resolver | ||
@options = options | ||
end | ||
|
||
attr_reader :resolver, :options | ||
|
||
def call(env) | ||
request = ActionDispatch::Request.new(env) | ||
|
||
shard = selected_shard(request) | ||
|
||
set_shard(shard) do | ||
@app.call(env) | ||
end | ||
end | ||
|
||
private | ||
def selected_shard(request) | ||
resolver.call(request) | ||
end | ||
|
||
def set_shard(shard, &block) | ||
ActiveRecord::Base.connected_to(shard: shard.to_sym) do | ||
ActiveRecord::Base.prohibit_shard_swapping(options.fetch(:lock, true), &block) | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# frozen_string_literal: true | ||
|
||
require "cases/helper" | ||
require "models/person" | ||
require "action_dispatch" | ||
|
||
module ActiveRecord | ||
class ShardSelectorTest < ActiveRecord::TestCase | ||
def test_middleware_locks_to_shard_by_default | ||
middleware = ActiveRecord::Middleware::ShardSelector.new(lambda { |env| | ||
assert_predicate ActiveRecord::Base, :shard_swapping_prohibited? | ||
[200, {}, ["body"]] | ||
}, ->(*) { :shard_one }) | ||
|
||
assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET") | ||
end | ||
|
||
def test_middleware_can_turn_off_lock_option | ||
middleware = ActiveRecord::Middleware::ShardSelector.new(lambda { |env| | ||
assert_not_predicate ActiveRecord::Base, :shard_swapping_prohibited? | ||
[200, {}, ["body"]] | ||
}, ->(*) { :shard_one }, { lock: false }) | ||
|
||
assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET") | ||
end | ||
|
||
def test_middleware_can_change_shards | ||
middleware = ActiveRecord::Middleware::ShardSelector.new(lambda { |env| | ||
assert ActiveRecord::Base.connected_to?(role: :writing, shard: :shard_one) | ||
[200, {}, ["body"]] | ||
}, ->(*) { :shard_one }) | ||
|
||
assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET") | ||
end | ||
|
||
def test_middleware_can_handle_string_shards | ||
middleware = ActiveRecord::Middleware::ShardSelector.new(lambda { |env| | ||
assert ActiveRecord::Base.connected_to?(role: :writing, shard: :shard_one) | ||
[200, {}, ["body"]] | ||
}, ->(*) { "shard_one" }) | ||
|
||
assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET") | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters