Skip to content

Commit a1c0173

Browse files
committed
Move the database version cache from schema cache to pool config
Ref: rails#49378 As discussed with Matthew Draper, we have a bit of a chicken and egg problem with the schema cache and the database version. The database version is stored in the cache to avoid a query, but the schema cache need to query the schema version in the database to be revalidated. So `check_version` depends on `schema_cache`, which depends on `Migrator.current_version`, which depends on `configure_connection` which depends on `check_version`. But ultimately, we think storing the server version in the cache is incorrect, because upgrading a DB server is orthogonal from regenerating the schema cache. So not persisting the version in cache is better. Instead we store it in the pool config, so that we only check it once per process and per database.
1 parent 6c9967f commit a1c0173

File tree

9 files changed

+39
-79
lines changed

9 files changed

+39
-79
lines changed

activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ def method_missing(*)
2222
end
2323
NULL_CONFIG = NullConfig.new # :nodoc:
2424

25+
def initialize
26+
super()
27+
@mutex = Mutex.new
28+
@server_version = nil
29+
end
30+
31+
def server_version(connection) # :nodoc:
32+
@server_version || @mutex.synchronize { @server_version ||= connection.get_database_version }
33+
end
34+
2535
def schema_reflection
2636
SchemaReflection.new(nil)
2737
end
@@ -111,7 +121,7 @@ class ConnectionPool
111121
attr_accessor :automatic_reconnect, :checkout_timeout
112122
attr_reader :db_config, :size, :reaper, :pool_config, :async_executor, :role, :shard
113123

114-
delegate :schema_reflection, :schema_reflection=, to: :pool_config
124+
delegate :schema_reflection, :schema_reflection=, :server_version, to: :pool_config
115125

116126
# Creates a new ConnectionPool object. +pool_config+ is a PoolConfig
117127
# object which describes database connection information (e.g. adapter,

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ def get_database_version # :nodoc:
868868
end
869869

870870
def database_version # :nodoc:
871-
schema_cache.database_version
871+
pool.server_version(self)
872872
end
873873

874874
def check_version # :nodoc:

activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def configure_connection
174174
end
175175

176176
def full_version
177-
schema_cache.database_version.full_version_string
177+
database_version.full_version_string
178178
end
179179

180180
def get_full_version

activerecord/lib/active_record/connection_adapters/pool_config.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
module ActiveRecord
44
module ConnectionAdapters
55
class PoolConfig # :nodoc:
6-
include Mutex_m
6+
include MonitorMixin
77

88
attr_reader :db_config, :role, :shard
9-
attr_writer :schema_reflection
9+
attr_writer :schema_reflection, :server_version
1010
attr_accessor :connection_class
1111

1212
def schema_reflection
@@ -28,6 +28,7 @@ def disconnect_all!
2828

2929
def initialize(connection_class, db_config, role, shard)
3030
super()
31+
@server_version = nil
3132
@connection_class = connection_class
3233
@db_config = db_config
3334
@role = role
@@ -36,6 +37,10 @@ def initialize(connection_class, db_config, role, shard)
3637
INSTANCES[self] = self
3738
end
3839

40+
def server_version(connection)
41+
@server_version || synchronize { @server_version ||= connection.get_database_version }
42+
end
43+
3944
def connection_name
4045
if connection_class.primary_class?
4146
"ActiveRecord::Base"

activerecord/lib/active_record/connection_adapters/schema_cache.rb

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ def indexes(connection, table_name)
6666
cache(connection).indexes(connection, table_name)
6767
end
6868

69-
def database_version(connection)
70-
cache(connection).database_version(connection)
71-
end
72-
7369
def version(connection)
7470
cache(connection).version(connection)
7571
end
@@ -196,10 +192,6 @@ def indexes(table_name)
196192
@schema_reflection.indexes(@connection, table_name)
197193
end
198194

199-
def database_version
200-
@schema_reflection.database_version(@connection)
201-
end
202-
203195
def version
204196
@schema_reflection.version(@connection)
205197
end
@@ -264,7 +256,6 @@ def initialize
264256
@primary_keys = {}
265257
@data_sources = {}
266258
@indexes = {}
267-
@database_version = nil
268259
@version = nil
269260
end
270261

@@ -283,7 +274,6 @@ def encode_with(coder) # :nodoc:
283274
coder["data_sources"] = @data_sources.sort.to_h
284275
coder["indexes"] = @indexes.sort.to_h
285276
coder["version"] = @version
286-
coder["database_version"] = @database_version
287277
end
288278

289279
def init_with(coder)
@@ -293,7 +283,6 @@ def init_with(coder)
293283
@data_sources = coder["data_sources"]
294284
@indexes = coder["indexes"] || {}
295285
@version = coder["version"]
296-
@database_version = coder["database_version"]
297286

298287
unless coder["deduplicated"]
299288
derive_columns_hash_and_deduplicate_values
@@ -370,10 +359,6 @@ def indexes(connection, table_name)
370359
end
371360
end
372361

373-
def database_version(connection) # :nodoc:
374-
@database_version ||= connection.get_database_version
375-
end
376-
377362
def version(connection)
378363
@version ||= connection.schema_version
379364
end
@@ -401,7 +386,6 @@ def add_all(connection) # :nodoc:
401386
end
402387

403388
version(connection)
404-
database_version(connection)
405389
end
406390

407391
def dump_to(filename)
@@ -415,11 +399,11 @@ def dump_to(filename)
415399
end
416400

417401
def marshal_dump # :nodoc:
418-
[@version, @columns, {}, @primary_keys, @data_sources, @indexes, @database_version]
402+
[@version, @columns, {}, @primary_keys, @data_sources, @indexes]
419403
end
420404

421405
def marshal_load(array) # :nodoc:
422-
@version, @columns, _columns_hash, @primary_keys, @data_sources, @indexes, @database_version = array
406+
@version, @columns, _columns_hash, @primary_keys, @data_sources, @indexes, _database_version = array
423407
@indexes ||= {}
424408

425409
derive_columns_hash_and_deduplicate_values

activerecord/lib/active_record/connection_adapters/trilogy_adapter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def reconnect
202202
end
203203

204204
def full_version
205-
schema_cache.database_version.full_version_string
205+
database_version.full_version_string
206206
end
207207

208208
def get_full_version

activerecord/lib/active_record/railtie.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,13 @@ class Railtie < Rails::Railtie # :nodoc:
162162
warn "Failed to validate the schema cache because of #{error.class}: #{error.message}"
163163
nil
164164
end
165-
next if current_version.nil?
166165

167-
if cache.schema_version != current_version
166+
if current_version.nil?
167+
connection_pool.schema_reflection.clear!
168+
next
169+
elsif cache.schema_version != current_version
168170
warn "Ignoring #{filename} because it has expired. The current schema version is #{current_version}, but the one in the schema cache file is #{cache.schema_version}."
171+
connection_pool.schema_reflection.clear!
169172
next
170173
end
171174
end

activerecord/test/cases/adapters/abstract_mysql_adapter/datetime_precision_quoting_test.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,10 @@ def assert_match_quoted_microsecond_datetime(match)
3838
assert_match match, @connection.quoted_date(Time.now.change(sec: 55, usec: 123456))
3939
end
4040

41-
def stub_version(full_version_string)
42-
@connection.stub(:get_full_version, full_version_string) do
43-
@connection.schema_cache.clear!
44-
yield
45-
end
41+
def stub_version(full_version_string, &block)
42+
@connection.pool.pool_config.server_version = nil
43+
@connection.stub(:get_full_version, full_version_string, &block)
4644
ensure
47-
@connection.schema_cache.clear!
45+
@connection.pool.pool_config.server_version = nil
4846
end
4947
end

activerecord/test/cases/connection_adapters/schema_cache_test.rb

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ module ActiveRecord
66
module ConnectionAdapters
77
class SchemaCacheTest < ActiveRecord::TestCase
88
def setup
9-
@connection = ARUnit2Model.connection
10-
@cache = new_bound_reflection
11-
@database_version = @connection.get_database_version
9+
@connection = ARUnit2Model.connection
10+
@cache = new_bound_reflection
1211
@check_schema_cache_dump_version_was = SchemaReflection.check_schema_cache_dump_version
1312
end
1413

@@ -67,7 +66,6 @@ def test_yaml_dump_and_load
6766
assert cache.data_source_exists?("courses")
6867
assert_equal "id", cache.primary_keys("courses")
6968
assert_equal 1, cache.indexes("courses").size
70-
assert_equal @database_version.to_s, cache.database_version.to_s
7169
end
7270
ensure
7371
tempfile.unlink
@@ -104,7 +102,6 @@ def test_yaml_dump_and_load_with_gzip
104102
assert cache.data_source_exists?(@connection, "courses")
105103
assert_equal "id", cache.primary_keys(@connection, "courses")
106104
assert_equal 1, cache.indexes(@connection, "courses").size
107-
assert_equal @database_version.to_s, cache.database_version(@connection).to_s
108105
end
109106

110107
# Load the cache the usual way.
@@ -116,7 +113,6 @@ def test_yaml_dump_and_load_with_gzip
116113
assert cache.data_source_exists?("courses")
117114
assert_equal "id", cache.primary_keys("courses")
118115
assert_equal 1, cache.indexes("courses").size
119-
assert_equal @database_version.to_s, cache.database_version.to_s
120116
end
121117
ensure
122118
tempfile.unlink
@@ -141,18 +137,6 @@ def test_yaml_loads_5_1_dump_without_indexes_still_queries_for_indexes
141137
end
142138
end
143139

144-
def test_yaml_loads_5_1_dump_without_database_version_still_queries_for_database_version
145-
cache = load_bound_reflection(schema_dump_path)
146-
147-
# We can't verify queries get executed because the database version gets
148-
# cached in both MySQL and PostgreSQL outside of the schema cache.
149-
150-
assert_not_nil reflection = @cache.instance_variable_get(:@schema_reflection)
151-
assert_nil reflection.instance_variable_get(:@cache)
152-
153-
assert_equal @database_version.to_s, cache.database_version.to_s
154-
end
155-
156140
def test_primary_key_for_existent_table
157141
assert_equal "id", @cache.primary_keys("courses")
158142
end
@@ -189,18 +173,6 @@ def test_indexes_for_non_existent_table
189173
assert_equal [], @cache.indexes("omgponies")
190174
end
191175

192-
def test_caches_database_version
193-
@cache.database_version # cache database_version
194-
195-
assert_no_queries do
196-
assert_equal @database_version.to_s, @cache.database_version.to_s
197-
198-
if current_adapter?(:Mysql2Adapter, :TrilogyAdapter)
199-
assert_not_nil @cache.database_version.full_version_string
200-
end
201-
end
202-
end
203-
204176
def test_clearing
205177
@cache.columns("courses")
206178
@cache.columns_hash("courses")
@@ -211,9 +183,6 @@ def test_clearing
211183
@cache.clear!
212184

213185
assert_equal 0, @cache.size
214-
reflection = @cache.instance_variable_get(:@schema_reflection)
215-
schema_cache = reflection.instance_variable_get(:@cache)
216-
assert_nil schema_cache.instance_variable_get(:@database_version)
217186
end
218187

219188
def test_marshal_dump_and_load
@@ -223,10 +192,6 @@ def test_marshal_dump_and_load
223192
# Populate it.
224193
cache.add("courses")
225194

226-
# We're going to manually dump, so we also need to force
227-
# database_version to be stored.
228-
cache.database_version
229-
230195
# Create a new cache by marshal dumping / loading.
231196
cache = Marshal.load(Marshal.dump(cache.instance_variable_get(:@schema_reflection).instance_variable_get(:@cache)))
232197

@@ -236,7 +201,6 @@ def test_marshal_dump_and_load
236201
assert cache.data_source_exists?(@connection, "courses")
237202
assert_equal "id", cache.primary_keys(@connection, "courses")
238203
assert_equal 1, cache.indexes(@connection, "courses").size
239-
assert_equal @database_version.to_s, cache.database_version(@connection).to_s
240204
end
241205
end
242206

@@ -257,7 +221,6 @@ def test_marshal_dump_and_load_via_disk
257221
assert cache.data_source_exists?("courses")
258222
assert_equal "id", cache.primary_keys("courses")
259223
assert_equal 1, cache.indexes("courses").size
260-
assert_equal @database_version.to_s, cache.database_version.to_s
261224
end
262225
ensure
263226
tempfile.unlink
@@ -316,7 +279,6 @@ def test_marshal_dump_and_load_with_gzip
316279
assert cache.data_source_exists?(@connection, "courses")
317280
assert_equal "id", cache.primary_keys(@connection, "courses")
318281
assert_equal 1, cache.indexes(@connection, "courses").size
319-
assert_equal @database_version.to_s, cache.database_version(@connection).to_s
320282
end
321283

322284
# Load a new cache.
@@ -328,7 +290,6 @@ def test_marshal_dump_and_load_with_gzip
328290
assert cache.data_source_exists?("courses")
329291
assert_equal "id", cache.primary_keys("courses")
330292
assert_equal 1, cache.indexes("courses").size
331-
assert_equal @database_version.to_s, cache.database_version.to_s
332293
end
333294
ensure
334295
tempfile.unlink
@@ -389,28 +350,28 @@ def test_when_lazily_load_schema_cache_is_set_cache_is_lazily_populated_when_est
389350
ActiveRecord::Base.establish_connection(new_config)
390351

391352
# cache starts empty
392-
assert_equal 0, ActiveRecord::Base.connection.pool.schema_reflection.instance_variable_get(:@cache).size
353+
assert_nil ActiveRecord::Base.connection.pool.schema_reflection.instance_variable_get(:@cache)
393354

394355
# now we access the cache, causing it to load
395-
assert ActiveRecord::Base.connection.schema_cache.version
356+
assert_not_nil ActiveRecord::Base.connection.schema_cache.version
396357

397358
assert File.exist?(tempfile)
398-
assert ActiveRecord::Base.connection.pool.schema_reflection.instance_variable_get(:@cache)
359+
assert_not_nil ActiveRecord::Base.connection.pool.schema_reflection.instance_variable_get(:@cache)
399360

400361
# assert cache is still empty on new connection (precondition for the
401362
# following to show it is loading because of the config change)
402363
ActiveRecord::Base.establish_connection(new_config)
403364

404365
assert File.exist?(tempfile)
405-
assert_equal 0, ActiveRecord::Base.connection.pool.schema_reflection.instance_variable_get(:@cache).size
366+
assert_nil ActiveRecord::Base.connection.pool.schema_reflection.instance_variable_get(:@cache)
406367

407368
# cache is loaded upon connection when lazily loading is on
408369
old_config = ActiveRecord.lazily_load_schema_cache
409370
ActiveRecord.lazily_load_schema_cache = true
410371
ActiveRecord::Base.establish_connection(new_config)
411372

412373
assert File.exist?(tempfile)
413-
assert ActiveRecord::Base.connection.pool.schema_reflection.instance_variable_get(:@cache)
374+
assert_not_nil ActiveRecord::Base.connection.pool.schema_reflection.instance_variable_get(:@cache)
414375
ensure
415376
ActiveRecord.lazily_load_schema_cache = old_config
416377
ActiveRecord::Base.establish_connection(:arunit)
@@ -449,7 +410,6 @@ def test_when_lazily_load_schema_cache_is_set_cache_is_lazily_populated_when_est
449410
assert_equal expected, coder["data_sources"]
450411
assert_equal expected, coder["indexes"]
451412
assert coder.key?("version")
452-
assert coder.key?("database_version")
453413
end
454414

455415
private

0 commit comments

Comments
 (0)