Skip to content

Commit 9c4d3c3

Browse files
authored
Merge pull request rails#33835 from schneems/schneems/faster_cache_version
Use raw time string from DB to generate ActiveRecord#cache_version
2 parents 129788d + 73f9cd2 commit 9c4d3c3

File tree

2 files changed

+130
-4
lines changed

2 files changed

+130
-4
lines changed

activerecord/lib/active_record/integration.rb

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,19 @@ def cache_key(*timestamp_names)
9696
# Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
9797
# +false+ (which it is by default until Rails 6.0).
9898
def cache_version
99-
if cache_versioning && timestamp = try(:updated_at)
100-
timestamp.utc.to_s(:usec)
99+
return unless cache_versioning
100+
101+
if has_attribute?("updated_at")
102+
timestamp = updated_at_before_type_cast
103+
if can_use_fast_cache_version?(timestamp)
104+
raw_timestamp_to_cache_version(timestamp)
105+
elsif timestamp = updated_at
106+
timestamp.utc.to_s(cache_timestamp_format)
107+
end
108+
else
109+
if self.class.has_attribute?("updated_at")
110+
raise ActiveModel::MissingAttributeError, "missing attribute: updated_at"
111+
end
101112
end
102113
end
103114

@@ -151,5 +162,42 @@ def to_param(method_name = nil)
151162
end
152163
end
153164
end
165+
166+
private
167+
# Detects if the value before type cast
168+
# can be used to generate a cache_version.
169+
#
170+
# The fast cache version only works with a
171+
# string value directly from the database.
172+
#
173+
# We also must check if the timestamp format has been changed
174+
# or if the timezone is not set to UTC then
175+
# we cannot apply our transformations correctly.
176+
def can_use_fast_cache_version?(timestamp)
177+
timestamp.is_a?(String) &&
178+
cache_timestamp_format == :usec &&
179+
default_timezone == :utc &&
180+
!updated_at_came_from_user?
181+
end
182+
183+
# Converts a raw database string to `:usec`
184+
# format.
185+
#
186+
# Example:
187+
#
188+
# timestamp = "2018-10-15 20:02:15.266505"
189+
# raw_timestamp_to_cache_version(timestamp)
190+
# # => "20181015200215266505"
191+
#
192+
# Postgres truncates trailing zeros, https://bit.ly/2QUlXiZ
193+
# to account for this we pad the output with zeros
194+
def raw_timestamp_to_cache_version(timestamp)
195+
key = timestamp.delete("- :.")
196+
if key.length < 20
197+
key.ljust(20, "0")
198+
else
199+
key
200+
end
201+
end
154202
end
155203
end

activerecord/test/cases/cache_key_test.rb

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,88 @@ class CacheMeWithVersion < ActiveRecord::Base
4444

4545
test "cache_key_with_version always has both key and version" do
4646
r1 = CacheMeWithVersion.create
47-
assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version
47+
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
4848

4949
r2 = CacheMe.create
50-
assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version
50+
assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.utc.to_s(:usec)}", r2.cache_key_with_version
51+
end
52+
53+
test "cache_version is the same when it comes from the DB or from the user" do
54+
skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
55+
56+
record = CacheMeWithVersion.create
57+
record_from_db = CacheMeWithVersion.find(record.id)
58+
assert_not_called(record_from_db, :updated_at) do
59+
record_from_db.cache_version
60+
end
61+
62+
assert_equal record.cache_version, record_from_db.cache_version
63+
end
64+
65+
test "cache_version does not truncate zeros when timestamp ends in zeros" do
66+
skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
67+
68+
travel_to Time.now.beginning_of_day do
69+
record = CacheMeWithVersion.create
70+
record_from_db = CacheMeWithVersion.find(record.id)
71+
assert_not_called(record_from_db, :updated_at) do
72+
record_from_db.cache_version
73+
end
74+
75+
assert_equal record.cache_version, record_from_db.cache_version
76+
end
77+
end
78+
79+
test "cache_version calls updated_at when the value is generated at create time" do
80+
record = CacheMeWithVersion.create
81+
assert_called(record, :updated_at) do
82+
record.cache_version
83+
end
84+
end
85+
86+
test "cache_version does NOT call updated_at when value is from the database" do
87+
skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
88+
89+
record = CacheMeWithVersion.create
90+
record_from_db = CacheMeWithVersion.find(record.id)
91+
assert_not_called(record_from_db, :updated_at) do
92+
record_from_db.cache_version
93+
end
94+
end
95+
96+
test "cache_version does call updated_at when it is assigned via a Time object" do
97+
record = CacheMeWithVersion.create
98+
record_from_db = CacheMeWithVersion.find(record.id)
99+
assert_called(record_from_db, :updated_at) do
100+
record_from_db.updated_at = Time.now
101+
record_from_db.cache_version
102+
end
103+
end
104+
105+
test "cache_version does call updated_at when it is assigned via a string" do
106+
record = CacheMeWithVersion.create
107+
record_from_db = CacheMeWithVersion.find(record.id)
108+
assert_called(record_from_db, :updated_at) do
109+
record_from_db.updated_at = Time.now.to_s
110+
record_from_db.cache_version
111+
end
112+
end
113+
114+
test "cache_version does call updated_at when it is assigned via a hash" do
115+
record = CacheMeWithVersion.create
116+
record_from_db = CacheMeWithVersion.find(record.id)
117+
assert_called(record_from_db, :updated_at) do
118+
record_from_db.updated_at = { 1 => 2016, 2 => 11, 3 => 12, 4 => 1, 5 => 2, 6 => 3, 7 => 22 }
119+
record_from_db.cache_version
120+
end
121+
end
122+
123+
test "updated_at on class but not on instance raises an error" do
124+
record = CacheMeWithVersion.create
125+
record_from_db = CacheMeWithVersion.where(id: record.id).select(:id).first
126+
assert_raises(ActiveModel::MissingAttributeError) do
127+
record_from_db.cache_version
128+
end
51129
end
52130
end
53131
end

0 commit comments

Comments
 (0)