mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Use raw time string from DB to generate ActiveRecord#cache_version
Currently, the `updated_at` field is used to generate a `cache_version`. Some database adapters return this timestamp value as a string that must then be converted to a Time value. This process requires a lot of memory and even more CPU time. In the case where this value is only being used for a cache version, we can skip the Time conversion by using the string value directly. - This PR preserves existing cache format by converting a UTC string from the database to `:usec` format. - Some databases return an already converted Time object, in those instances, we can directly use `created_at`. - The `updated_at_before_type_cast` can be a value that comes from either the database or the user. We only want to optimize the case where it is from the database. - If the format of the cache version has been changed, we cannot apply this optimization, and it is skipped. - If the format of the time in the database is not UTC, then we cannot use this optimization, and it is skipped. Some databases (notably PostgreSQL) returns a variable length nanosecond value in the time string. If the value ends in a zero, then it is truncated For instance instead of `2018-10-12 05:00:00.000000` the value `2018-10-12 05:00:00` is returned. We detect this case and pad the remaining zeros to ensure consistent cache version generation. Before: Total allocated: 743842 bytes (6626 objects) After: Total allocated: 702955 bytes (6063 objects) (743842 - 702955) / 743842.0 # => 5.4% ⚡️⚡️⚡️⚡️⚡️ Using the CodeTriage application and derailed benchmarks this PR shows between 9-11% (statistically significant) performance improvement versus the commit before it. Special thanks to @lsylvester for helping to figure out a way to preserve the usec format and for helping with many implementation details.
This commit is contained in:
parent
ead868315f
commit
04454839a1
2 changed files with 118 additions and 4 deletions
|
@ -96,8 +96,14 @@ module ActiveRecord
|
||||||
# Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
|
# Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
|
||||||
# +false+ (which it is by default until Rails 6.0).
|
# +false+ (which it is by default until Rails 6.0).
|
||||||
def cache_version
|
def cache_version
|
||||||
if cache_versioning && timestamp = try(:updated_at)
|
return unless cache_versioning
|
||||||
timestamp.utc.to_s(:usec)
|
return unless 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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -151,5 +157,43 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
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("- :.")
|
||||||
|
padding = 20 - key.length
|
||||||
|
if padding != 0
|
||||||
|
key << "0" * padding
|
||||||
|
else
|
||||||
|
key
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,10 +44,80 @@ module ActiveRecord
|
||||||
|
|
||||||
test "cache_key_with_version always has both key and version" do
|
test "cache_key_with_version always has both key and version" do
|
||||||
r1 = CacheMeWithVersion.create
|
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
|
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
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue