aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorSean Griffin <sean@seantheprogrammer.com>2018-11-27 18:51:20 -0700
committerGitHub <noreply@github.com>2018-11-27 18:51:20 -0700
commit9c4d3c341c0f09ea26a7f97a4a0af28f4e998d08 (patch)
tree06863c2ac038678e33d22bdab604f987f34f5bc4 /activerecord
parent129788d6ec68e9e0c14e4048a63688a91b0ee45d (diff)
parent73f9cd2da452c97061a7b01d3d8e9f480950fe80 (diff)
downloadrails-9c4d3c341c0f09ea26a7f97a4a0af28f4e998d08.tar.gz
rails-9c4d3c341c0f09ea26a7f97a4a0af28f4e998d08.tar.bz2
rails-9c4d3c341c0f09ea26a7f97a4a0af28f4e998d08.zip
Merge pull request #33835 from schneems/schneems/faster_cache_version
Use raw time string from DB to generate ActiveRecord#cache_version
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/lib/active_record/integration.rb52
-rw-r--r--activerecord/test/cases/cache_key_test.rb82
2 files changed, 130 insertions, 4 deletions
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 33c4066b89..ec39ece87f 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -96,8 +96,19 @@ module ActiveRecord
# Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
# +false+ (which it is by default until Rails 6.0).
def cache_version
- if cache_versioning && timestamp = try(:updated_at)
- timestamp.utc.to_s(:usec)
+ return unless cache_versioning
+
+ if has_attribute?("updated_at")
+ timestamp = updated_at_before_type_cast
+ if can_use_fast_cache_version?(timestamp)
+ raw_timestamp_to_cache_version(timestamp)
+ elsif timestamp = updated_at
+ timestamp.utc.to_s(cache_timestamp_format)
+ end
+ else
+ if self.class.has_attribute?("updated_at")
+ raise ActiveModel::MissingAttributeError, "missing attribute: updated_at"
+ end
end
end
@@ -151,5 +162,42 @@ module ActiveRecord
end
end
end
+
+ private
+ # Detects if the value before type cast
+ # can be used to generate a cache_version.
+ #
+ # The fast cache version only works with a
+ # string value directly from the database.
+ #
+ # We also must check if the timestamp format has been changed
+ # or if the timezone is not set to UTC then
+ # we cannot apply our transformations correctly.
+ def can_use_fast_cache_version?(timestamp)
+ timestamp.is_a?(String) &&
+ cache_timestamp_format == :usec &&
+ default_timezone == :utc &&
+ !updated_at_came_from_user?
+ end
+
+ # Converts a raw database string to `:usec`
+ # format.
+ #
+ # Example:
+ #
+ # timestamp = "2018-10-15 20:02:15.266505"
+ # raw_timestamp_to_cache_version(timestamp)
+ # # => "20181015200215266505"
+ #
+ # Postgres truncates trailing zeros, https://bit.ly/2QUlXiZ
+ # to account for this we pad the output with zeros
+ def raw_timestamp_to_cache_version(timestamp)
+ key = timestamp.delete("- :.")
+ if key.length < 20
+ key.ljust(20, "0")
+ else
+ key
+ end
+ end
end
end
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
index 3a569f226e..3a06b1c795 100644
--- a/activerecord/test/cases/cache_key_test.rb
+++ b/activerecord/test/cases/cache_key_test.rb
@@ -44,10 +44,88 @@ module ActiveRecord
test "cache_key_with_version always has both key and version" do
r1 = CacheMeWithVersion.create
- assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version
+ assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.utc.to_s(:usec)}", r1.cache_key_with_version
r2 = CacheMe.create
- assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version
+ assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.utc.to_s(:usec)}", r2.cache_key_with_version
+ end
+
+ test "cache_version is the same when it comes from the DB or from the user" do
+ skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
+
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_not_called(record_from_db, :updated_at) do
+ record_from_db.cache_version
+ end
+
+ assert_equal record.cache_version, record_from_db.cache_version
+ end
+
+ test "cache_version does not truncate zeros when timestamp ends in zeros" do
+ skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
+
+ travel_to Time.now.beginning_of_day do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_not_called(record_from_db, :updated_at) do
+ record_from_db.cache_version
+ end
+
+ assert_equal record.cache_version, record_from_db.cache_version
+ end
+ end
+
+ test "cache_version calls updated_at when the value is generated at create time" do
+ record = CacheMeWithVersion.create
+ assert_called(record, :updated_at) do
+ record.cache_version
+ end
+ end
+
+ test "cache_version does NOT call updated_at when value is from the database" do
+ skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
+
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_not_called(record_from_db, :updated_at) do
+ record_from_db.cache_version
+ end
+ end
+
+ test "cache_version does call updated_at when it is assigned via a Time object" do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_called(record_from_db, :updated_at) do
+ record_from_db.updated_at = Time.now
+ record_from_db.cache_version
+ end
+ end
+
+ test "cache_version does call updated_at when it is assigned via a string" do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_called(record_from_db, :updated_at) do
+ record_from_db.updated_at = Time.now.to_s
+ record_from_db.cache_version
+ end
+ end
+
+ test "cache_version does call updated_at when it is assigned via a hash" do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_called(record_from_db, :updated_at) do
+ record_from_db.updated_at = { 1 => 2016, 2 => 11, 3 => 12, 4 => 1, 5 => 2, 6 => 3, 7 => 22 }
+ record_from_db.cache_version
+ end
+ end
+
+ test "updated_at on class but not on instance raises an error" do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.where(id: record.id).select(:id).first
+ assert_raises(ActiveModel::MissingAttributeError) do
+ record_from_db.cache_version
+ end
end
end
end