diff options
| -rw-r--r-- | activerecord/lib/active_record/integration.rb | 52 | ||||
| -rw-r--r-- | activerecord/test/cases/cache_key_test.rb | 82 | 
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 | 
