Merge pull request #5162 from kennyj/schema_cache_dump

[Proposal] Schema cache dump
This commit is contained in:
Aaron Patterson 2012-03-07 16:44:50 -08:00
commit 447ecb08ca
9 changed files with 173 additions and 18 deletions

View File

@ -1,5 +1,28 @@
## Rails 4.0.0 (unreleased) ##
* Added the schema cache dump feature.
`Schema cache dump` feature was implemetend. This feature can dump/load internal state of `SchemaCache` instance
because we want to boot rails more quickly when we have many models.
Usage notes:
1) execute rake task.
RAILS_ENV=production bundle exec rake db:schema:cache:dump
=> generate db/schema_cache.dump
2) add config.use_schema_cache_dump = true in config/production.rb. BTW, true is default.
3) boot rails.
RAILS_ENV=production bundle exec rails server
=> use db/schema_cache.db
4) If you remove clear dumped cache, execute rake task.
RAILS_ENV=production bundle exec rake db:schema:cache:clear
=> remove db/schema_cache.dump
*kennyj*
* Added support for partial indices to PostgreSQL adapter
The `add_index` method now supports a `where` option that receives a

View File

@ -86,6 +86,11 @@ module ActiveRecord
end
end
def schema_cache=(cache)
cache.connection = self
@schema_cache = cache
end
def expire
@in_use = false
end

View File

@ -1,26 +1,17 @@
module ActiveRecord
module ConnectionAdapters
class SchemaCache
attr_reader :columns, :columns_hash, :primary_keys, :tables
attr_reader :connection
attr_reader :columns, :columns_hash, :primary_keys, :tables, :version
attr_accessor :connection
def initialize(conn)
@connection = conn
@tables = {}
@columns = Hash.new do |h, table_name|
h[table_name] = conn.columns(table_name)
end
@columns_hash = Hash.new do |h, table_name|
h[table_name] = Hash[columns[table_name].map { |col|
[col.name, col]
}]
end
@primary_keys = Hash.new do |h, table_name|
h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil
end
@columns = {}
@columns_hash = {}
@primary_keys = {}
@tables = {}
prepare_default_proc
end
# A cached lookup for table existence.
@ -30,12 +21,22 @@ module ActiveRecord
@tables[name] = connection.table_exists?(name)
end
# Add internal cache for table with +table_name+.
def add(table_name)
if table_exists?(table_name)
@primary_keys[table_name]
@columns[table_name]
@columns_hash[table_name]
end
end
# Clears out internal caches
def clear!
@columns.clear
@columns_hash.clear
@primary_keys.clear
@tables.clear
@version = nil
end
# Clear out internal caches for table with +table_name+.
@ -45,6 +46,37 @@ module ActiveRecord
@primary_keys.delete table_name
@tables.delete table_name
end
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = ActiveRecord::Migrator.current_version
[@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val|
self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h }
end
end
def marshal_load(array)
@version, @columns, @columns_hash, @primary_keys, @tables = array
prepare_default_proc
end
private
def prepare_default_proc
@columns.default_proc = Proc.new do |h, table_name|
h[table_name] = connection.columns(table_name)
end
@columns_hash.default_proc = Proc.new do |h, table_name|
h[table_name] = Hash[columns[table_name].map { |col|
[col.name, col]
}]
end
@primary_keys.default_proc = Proc.new do |h, table_name|
h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil
end
end
end
end
end

View File

@ -107,7 +107,7 @@ module ActiveRecord
config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"]
end
config.after_initialize do
config.after_initialize do |app|
ActiveSupport.on_load(:active_record) do
ActiveRecord::Base.instantiate_observers
@ -115,6 +115,21 @@ module ActiveRecord
ActiveRecord::Base.instantiate_observers
end
end
ActiveSupport.on_load(:active_record) do
if app.config.use_schema_cache_dump
filename = File.join(app.config.paths["db"].first, "schema_cache.dump")
if File.file?(filename)
cache = Marshal.load(open(filename, 'rb') { |f| f.read })
if cache.version == ActiveRecord::Migrator.current_version
ActiveRecord::Base.connection.schema_cache = cache
else
warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}."
end
end
end
end
end
end
end

View File

@ -372,6 +372,25 @@ db_namespace = namespace :db do
task :load_if_ruby => 'db:create' do
db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby
end
namespace :cache do
desc 'Create a db/schema_cache.dump file.'
task :dump => :environment do
con = ActiveRecord::Base.connection
filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
con.schema_cache.clear!
con.tables.each { |table| con.schema_cache.add(table) }
open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) }
end
desc 'Clear a db/schema_cache.dump file.'
task :clear => :environment do
filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
FileUtils.rm(filename) if File.exists?(filename)
end
end
end
namespace :structure do

View File

@ -39,6 +39,21 @@ module ActiveRecord
assert_equal 0, @cache.tables.size
assert_equal 0, @cache.primary_keys.size
end
def test_dump_and_load
@cache.columns['posts']
@cache.columns_hash['posts']
@cache.tables['posts']
@cache.primary_keys['posts']
@cache = Marshal.load(Marshal.dump(@cache))
assert_equal 12, @cache.columns['posts'].size
assert_equal 12, @cache.columns_hash['posts'].size
assert @cache.tables['posts']
assert_equal 'id', @cache.primary_keys['posts']
end
end
end
end

View File

@ -11,7 +11,7 @@ module Rails
:force_ssl, :helpers_paths, :logger, :log_tags, :preload_frameworks,
:railties_order, :relative_url_root, :secret_token,
:serve_static_assets, :ssl_options, :static_cache_control, :session_options,
:time_zone, :reload_classes_only_on_change
:time_zone, :reload_classes_only_on_change, :use_schema_cache_dump
attr_writer :log_level
attr_reader :encoding
@ -41,6 +41,7 @@ module Rails
@file_watcher = ActiveSupport::FileUpdateChecker
@exceptions_app = nil
@autoflush_log = true
@use_schema_cache_dump = true
@assets = ActiveSupport::OrderedOptions.new
@assets.enabled = false

View File

@ -193,5 +193,31 @@ module ApplicationTests
require "#{app_path}/config/environment"
assert_nil defined?(ActiveRecord::Base)
end
test "use schema cache dump" do
Dir.chdir(app_path) do
`rails generate model post title:string`
`bundle exec rake db:migrate`
`bundle exec rake db:schema:cache:dump`
end
require "#{app_path}/config/environment"
ActiveRecord::Base.connection.drop_table("posts") # force drop posts table for test.
assert ActiveRecord::Base.connection.schema_cache.tables["posts"]
end
test "expire schema cache dump" do
Dir.chdir(app_path) do
`rails generate model post title:string`
`bundle exec rake db:migrate`
`bundle exec rake db:schema:cache:dump`
`bundle exec rake db:rollback`
end
silence_warnings {
require "#{app_path}/config/environment"
assert !ActiveRecord::Base.connection.schema_cache.tables["posts"]
}
end
end
end

View File

@ -138,5 +138,24 @@ module ApplicationTests
end
assert File.exists?(File.join(app_path, 'db', 'my_structure.sql'))
end
def test_rake_dump_schema_cache
Dir.chdir(app_path) do
`rails generate model post title:string`
`rails generate model product name:string`
`bundle exec rake db:migrate`
`bundle exec rake db:schema:cache:dump`
end
assert File.exists?(File.join(app_path, 'db', 'schema_cache.dump'))
end
def test_rake_clear_schema_cache
Dir.chdir(app_path) do
`bundle exec rake db:schema:cache:dump`
`bundle exec rake db:schema:cache:clear`
end
assert !File.exists?(File.join(app_path, 'db', 'schema_cache.dump'))
end
end
end