module Scrivito module Backend module ObjDataCache # in the cache, missing objs (no obj exists with the given id) are # represented as an empty hash to distinguish them from cache misses. # nil => cache miss for given id # {} => cache hit for given id, no obj with this id exists NONEXISTENT_OBJ = {} def self.convert_from_backend(obj_data) if obj_data.blank? || obj_data["_deleted"] ObjDataCache::NONEXISTENT_OBJ else obj_data end end def self.view_for_revision(revision) if revision.base SimpleCacheView.new(revision.id) else view_for_workspace(revision.workspace.id, revision.content_state_node) end end def self.view_for_workspace(workspace_id, content_state_node) IncrementalCacheView.new(workspace_id, content_state_node) end # builds a "change index", i.e. a hash mapping from `_id` to # CmsDataCache-Tags that reference the data of the Obj def self.changes_index_from(objs) obj_ids = objs.map { |obj| obj["_id"] || obj["_deleted"] } tags = objs. map(&method(:convert_from_backend)). map(&CmsDataCache.method(:write_data_to_tag)) Hash[obj_ids.zip(tags)] end # accepts a "change index" (id --> tag), looks up each # tag in the cache and return an "expanded index" (id --> data). # return `nil` if any tag cannot be found in the cache. def self.expand_changes_index(change_index) # using "merge(self)" to map over a hash change_index.merge(change_index) do |id, tag| CmsDataCache.read_data_from_tag(tag) or return nil end end class SimpleCacheView < Struct.new(:cache_id) def read_obj(id) if tag = CmsDataCache.read_obj_data(cache_id, "id", id) CmsDataCache.read_data_from_tag(tag) end end def write_obj(id, data) write_cache("id", id, CmsDataCache.write_data_to_tag(data)) end def read_index(index, key) index_access end def write_index(index, key, data, **options) index_access end def write_index_not_updatable(index, key, data, **options) index_access end private def index_access # SimpleCacheView is currently only used for base revision that do not # support index queries. raise InternalError end def write_cache(index, key, data) raise InternalError unless data CmsDataCache.write_obj_data(cache_id, index, key, data) end end class IncrementalCacheView < Struct.new(:cache_id, :viewed_state) def read_index(index, key, &update_function) if hit = CmsDataCache.read_obj_data(cache_id, index, key) cached_at = hit.first cached_result = hit.second # if no update needed, just return return cached_result if cached_at == viewed_state.content_state_id updated_result = update_result( key, cached_result, cached_at, &update_function) return nil unless updated_result unless recent?(cached_at) write_cache_updatable(index, key, updated_result) end updated_result end end def write_index(index, key, data, **options) write_cache_updatable(index, key, data, **options) end # use this to cache data that cannot be updated def write_index_not_updatable(index, key, data, **options) write_cache(index, key, viewed_state.content_state_id, data, **options) end def read_obj(id) if hit = CmsDataCache.read_obj_data(cache_id, "id", id) cached_at, cached_tag = hit tag_in_changes = viewed_state.obj_from_changes(id, cached_at) updated_tag = case tag_in_changes when ContentStateNode::CACHE_MISS # could not reconstruct all changes since `cached_at` # so we don't know if data is still valid. nil when ContentStateNode::NOT_FOUND # there were no relevant changes, so the data is still valid cached_tag else # the changes revealed a newer version, return it instead tag_in_changes end return nil unless updated_tag unless recent?(cached_at) write_cache_updatable("id", id, updated_tag) end CmsDataCache.read_data_from_tag(updated_tag) end end def write_obj(id, data) write_obj_tag(id, CmsDataCache.write_data_to_tag(data)) end def write_obj_tag(id, tag) write_cache_updatable("id", id, tag) end private # is the given csid reachable with at most one ContentStateNode traversal? def recent?(csid) return true if csid == viewed_state.content_state_id predecessor = viewed_state.predecessor predecessor && csid == predecessor.content_state_id end def write_cache_updatable(index, key, data, **options) stable_csid = viewed_state.next_stable_node.content_state_id write_cache(index, key, stable_csid, data, **options) end def write_cache(index, key, csid, data, **options) raise InternalError unless data CmsDataCache.write_obj_data( cache_id, index, key, [csid, data], **options) end # tries to update an outdated cache result using the changes recorded in the # ContentStateNode history. # returns `nil` (= cache-miss) if update is not possible. def update_result(key, cached_result, cached_at, &update_function) # if index does not support update, treat as cache-miss return nil unless update_function change_index = viewed_state.change_index_for(cached_at) return nil if change_index == ContentStateNode::CACHE_MISS expanded_index = ObjDataCache.expand_changes_index(change_index) # if the data of changed objs was evicted, treat as cache-miss return nil if !expanded_index update_function.call(key, cached_result, expanded_index) end end end end end