Permalink
Browse files

Merge pull request #5162 from kennyj/schema_cache_dump

[Proposal] Schema cache dump
  • Loading branch information...
2 parents b700153 + 46c1217 commit 447ecb08ca1bab594198282237c4e9a027f7a3f4 @tenderlove tenderlove committed Mar 8, 2012
View
@@ -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
@exviva

exviva Mar 8, 2012

Contributor

I think you meant db/schema_cache.dump, right?

@kennyj

kennyj Mar 8, 2012

Contributor

Oops, You are right. I'll fix it.

+
+ 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
@@ -86,6 +86,11 @@ def lease
end
end
+ def schema_cache=(cache)
+ cache.connection = self
+ @schema_cache = cache
+ end
+
def expire
@in_use = false
end
@@ -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 @@ def table_exists?(name)
@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 @@ def clear_table_cache!(table_name)
@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
@@ -107,14 +107,29 @@ class Railtie < Rails::Railtie
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
ActionDispatch::Reloader.to_prepare do
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
@@ -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
@@ -39,6 +39,21 @@ def test_clearing
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
@@ -11,7 +11,7 @@ class Configuration < ::Rails::Engine::Configuration
: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 @@ def initialize(*)
@file_watcher = ActiveSupport::FileUpdateChecker
@exceptions_app = nil
@autoflush_log = true
+ @use_schema_cache_dump = true
@assets = ActiveSupport::OrderedOptions.new
@assets.enabled = false
@@ -193,5 +193,31 @@ def from_bar_helper
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
@@ -138,5 +138,24 @@ def test_rake_dump_structure_should_respect_db_structure_env_variable
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

0 comments on commit 447ecb0

Please sign in to comment.