From 770e12fddc278d4d72ed311d3b7e1cc685e430c0 Mon Sep 17 00:00:00 2001 From: eileencodes Date: Tue, 7 Dec 2021 17:01:38 -0500 Subject: [PATCH] Move multi-db config options to middleware This doesn't really make sense in the production config especially since you probably want to use it in all of your environments. This change moves the database and shard swapping configuration options into a generator. The generator can be run like this: ``` bin/rails g active_record:automatic_swapping ``` This change allows apps to add additional configuration for multiple databases all in one place. The config options can still be defined in the environment config if desired but this cleans up the default config for new applications especially since new applications probably don't need multiple databases. --- activerecord/CHANGELOG.md | 4 ++ .../multi_db/multi_db_generator.rb | 16 +++++++ .../multi_db/templates/multi_db.rb.tt | 44 +++++++++++++++++++ .../active_record_multiple_databases.md | 36 +++++++++++---- .../config/environments/production.rb.tt | 31 ------------- .../generators/multi_db_generator_test.rb | 18 ++++++++ 6 files changed, 109 insertions(+), 40 deletions(-) create mode 100644 activerecord/lib/rails/generators/active_record/multi_db/multi_db_generator.rb create mode 100644 activerecord/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt create mode 100644 railties/test/generators/multi_db_generator_test.rb diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 0a6375be0d..ba933cf09b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,7 @@ +* Move database and shard selection config options to a generator. + Rather than generating the config options in the production.rb when applications are created, applications can now run a generator to create an initializer and uncomment / update options as needed. All multi-db configuration can be imlpemented in this initializer. + + *Eileen M. Uchitelle* Please check [7-0-stable](https://github.com/rails/rails/blob/7-0-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/lib/rails/generators/active_record/multi_db/multi_db_generator.rb b/activerecord/lib/rails/generators/active_record/multi_db/multi_db_generator.rb new file mode 100644 index 0000000000..32a11b4d9c --- /dev/null +++ b/activerecord/lib/rails/generators/active_record/multi_db/multi_db_generator.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "rails/generators/active_record" + +module ActiveRecord + module Generators # :nodoc: + class MultiDbGenerator < ::Rails::Generators::Base # :nodoc: + source_root File.expand_path("templates", __dir__) + + def create_multi_db + filename = "multi_db.rb" + template filename, "config/initializers/#{filename}" + end + end + end +end diff --git a/activerecord/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt b/activerecord/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt new file mode 100644 index 0000000000..6016d955b7 --- /dev/null +++ b/activerecord/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt @@ -0,0 +1,44 @@ +# Multi-db Configuration +# +# This file is used for configuration settings related to multiple databases. +# +# Enable Database Selector +# +# Inserts middleware to perform automatic connection switching. +# The `database_selector` hash is used to pass options to the DatabaseSelector +# middleware. The `delay` is used to determine how long to wait after a write +# to send a subsequent read to the primary. +# +# The `database_resolver` class is used by the middleware to determine which +# database is appropriate to use based on the time delay. +# +# The `database_resolver_context` class is used by the middleware to set +# timestamps for the last write to the primary. The resolver uses the context +# class timestamps to determine how long to wait before reading from the +# replica. +# +# By default Rails will store a last write timestamp in the session. The +# DatabaseSelector middleware is designed as such you can define your own +# strategy for connection switching and pass that into the middleware through +# these configuration options. +# +# Rails.application.configure do +# config.active_record.database_selector = { delay: 2.seconds } +# config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver +# config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session +# end +# +# Enable Shard Selector +# +# Inserts middleware to perform automatic shard swapping. The `shard_selector` hash +# can be used to pass options to the `ShardSelector` middleware. The `lock` option is +# used to determine whether shard swapping should be prohibited for the request. +# +# The `shard_resolver` option is used by the middleware to determine which shard +# to switch to. The application must provide a mechanism for finding the shard name +# in a proc. See guides for an example. +# +# Rails.application.configure do +# config.active_record.shard_selector = { lock: true } +# config.active_record.shard_resolver = ->(request) { Tenant.find_by!(host: request.host).shard } +# end diff --git a/guides/source/active_record_multiple_databases.md b/guides/source/active_record_multiple_databases.md index a59cd171d1..b03530a4ee 100644 --- a/guides/source/active_record_multiple_databases.md +++ b/guides/source/active_record_multiple_databases.md @@ -288,13 +288,21 @@ automatically write to the writer database. For the specified time after the wri application will read from the primary. For a GET or HEAD request the application will read from the replica unless there was a recent write. -To activate the automatic connection switching middleware, add or uncomment the following -lines in your application config. +To activate the automatic connection switching middleware you can run the automatic swapping +generator: + +``` +$ bin/rails g active_record:multi_db +``` + +And then uncomment the following lines: ```ruby -config.active_record.database_selector = { delay: 2.seconds } -config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver -config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session +Rails.application.configure do + config.active_record.database_selector = { delay: 2.seconds } + config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session +end ``` Rails guarantees "read your own write" and will send your GET or HEAD request to the @@ -442,13 +450,23 @@ 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: +The same generator as the database selector can be used to generate the file for +automatic shard swapping: -```ruby -config.active_record.shard_selector = { lock: true } +``` +$ bin/rails g active_record:multi_db ``` -Applications must also provide the code for the resolver as it depends on application +Then in the file uncomment the following: + +```ruby +Rails.application.configure do + config.active_record.shard_selector = { lock: true } + config.active_record.shard_resolver = ->(request) { Tenant.find_by!(host: request.host).shard } +end +``` + +Applications must provide the code for the resolver as it depends on application specific models. An example resolver would look like this: ```ruby diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 96a67d174f..0fb7d0976e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -104,35 +104,4 @@ Rails.application.configure do # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false <%- end -%> - - # Inserts middleware to perform automatic connection switching. - # The `database_selector` hash is used to pass options to the DatabaseSelector - # middleware. The `delay` is used to determine how long to wait after a write - # to send a subsequent read to the primary. - # - # The `database_resolver` class is used by the middleware to determine which - # database is appropriate to use based on the time delay. - # - # The `database_resolver_context` class is used by the middleware to set - # timestamps for the last write to the primary. The resolver uses the context - # class timestamps to determine how long to wait before reading from the - # replica. - # - # By default Rails will store a last write timestamp in the session. The - # DatabaseSelector middleware is designed as such you can define your own - # strategy for connection switching and pass that into the middleware through - # these configuration options. - # config.active_record.database_selector = { delay: 2.seconds } - # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver - # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session - - # Inserts middleware to perform automatic shard swapping. The `shard_selector` hash - # can be used to pass options to the `ShardSelector` middleware. The `lock` option is - # used to determine whether shard swapping should be prohibited for the request. - # - # The `shard_resolver` option is used by the middleware to determine which shard - # to switch to. The application must provide a mechanism for finding the shard name - # in a proc. See guides for an example. - # config.active_record.shard_selector = { lock: true } - # config.active_record.shard_resolver = ->(request) { Tenant.find_by!(host: request.host).shard } end diff --git a/railties/test/generators/multi_db_generator_test.rb b/railties/test/generators/multi_db_generator_test.rb new file mode 100644 index 0000000000..4b279b9b4f --- /dev/null +++ b/railties/test/generators/multi_db_generator_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/active_record/multi_db/multi_db_generator" + +class MultiDbGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + tests ActiveRecord::Generators::MultiDbGenerator + + def test_multi_db_skeleton_is_created + run_generator + assert_file "config/initializers/multi_db.rb" do |record| + assert_match(/Multi-db Configuration/, record) + assert_match(/config.active_record.database_resolver/, record) + assert_match(/config.active_record.shard_resolver/, record) + end + end +end