mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
7fc174aada
Sometimes a controller or a job has to perform multiple independent queries, e.g.: ``` def index @posts = Post.published @categories = Category.active end ``` Since these two queries are totally independent, ideally you could execute them in parallel, so that assuming that each take 50ms, the total query time would be 50ms rather than 100ms. A very naive way to do this is to simply call `Relation#to_a` in a background thread, the problem is that most Rails applications, and even Rails itself rely on thread local state (`PerThreadRegistry`, `CurrentAttributes`, etc). So executing such a high level interface from another thread is likely to lead to many context loss problems or even thread safety issues. What we can do instead, is to schedule a much lower level operation (`Adapter#select_all`) in a thread pool, and return a future/promise. This way we kepp most of the risky code on the main thread, but perform the slow IO in background, with very little chance of executing some code that rely on state stored in thread local storage. Also since most users are on MRI, only the IO can really be parallelized, so scheduling more code to be executed in background wouldn't lead to better performance.
280 lines
10 KiB
Ruby
280 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "active_record"
|
|
require "rails"
|
|
require "active_support/core_ext/object/try"
|
|
require "active_model/railtie"
|
|
|
|
# For now, action_controller must always be present with
|
|
# Rails, so let's make sure that it gets required before
|
|
# here. This is needed for correctly setting up the middleware.
|
|
# In the future, this might become an optional require.
|
|
require "action_controller/railtie"
|
|
|
|
module ActiveRecord
|
|
# = Active Record Railtie
|
|
class Railtie < Rails::Railtie # :nodoc:
|
|
config.active_record = ActiveSupport::OrderedOptions.new
|
|
|
|
config.app_generators.orm :active_record, migration: true,
|
|
timestamps: true
|
|
|
|
config.action_dispatch.rescue_responses.merge!(
|
|
"ActiveRecord::RecordNotFound" => :not_found,
|
|
"ActiveRecord::StaleObjectError" => :conflict,
|
|
"ActiveRecord::RecordInvalid" => :unprocessable_entity,
|
|
"ActiveRecord::RecordNotSaved" => :unprocessable_entity
|
|
)
|
|
|
|
config.active_record.use_schema_cache_dump = true
|
|
config.active_record.check_schema_cache_dump_version = true
|
|
config.active_record.maintain_test_schema = true
|
|
config.active_record.has_many_inversing = false
|
|
|
|
config.active_record.queues = ActiveSupport::InheritableOptions.new
|
|
|
|
config.eager_load_namespaces << ActiveRecord
|
|
|
|
rake_tasks do
|
|
namespace :db do
|
|
task :load_config do
|
|
if defined?(ENGINE_ROOT) && engine = Rails::Engine.find(ENGINE_ROOT)
|
|
if engine.paths["db/migrate"].existent
|
|
ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths["db/migrate"].to_a
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
load "active_record/railties/databases.rake"
|
|
end
|
|
|
|
# When loading console, force ActiveRecord::Base to be loaded
|
|
# to avoid cross references when loading a constant for the
|
|
# first time. Also, make it output to STDERR.
|
|
console do |app|
|
|
require "active_record/railties/console_sandbox" if app.sandbox?
|
|
require "active_record/base"
|
|
unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
|
|
console = ActiveSupport::Logger.new(STDERR)
|
|
console.level = Rails.logger.level
|
|
Rails.logger.extend ActiveSupport::Logger.broadcast console
|
|
end
|
|
ActiveRecord::Base.verbose_query_logs = false
|
|
end
|
|
|
|
runner do
|
|
require "active_record/base"
|
|
end
|
|
|
|
initializer "active_record.initialize_timezone" do
|
|
ActiveSupport.on_load(:active_record) do
|
|
self.time_zone_aware_attributes = true
|
|
self.default_timezone = :utc
|
|
end
|
|
end
|
|
|
|
initializer "active_record.logger" do
|
|
ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
|
|
end
|
|
|
|
initializer "active_record.backtrace_cleaner" do
|
|
ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner }
|
|
end
|
|
|
|
initializer "active_record.migration_error" do |app|
|
|
if config.active_record.delete(:migration_error) == :page_load
|
|
config.app_middleware.insert_after ::ActionDispatch::Callbacks,
|
|
ActiveRecord::Migration::CheckPending,
|
|
file_watcher: app.config.file_watcher
|
|
end
|
|
end
|
|
|
|
initializer "active_record.database_selector" do
|
|
if options = config.active_record.delete(:database_selector)
|
|
resolver = config.active_record.delete(:database_resolver)
|
|
operations = config.active_record.delete(:database_resolver_context)
|
|
config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options
|
|
end
|
|
end
|
|
|
|
initializer "Check for cache versioning support" do
|
|
config.after_initialize do |app|
|
|
ActiveSupport.on_load(:active_record) do
|
|
if app.config.active_record.cache_versioning && Rails.cache
|
|
unless Rails.cache.class.try(:supports_cache_versioning?)
|
|
raise <<-end_error
|
|
|
|
You're using a cache store that doesn't support native cache versioning.
|
|
Your best option is to upgrade to a newer version of #{Rails.cache.class}
|
|
that supports cache versioning (#{Rails.cache.class}.supports_cache_versioning? #=> true).
|
|
|
|
Next best, switch to a different cache store that does support cache versioning:
|
|
https://guides.rubyonrails.org/caching_with_rails.html#cache-stores.
|
|
|
|
To keep using the current cache store, you can turn off cache versioning entirely:
|
|
|
|
config.active_record.cache_versioning = false
|
|
|
|
end_error
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
initializer "active_record.check_schema_cache_dump" do
|
|
check_schema_cache_dump_version = config.active_record.delete(:check_schema_cache_dump_version)
|
|
|
|
if config.active_record.delete(:use_schema_cache_dump)
|
|
config.after_initialize do |app|
|
|
ActiveSupport.on_load(:active_record) do
|
|
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first
|
|
|
|
filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(
|
|
db_config.name,
|
|
schema_cache_path: db_config&.schema_cache_path
|
|
)
|
|
|
|
cache = ActiveRecord::ConnectionAdapters::SchemaCache.load_from(filename)
|
|
next if cache.nil?
|
|
|
|
if check_schema_cache_dump_version
|
|
current_version = begin
|
|
ActiveRecord::Migrator.current_version
|
|
rescue ActiveRecordError => error
|
|
warn "Failed to validate the schema cache because of #{error.class}: #{error.message}"
|
|
nil
|
|
end
|
|
next if current_version.nil?
|
|
|
|
if cache.version != current_version
|
|
warn "Ignoring #{filename} because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}."
|
|
next
|
|
end
|
|
end
|
|
|
|
connection_pool.set_schema_cache(cache)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
initializer "active_record.define_attribute_methods" do |app|
|
|
config.after_initialize do
|
|
ActiveSupport.on_load(:active_record) do
|
|
if app.config.eager_load
|
|
begin
|
|
descendants.each do |model|
|
|
# If the schema cache was loaded from a dump, we can use it without connecting
|
|
schema_cache = model.connection_pool.schema_cache
|
|
|
|
# If there's no connection yet, we avoid connecting.
|
|
schema_cache ||= model.connected? && model.connection.schema_cache
|
|
|
|
# If the schema cache doesn't have the columns
|
|
# hash for the model cached, `define_attribute_methods` would trigger a query.
|
|
if schema_cache && schema_cache.columns_hash?(model.table_name)
|
|
model.define_attribute_methods
|
|
end
|
|
end
|
|
rescue ActiveRecordError => error
|
|
# Regardless of whether there was already a connection or not, we rescue any database
|
|
# error because it is critical that the application can boot even if the database
|
|
# is unhealthy.
|
|
warn "Failed to define attribute methods because of #{error.class}: #{error.message}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
initializer "active_record.warn_on_records_fetched_greater_than" do
|
|
if config.active_record.warn_on_records_fetched_greater_than
|
|
ActiveSupport.on_load(:active_record) do
|
|
require "active_record/relation/record_fetch_warning"
|
|
end
|
|
end
|
|
end
|
|
|
|
initializer "active_record.set_configs" do |app|
|
|
ActiveSupport.on_load(:active_record) do
|
|
configs = app.config.active_record
|
|
|
|
configs.each do |k, v|
|
|
send "#{k}=", v
|
|
end
|
|
end
|
|
end
|
|
|
|
# This sets the database configuration from Configuration#database_configuration
|
|
# and then establishes the connection.
|
|
initializer "active_record.initialize_database" do
|
|
ActiveSupport.on_load(:active_record) do
|
|
if ActiveRecord::Base.legacy_connection_handling
|
|
self.connection_handlers = { writing_role => ActiveRecord::Base.default_connection_handler }
|
|
end
|
|
self.configurations = Rails.application.config.database_configuration
|
|
establish_connection
|
|
end
|
|
end
|
|
|
|
# Expose database runtime to controller for logging.
|
|
initializer "active_record.log_runtime" do
|
|
require "active_record/railties/controller_runtime"
|
|
ActiveSupport.on_load(:action_controller) do
|
|
include ActiveRecord::Railties::ControllerRuntime
|
|
end
|
|
end
|
|
|
|
initializer "active_record.set_reloader_hooks" do
|
|
ActiveSupport.on_load(:active_record) do
|
|
ActiveSupport::Reloader.before_class_unload do
|
|
if ActiveRecord::Base.connected?
|
|
ActiveRecord::Base.clear_cache!
|
|
ActiveRecord::Base.clear_reloadable_connections!
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
initializer "active_record.set_executor_hooks" do
|
|
ActiveRecord::QueryCache.install_executor_hooks
|
|
ActiveRecord::AsynchronousQueriesTracker.install_executor_hooks
|
|
end
|
|
|
|
initializer "active_record.add_watchable_files" do |app|
|
|
path = app.paths["db"].first
|
|
config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"]
|
|
end
|
|
|
|
initializer "active_record.clear_active_connections" do
|
|
config.after_initialize do
|
|
ActiveSupport.on_load(:active_record) do
|
|
# Ideally the application doesn't connect to the database during boot,
|
|
# but sometimes it does. In case it did, we want to empty out the
|
|
# connection pools so that a non-database-using process (e.g. a master
|
|
# process in a forking server model) doesn't retain a needless
|
|
# connection. If it was needed, the incremental cost of reestablishing
|
|
# this connection is trivial: the rest of the pool would need to be
|
|
# populated anyway.
|
|
|
|
clear_active_connections!
|
|
flush_idle_connections!
|
|
end
|
|
end
|
|
end
|
|
|
|
initializer "active_record.set_filter_attributes" do
|
|
ActiveSupport.on_load(:active_record) do
|
|
self.filter_attributes += Rails.application.config.filter_parameters
|
|
end
|
|
end
|
|
|
|
initializer "active_record.set_signed_id_verifier_secret" do
|
|
ActiveSupport.on_load(:active_record) do
|
|
self.signed_id_verifier_secret ||= -> { Rails.application.key_generator.generate_key("active_record/signed_id") }
|
|
end
|
|
end
|
|
end
|
|
end
|