Enable Rails 7.1 Marshalling format (#28609)
This commit is contained in:
		
					parent
					
						
							
								12bed81187
							
						
					
				
			
			
				commit
				
					
						5a6d533c53
					
				
			
		
					 5 changed files with 8 additions and 180 deletions
				
			
		|  | @ -3,150 +3,6 @@ | |||
| module CacheConcern | ||||
|   extend ActiveSupport::Concern | ||||
| 
 | ||||
|   module ActiveRecordCoder | ||||
|     EMPTY_HASH = {}.freeze | ||||
| 
 | ||||
|     class << self | ||||
|       def dump(record) | ||||
|         instances = InstanceTracker.new | ||||
|         serialized_associations = serialize_associations(record, instances) | ||||
|         serialized_records = instances.map { |r| serialize_record(r) } | ||||
|         [serialized_associations, *serialized_records] | ||||
|       end | ||||
| 
 | ||||
|       def load(payload) | ||||
|         instances = InstanceTracker.new | ||||
|         serialized_associations, *serialized_records = payload | ||||
|         serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) } | ||||
|         deserialize_associations(serialized_associations, instances) | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       # Records without associations, or which have already been visited before, | ||||
|       # are serialized by their id alone. | ||||
|       # | ||||
|       # Records with associations are serialized as a two-element array including | ||||
|       # their id and the record's association cache. | ||||
|       # | ||||
|       def serialize_associations(record, instances) | ||||
|         return unless record | ||||
| 
 | ||||
|         if (id = instances.lookup(record)) | ||||
|           payload = id | ||||
|         else | ||||
|           payload = instances.push(record) | ||||
| 
 | ||||
|           cached_associations = record.class.reflect_on_all_associations.select do |reflection| | ||||
|             record.association_cached?(reflection.name) | ||||
|           end | ||||
| 
 | ||||
|           unless cached_associations.empty? | ||||
|             serialized_associations = cached_associations.map do |reflection| | ||||
|               association = record.association(reflection.name) | ||||
| 
 | ||||
|               serialized_target = if reflection.collection? | ||||
|                                     association.target.map { |target_record| serialize_associations(target_record, instances) } | ||||
|                                   else | ||||
|                                     serialize_associations(association.target, instances) | ||||
|                                   end | ||||
| 
 | ||||
|               [reflection.name, serialized_target] | ||||
|             end | ||||
| 
 | ||||
|             payload = [payload, serialized_associations] | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         payload | ||||
|       end | ||||
| 
 | ||||
|       def deserialize_associations(payload, instances) | ||||
|         return unless payload | ||||
| 
 | ||||
|         id, associations = payload | ||||
|         record = instances.fetch(id) | ||||
| 
 | ||||
|         associations&.each do |name, serialized_target| | ||||
|           begin | ||||
|             association = record.association(name) | ||||
|           rescue ActiveRecord::AssociationNotFoundError | ||||
|             raise AssociationMissingError, "undefined association: #{name}" | ||||
|           end | ||||
| 
 | ||||
|           target = if association.reflection.collection? | ||||
|                      serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) } | ||||
|                    else | ||||
|                      deserialize_associations(serialized_target, instances) | ||||
|                    end | ||||
| 
 | ||||
|           association.target = target | ||||
|         end | ||||
| 
 | ||||
|         record | ||||
|       end | ||||
| 
 | ||||
|       def serialize_record(record) | ||||
|         arguments = [record.class.name, attributes_for_database(record)] | ||||
|         arguments << true if record.new_record? | ||||
|         arguments | ||||
|       end | ||||
| 
 | ||||
|       def attributes_for_database(record) | ||||
|         attributes = record.attributes_for_database | ||||
|         attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } | ||||
|         attributes | ||||
|       end | ||||
| 
 | ||||
|       def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter | ||||
|         begin | ||||
|           klass = Object.const_get(class_name) | ||||
|         rescue NameError | ||||
|           raise ClassMissingError, "undefined class: #{class_name}" | ||||
|         end | ||||
| 
 | ||||
|         # Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass | ||||
|         # wether the record was persisted or not. | ||||
|         attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH) | ||||
|         klass.allocate.init_with_attributes(attributes, new_record) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     class Error < StandardError | ||||
|     end | ||||
| 
 | ||||
|     class ClassMissingError < Error | ||||
|     end | ||||
| 
 | ||||
|     class AssociationMissingError < Error | ||||
|     end | ||||
| 
 | ||||
|     class InstanceTracker | ||||
|       def initialize | ||||
|         @instances = [] | ||||
|         @ids = {}.compare_by_identity | ||||
|       end | ||||
| 
 | ||||
|       def map(&block) | ||||
|         @instances.map(&block) | ||||
|       end | ||||
| 
 | ||||
|       def fetch(...) | ||||
|         @instances.fetch(...) | ||||
|       end | ||||
| 
 | ||||
|       def push(instance) | ||||
|         id = @ids[instance] = @instances.size | ||||
|         @instances << instance | ||||
|         id | ||||
|       end | ||||
| 
 | ||||
|       def lookup(instance) | ||||
|         @ids[instance] | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   class_methods do | ||||
|     def vary_by(value, **kwargs) | ||||
|       before_action(**kwargs) do |controller| | ||||
|  | @ -196,11 +52,7 @@ module CacheConcern | |||
|     raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) | ||||
|     return [] if raw.empty? | ||||
| 
 | ||||
|     cached_keys_with_value = begin | ||||
|       Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) } | ||||
|     rescue ActiveRecordCoder::Error | ||||
|       {} # The serialization format may have changed, let's pretend it's a cache miss. | ||||
|     end | ||||
|     cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) | ||||
| 
 | ||||
|     uncached_ids = raw.map(&:id) - cached_keys_with_value.keys | ||||
| 
 | ||||
|  | @ -208,10 +60,7 @@ module CacheConcern | |||
| 
 | ||||
|     unless uncached_ids.empty? | ||||
|       uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) | ||||
| 
 | ||||
|       uncached.each_value do |item| | ||||
|         Rails.cache.write(item, ActiveRecordCoder.dump(item)) | ||||
|       end | ||||
|       Rails.cache.write_multi(uncached.values.to_h { |i| [i, i] }) | ||||
|     end | ||||
| 
 | ||||
|     raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] } | ||||
|  |  | |||
|  | @ -17,23 +17,8 @@ | |||
| class CustomFilter < ApplicationRecord | ||||
|   self.ignored_columns += %w(whole_word irreversible) | ||||
| 
 | ||||
|   # NOTE: We previously used `alias_attribute` but this does not play nicely | ||||
|   # with cache | ||||
|   def title | ||||
|     phrase | ||||
|   end | ||||
| 
 | ||||
|   def title=(value) | ||||
|     self.phrase = value | ||||
|   end | ||||
| 
 | ||||
|   def filter_action | ||||
|     action | ||||
|   end | ||||
| 
 | ||||
|   def filter_action=(value) | ||||
|     self.action = value | ||||
|   end | ||||
|   alias_attribute :title, :phrase | ||||
|   alias_attribute :filter_action, :action | ||||
| 
 | ||||
|   VALID_CONTEXTS = %w( | ||||
|     home | ||||
|  |  | |||
|  | @ -17,15 +17,7 @@ class CustomFilterKeyword < ApplicationRecord | |||
| 
 | ||||
|   validates :keyword, presence: true | ||||
| 
 | ||||
|   # NOTE: We previously used `alias_attribute` but this does not play nicely | ||||
|   # with cache | ||||
|   def phrase | ||||
|     keyword | ||||
|   end | ||||
| 
 | ||||
|   def phrase=(value) | ||||
|     self.keyword = value | ||||
|   end | ||||
|   alias_attribute :phrase, :keyword | ||||
| 
 | ||||
|   before_save :prepare_cache_invalidation! | ||||
|   before_destroy :prepare_cache_invalidation! | ||||
|  |  | |||
|  | @ -63,6 +63,8 @@ module Mastodon | |||
|     # Initialize configuration defaults for originally generated Rails version. | ||||
|     config.load_defaults 7.0 | ||||
| 
 | ||||
|     config.active_record.marshalling_format_version = 7.1 | ||||
| 
 | ||||
|     # Please, add to the `ignore` list any other `lib` subdirectories that do | ||||
|     # not contain `.rb` files, or that should not be reloaded or eager loaded. | ||||
|     # Common ones are `templates`, `generators`, or `middleware`, for example. | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ REDIS_CACHE_PARAMS = { | |||
|   driver: :hiredis, | ||||
|   url: ENV['CACHE_REDIS_URL'], | ||||
|   expires_in: 10.minutes, | ||||
|   namespace: cache_namespace, | ||||
|   namespace: "#{cache_namespace}:7.1", | ||||
|   connect_timeout: 5, | ||||
|   pool: { | ||||
|     size: Sidekiq.server? ? Sidekiq[:concurrency] : Integer(ENV['MAX_THREADS'] || 5), | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue