class ViewModel::ActiveRecord::Cache::CacheWorker
def add_refs_to_worklist(cacheable_references)
def add_refs_to_worklist(cacheable_references) cacheable_references.each do |ref_name, (type, id)| next if resolved_references.has_key?(ref_name) (@worklist[type] ||= {})[id] = WorklistEntry.new(ref_name, nil) end end
def add_viewmodels_to_worklist(referenced_viewmodels)
def add_viewmodels_to_worklist(referenced_viewmodels) referenced_viewmodels.each do |ref_name, viewmodel| next if resolved_references.has_key?(ref_name) (@worklist[viewmodel.class.view_name] ||= {})[viewmodel.id] = WorklistEntry.new(ref_name, viewmodel) end end
def cacheable_reference(viewmodel)
def cacheable_reference(viewmodel) [viewmodel.class.view_name, viewmodel.id] end
def find_and_preload_viewmodels(viewmodel_class, ids, available_viewmodels: nil)
Resolves viewmodels for the provided ids from the database or
def find_and_preload_viewmodels(viewmodel_class, ids, available_viewmodels: nil) viewmodels = [] if available_viewmodels.present? ids = ids.reject do |id| if (vm = available_viewmodels[id]) viewmodels << vm end end end if ids.present? found = viewmodel_class.find(ids, eager_include: false, lock: 'FOR SHARE') viewmodels.concat(found) end ViewModel.preload_for_serialization(viewmodels, include_referenced: false, lock: 'FOR SHARE') viewmodels end
def initialize(migration_versions:, serialize_context:, ignore_existing: false)
def initialize(migration_versions:, serialize_context:, ignore_existing: false) @worklist = {} # Hash[type_name, Hash[id, WorklistEntry]] @resolved_references = {} # Hash[refname, json] @migration_versions = migration_versions @migrated_cache_versions = {} @serialize_context = serialize_context @ignore_existing = ignore_existing end
def load_from_cache(viewmodel_cache, ids)
{id=>serialized_view}. Any references encountered are added to the
Loads the specified entities from the cache and returns a hash of
def load_from_cache(viewmodel_cache, ids) cached_serializations = viewmodel_cache.load(ids, migrated_cache_version(viewmodel_cache)) cached_serializations.each_with_object({}) do |(id, cached_serialization), result| add_refs_to_worklist(cached_serialization[:ref_cache]) result[id] = cached_serialization[:data] end end
def migrated_cache_version(viewmodel_cache)
def migrated_cache_version(viewmodel_cache) @migrated_cache_versions[viewmodel_cache] ||= viewmodel_cache.migrated_cache_version(migration_versions) end
def render_from_cache(viewmodel_class, ids, initial_viewmodels: nil, locked: false)
def render_from_cache(viewmodel_class, ids, initial_viewmodels: nil, locked: false) viewmodel_class.transaction do root_serializations = Array.new(ids.size) # Collect input array positions for each id, allowing duplicates positions = ids.each_with_index.with_object({}) do |(id, i), h| (h[id] ||= []) << i end # If duplicates are specified, fetch each only once ids = positions.keys ids_to_render = ids.to_set if viewmodel_class < CacheableView && !@ignore_existing # Load existing serializations from the cache cached_serializations = load_from_cache(viewmodel_class.viewmodel_cache, ids) cached_serializations.each do |id, data| positions[id].each do |idx| root_serializations[idx] = data end end ids_to_render.subtract(cached_serializations.keys) # If initial root viewmodels were provided, call hooks on any # viewmodels which were rendered from the cache to ensure that the # root is visible (in isolation). Other than this, no traversal # callbacks are performed for cache-rendered views. This particularly # requires care for references: if a visible view may refer to # non-visible cacheable views, those referenced views will not be # access control checked. initial_viewmodels&.each do |v| next unless cached_serializations.has_key?(v.id) serialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeVisit, v) serialize_context.run_callback(ViewModel::Callbacks::Hook::AfterVisit, v) end end # Render remaining views. If initial viewmodels have been locked, we may # use them to serialize from, otherwise we must reload with share lock # in find_and_preload. available_viewmodels = if locked initial_viewmodels&.each_with_object({}) do |vm, h| h[vm.id] = vm if ids_to_render.include?(vm.id) end end viewmodels = find_and_preload_viewmodels(viewmodel_class, ids_to_render.to_a, available_viewmodels: available_viewmodels) loaded_serializations = serialize_and_cache(viewmodels) loaded_serializations.each do |id, data| positions[id].each do |idx| root_serializations[idx] = data end end # recursively resolve referenced views self.resolve_references! [root_serializations, self.resolved_references] end end
def resolve_references!
def resolve_references! @serialize_context = serialize_context.for_references while @worklist.present? type_name, required_entries = @worklist.shift viewmodel_class = ViewModel::Registry.for_view_name(type_name) required_entries.each do |_id, entry| @resolved_references[entry.ref_name] = SENTINEL end if viewmodel_class < CacheableView && !@ignore_existing cached_serializations = load_from_cache(viewmodel_class.viewmodel_cache, required_entries.keys) cached_serializations.each do |id, data| ref_name = required_entries.delete(id).ref_name @resolved_references[ref_name] = data end end # Load remaining entries from database available_viewmodels = required_entries.each_with_object({}) do |(id, entry), h| h[id] = entry.viewmodel if entry.viewmodel end viewmodels = begin find_and_preload_viewmodels(viewmodel_class, required_entries.keys, available_viewmodels: available_viewmodels) rescue ViewModel::DeserializationError::NotFound => e # We encountered a reference to an entity that does not exist. # If this reference was potentially found in cached data, it # could be stale: we can retry without using the cache. # If the reference was obtained directly, it indicates invalid # data such as an invalid foreign key, and we cannot recover. raise StaleCachedReference.new(e) end loaded_serializations = serialize_and_cache(viewmodels) loaded_serializations.each do |id, data| ref_name = required_entries[id].ref_name @resolved_references[ref_name] = data end end end
def serialize_and_cache(viewmodels)
added to the cache. Any references encountered during serialization are
{id=>serialized_view}. If the viewmodel type is cacheable, it will be
Serializes the specified preloaded viewmodels and returns a hash of
def serialize_and_cache(viewmodels) viewmodels.each_with_object({}) do |viewmodel, result| builder = Jbuilder.new do |json| ViewModel.serialize(viewmodel, json, serialize_context: serialize_context) end # viewmodels referenced from roots referenced_viewmodels = serialize_context.extract_referenced_views! if migration_versions.present? migrator = ViewModel::DownMigrator.new(migration_versions) # This migration isn't able to affect the contents of referenced # views, only their presence. The references will be themselves # rendered (and migrated) independently later. We mark the dummy # references provided to exclude their partial contents from being # themselves migrated. dummy_references = referenced_viewmodels.transform_values do |ref_vm| { ViewModel::TYPE_ATTRIBUTE => ref_vm.class.view_name, ViewModel::VERSION_ATTRIBUTE => ref_vm.class.schema_version, ViewModel::ID_ATTRIBUTE => ref_vm.id, ViewModel::Migrator::EXCLUDE_FROM_MIGRATION => true, }.freeze end migrator.migrate!({ 'data' => builder.attributes!, 'references' => dummy_references }) # Removed dummy references can be removed from referenced_viewmodels. referenced_viewmodels.keep_if { |k, _| dummy_references.has_key?(k) } # Introduced dummy references cannot be handled. if dummy_references.keys != referenced_viewmodels.keys version = migration_versions[viewmodel.class] raise ViewModel::Error.new( status: 500, detail: "Down-migration for cacheable view #{viewmodel.class} to v#{version} must not introduce new shared references") end end data_serialization = builder.target! add_viewmodels_to_worklist(referenced_viewmodels) if viewmodel.class < CacheableView cacheable_references = referenced_viewmodels.transform_values { |vm| cacheable_reference(vm) } target_cache = viewmodel.class.viewmodel_cache target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references) end result[viewmodel.id] = data_serialization end end