diff --git a/app/models/manager_refresh/inventory_collection.rb b/app/models/manager_refresh/inventory_collection.rb index af4a62fb600..0e9c6ca6090 100644 --- a/app/models/manager_refresh/inventory_collection.rb +++ b/app/models/manager_refresh/inventory_collection.rb @@ -44,17 +44,19 @@ module ManagerRefresh # puts @ems.vms.collect(&:ems_ref) # => ["vm2", "vm3"] # class InventoryCollection + # @return [Boolean] A true value marks that we collected all the data of the InventoryCollection, + # meaning we also collected all the references. + attr_accessor :data_collection_finalized + + # @return [ManagerRefresh::InventoryCollection::DataStorage] An InventoryCollection encapsulating all data with + # indexes + attr_accessor :data_storage + # @return [Boolean] true if this collection is already saved into the DB. E.g. InventoryCollections with # DB only strategy are marked as saved. This causes InventoryCollection not being a dependency for any other # InventoryCollection, since it is already persisted into the DB. attr_accessor :saved - # @return [Array] objects of the InventoryCollection in an Array - attr_accessor :data - - # @return [Hash] InventoryObject objects of the InventoryCollection indexed in a Hash by their :manager_ref. - attr_accessor :data_index - # @return [Set] A set of InventoryObjects manager_uuids, which tells us which InventoryObjects were # referenced by other InventoryObjects using a lazy_find. attr_accessor :references @@ -63,10 +65,6 @@ class InventoryCollection # were referenced by other InventoryObject objects using a lazy_find with :key. attr_accessor :attribute_references - # @return [Boolean] A true value marks that we collected all the data of the InventoryCollection, - # meaning we also collected all the references. - attr_accessor :data_collection_finalized - # If present, InventoryCollection switches into delete_complement mode, where it will # delete every record from the DB, that is not present in this list. This is used for the batch processing, # where we don't know which InventoryObject should be deleted, but we know all manager_uuids of all @@ -85,13 +83,34 @@ class InventoryCollection attr_reader :model_class, :strategy, :attributes_blacklist, :attributes_whitelist, :custom_save_block, :parent, :internal_attributes, :delete_method, :dependency_attributes, :manager_ref, :association, :complete, :update_only, :transitive_dependency_attributes, :custom_manager_uuid, - :custom_db_finder, :check_changed, :arel, :builder_params, :loaded_references, :db_data_index, + :custom_db_finder, :check_changed, :arel, :builder_params, :inventory_object_attributes, :name, :saver_strategy, :manager_uuids, :skeletal_manager_uuids, :targeted_arel, :targeted, :manager_ref_allowed_nil, :use_ar_object, - :secondary_refs, :secondary_indexes, :created_records, :updated_records, :deleted_records, + :created_records, :updated_records, :deleted_records, :custom_reconnect_block, :batch_extra_attributes - delegate :each, :size, :to => :to_a + delegate :<<, + :build, + :data, + :each, + :find_or_build, + :find_or_build_by, + :from_raw_data, + :from_raw_value, + :index_proxy, + :push, + :size, + :to_a, + :to_raw_data, + :to => :data_storage + + delegate :find, + :find_by, + :lazy_find, + :lazy_find_by, + :primary_index, + :reindex_secondary_indexes!, + :to => :index_proxy # @param model_class [Class] A class of an ApplicationRecord model, that we want to persist into the DB or load from # the DB. @@ -415,7 +434,6 @@ def initialize(model_class: nil, manager_ref: nil, association: nil, parent: nil @parent = parent || nil @arel = arel @dependency_attributes = dependency_attributes || {} - @secondary_indexes = secondary_refs.map { |n, _k| [n, {}] }.to_h @strategy = process_strategy(strategy) @delete_method = delete_method || :destroy @custom_save_block = custom_save_block @@ -442,8 +460,6 @@ def initialize(model_class: nil, manager_ref: nil, association: nil, parent: nil @inventory_object_attributes = inventory_object_attributes - @data = [] - @data_index = {} @saved ||= false @attributes_blacklist = Set.new @attributes_whitelist = Set.new @@ -451,9 +467,8 @@ def initialize(model_class: nil, manager_ref: nil, association: nil, parent: nil @dependees = Set.new @references = Set.new @attribute_references = Set.new - @loaded_references = Set.new - @db_data_index = nil - @data_collection_finalized = false + + @data_storage = ManagerRefresh::InventoryCollection::DataStorage.new(self, secondary_refs) @created_records = [] @updated_records = [] @@ -477,57 +492,6 @@ def store_deleted_records(records) @deleted_records.concat(records_identities(records)) end - def to_a - data - end - - def to_hash - data_index - end - - def from_raw_data(inventory_objects_data, available_inventory_collections) - inventory_objects_data.each do |inventory_object_data| - hash = inventory_object_data.each_with_object({}) do |(key, value), result| - result[key.to_sym] = if value.kind_of?(Array) - value.map { |x| from_raw_value(x, available_inventory_collections) } - else - from_raw_value(value, available_inventory_collections) - end - end - build(hash) - end - end - - def from_raw_value(value, available_inventory_collections) - if value.kind_of?(Hash) && (value['type'] || value[:type]) == "ManagerRefresh::InventoryObjectLazy" - value.transform_keys!(&:to_s) - end - - if value.kind_of?(Hash) && value['type'] == "ManagerRefresh::InventoryObjectLazy" - inventory_collection = available_inventory_collections[value['inventory_collection_name'].try(:to_sym)] - raise "Couldn't build lazy_link #{value} the inventory_collection_name was not found" if inventory_collection.blank? - inventory_collection.lazy_find(value['ems_ref'], :key => value['key'], :default => value['default']) - else - value - end - end - - def to_raw_data - data.map do |inventory_object| - inventory_object.data.transform_values do |value| - if inventory_object_lazy?(value) - value.to_raw_lazy_relation - elsif value.kind_of?(Array) && (inventory_object_lazy?(value.compact.first) || inventory_object?(value.compact.first)) - value.compact.map(&:to_raw_lazy_relation) - elsif inventory_object?(value) - value.to_raw_lazy_relation - else - value - end - end - end - end - def process_saver_strategy(saver_strategy) return :default unless saver_strategy @@ -541,6 +505,8 @@ def process_saver_strategy(saver_strategy) end def process_strategy(strategy_name) + self.data_collection_finalized = false + return unless strategy_name case strategy_name @@ -626,50 +592,6 @@ def targeted? targeted end - def <<(inventory_object) - unless data_index[inventory_object.manager_uuid] - data_index[inventory_object.manager_uuid] = inventory_object - secondary_refs.each do |name, keys| - secondary_indexes[name][inventory_object.id_with_keys(keys)] = inventory_object - end - data << inventory_object - end - self - end - alias push << - - def named_ref(ref) - ref == :manager_ref ? manager_ref : secondary_refs[ref] - end - - def named_index(ref) - ref == :manager_ref ? data_index : secondary_indexes[ref] - end - - # TODO: use object_index_with_keys instead of adding ref? - def object_index(object, ref: :manager_ref) - index_array = named_ref(ref).map do |attribute| - if object.respond_to?(:[]) - object[attribute].to_s - else - object.public_send(attribute).try(:id) || object.public_send(attribute).to_s - end - end - stringify_reference(index_array) - end - - def object_index_with_keys(keys, object) - keys.map { |attribute| object.public_send(attribute).to_s }.join(stringify_joiner) - end - - def stringify_joiner - "__" - end - - def stringify_reference(reference) - reference.join(stringify_joiner) - end - def manager_ref_to_cols # TODO(lsmola) this should contain the polymorphic _type, otherwise the IC with polymorphic unique key will get # conflicts @@ -679,86 +601,6 @@ def manager_ref_to_cols end end - def find_or_build(manager_uuid) - raise "The uuid consists of #{manager_ref.size} attributes, please find_or_build_by method" if manager_ref.size > 1 - - find_or_build_by(manager_ref.first => manager_uuid) - end - - def find_or_build_by(manager_uuid_hash) - if !manager_uuid_hash.keys.all? { |x| manager_ref.include?(x) } || manager_uuid_hash.keys.size != manager_ref.size - raise "Allowed find_or_build_by keys are #{manager_ref}" - end - - # Not using find by since if could take record from db, then any changes would be ignored, since such record will - # not be stored to DB, maybe we should rethink this? - data_index[object_index(manager_uuid_hash)] || build(manager_uuid_hash) - end - - def find(manager_uuid, ref: :manager_ref) - return if manager_uuid.nil? - # TODO `ref` handling is partial - case strategy - when :local_db_find_references, :local_db_cache_all - find_in_db(manager_uuid) - when :local_db_find_missing_references - named_index(ref)[manager_uuid] || find_in_db(manager_uuid) - else - # TODO: find(hash) code path apparently exists for lazy_find(hash) ? Consider deprecating. - # Better have lazy_find_by that computes string uuid ahead of time. - manager_uuid.kind_of?(Hash) ? find_by(manager_uuid, :ref => ref) : named_index(ref)[manager_uuid] - end - end - - def find_by(manager_uuid_hash, ref: :manager_ref) - keys = named_ref(ref) - if !manager_uuid_hash.keys.all? { |x| keys.include?(x) } || manager_uuid_hash.keys.size != keys.size - raise "Allowed find_by ref=#{ref} keys are #{keys}" - end - manager_uuid = object_index(manager_uuid_hash, :ref => ref) - find(manager_uuid, :ref => ref) - end - - def lazy_find_by(manager_uuid_hash, ref: :manager_ref, key: nil, default: nil) - # TODO raise for missing keys like `find_by` - lazy_find(object_index(manager_uuid_hash, :ref => ref), :ref => ref, :key => key, :default => default) - end - - def lazy_find(manager_uuid, ref: :manager_ref, key: nil, default: nil) - ::ManagerRefresh::InventoryObjectLazy.new(self, manager_uuid, :ref => ref, :key => key, :default => default) - end - - def inventory_object_class - @inventory_object_class ||= begin - klass = Class.new(::ManagerRefresh::InventoryObject) - klass.add_attributes(inventory_object_attributes) if inventory_object_attributes - klass - end - end - - def new_inventory_object(hash) - manager_ref.each do |x| - # TODO(lsmola) with some effort, we can do this, but it's complex - raise "A lazy_find with a :key can't be a part of the manager_uuid" if inventory_object_lazy?(hash[x]) && hash[x].key - end - - inventory_object_class.new(self, hash) - end - - def build(hash) - hash = builder_params.merge(hash) - inventory_object = new_inventory_object(hash) - - uuid = inventory_object.manager_uuid - # Each InventoryObject must be able to build an UUID, return nil if it can't - return nil if uuid.blank? - # Return existing InventoryObject if we have it - return data_index[uuid] if data_index[uuid] - # Store new InventoryObject and return it - push(inventory_object) - inventory_object - end - def filtered_dependency_attributes filtered_attributes = dependency_attributes @@ -837,8 +679,7 @@ def clone # InventoryCollection :dependency_attributes => dependency_attributes.clone) - cloned.data_index = data_index - cloned.data = data + cloned.data_storage = data_storage cloned end @@ -933,6 +774,22 @@ def batch_size_pure_sql 10_000 end + def hash_index_with_keys(keys, hash) + stringify_reference(keys.map { |attribute| hash[attribute].to_s }) + end + + def object_index_with_keys(keys, object) + stringify_reference(keys.map { |attribute| object.public_send(attribute).to_s }) + end + + def stringify_joiner + "__" + end + + def stringify_reference(reference) + reference.join(stringify_joiner) + end + def build_multi_selection_condition(hashes, keys = nil) keys ||= manager_ref table_name = model_class.table_name @@ -978,9 +835,26 @@ def full_collection_for_comparison parent.send(association) end + def new_inventory_object(hash) + manager_ref.each do |x| + # TODO(lsmola) with some effort, we can do this, but it's complex + raise "A lazy_find with a :key can't be a part of the manager_uuid" if inventory_object_lazy?(hash[x]) && hash[x].key + end + + inventory_object_class.new(self, hash) + end + + attr_writer :attributes_blacklist, :attributes_whitelist + private - attr_writer :attributes_blacklist, :attributes_whitelist, :db_data_index + def inventory_object_class + @inventory_object_class ||= begin + klass = Class.new(::ManagerRefresh::InventoryObject) + klass.add_attributes(inventory_object_attributes) if inventory_object_attributes + klass + end + end # Returns array of records identities def records_identities(records) @@ -997,128 +871,6 @@ def record_identity(record) } end - # Finds manager_uuid in the DB. Using a configured strategy we cache obtained data in the db_data_index, so the - # same find will not hit database twice. Also if we use lazy_links and this is called when - # data_collection_finalized?, we load all data from the DB, referenced by lazy_links, in one query. - # - # @param manager_uuid [String] a manager_uuid of the InventoryObject we search in the local DB - def find_in_db(manager_uuid) - # Use the cached db_data_index only data_collection_finalized?, meaning no new reference can occur - if data_collection_finalized? && db_data_index - return db_data_index[manager_uuid] - else - return db_data_index[manager_uuid] if db_data_index && db_data_index[manager_uuid] - # We haven't found the reference, lets add it to the list of references and load it - references << manager_uuid if manager_uuid - end - - # Put our existing data keys into loaded references - loaded_references.merge(data_index.keys) - # Load the rest of the references from the DB - populate_db_data_index! - - db_data_index[manager_uuid] - end - - # Fills db_data_index with InventoryObjects obtained from the DB - def populate_db_data_index! - # Load only new references from the DB - new_references = references - loaded_references - # And store which references we've already loaded - loaded_references.merge(new_references) - - # Initialize db_data_index in nil - self.db_data_index ||= {} - - return if new_references.blank? # Return if all references are already loaded - - # TODO(lsmola) selected need to contain also :keys used in other InventoryCollections pointing to this one, once - # we get list of all keys for each InventoryCollection ,we can uncomnent - # selected = [:id] + manager_ref.map { |x| model_class.reflect_on_association(x).try(:foreign_key) || x } - # selected << :type if model_class.new.respond_to? :type - # load_from_db.select(selected).find_each do |record| - - # Return the the correct relation based on strategy and selection&projection - case strategy - when :local_db_cache_all - selection = nil - projection = nil - else - selection = extract_references(new_references) - projection = nil - end - - db_relation(selection, projection).find_each do |record| - process_db_record!(record) - end - end - - # Return a Rails relation or array that will be used to obtain the records we need to load from the DB - # - # @param selection [Hash] A selection hash resulting in Select operation (in Relation algebra terms) - # @param projection [Array] A projection array resulting in Project operation (in Relation algebra terms) - def db_relation(selection = nil, projection = nil) - relation = if !custom_db_finder.blank? - custom_db_finder.call(self, selection, projection) - else - rel = if !parent.nil? && !association.nil? - parent.send(association) - elsif !arel.nil? - arel - end - rel = rel.where(build_multi_selection_condition(selection)) if rel && selection - rel = rel.select(projection) if rel && projection - rel - end - - relation || model_class.none - end - - # Extracting references to a relation friendly format, or a format processable by a custom_db_finder - # - # @param new_references [Array] array of manager_uuids of the InventoryObjects - def extract_references(new_references = []) - hash_uuids_by_ref = [] - - new_references.each do |manager_uuid| - next if manager_uuid.nil? - uuids = manager_uuid.split(stringify_joiner) - - reference = {} - manager_ref.each_with_index do |ref, index| - reference[ref] = uuids[index] - end - hash_uuids_by_ref << reference - end - hash_uuids_by_ref - end - - # Takes ApplicationRecord record, converts it to the InventoryObject and places it to db_data_index - # - # @param record [ApplicationRecord] ApplicationRecord record we want to place to the db_data_index - def process_db_record!(record) - index = if custom_manager_uuid.nil? - object_index(record) - else - stringify_reference(custom_manager_uuid.call(record)) - end - - attributes = record.attributes.symbolize_keys - attribute_references.each do |ref| - # We need to fill all references that are relations, we will use a ManagerRefresh::ApplicationRecordReference which - # can be used for filling a relation and we don't need to do any query here - # TODO(lsmola) maybe loading all, not just referenced here? Otherwise this will have issue for db_cache_all - # and find used in parser - next unless (foreign_key = association_to_foreign_key_mapping[ref]) - base_class_name = attributes[association_to_foreign_type_mapping[ref].try(:to_sym)] || association_to_base_class_mapping[ref] - id = attributes[foreign_key.to_sym] - attributes[ref] = ManagerRefresh::ApplicationRecordReference.new(base_class_name, id) - end - - db_data_index[index] = new_inventory_object(attributes) - db_data_index[index].id = record.id - end - def validate_inventory_collection! if @strategy == :local_db_cache_all if (manager_ref & association_attributes).present? diff --git a/app/models/manager_refresh/inventory_collection/data_storage.rb b/app/models/manager_refresh/inventory_collection/data_storage.rb new file mode 100644 index 00000000000..e0e97944454 --- /dev/null +++ b/app/models/manager_refresh/inventory_collection/data_storage.rb @@ -0,0 +1,128 @@ +module ManagerRefresh + class InventoryCollection + class DataStorage + include Vmdb::Logging + + # @return [Array] objects of the InventoryCollection in an Array + attr_accessor :data + + attr_reader :index_proxy, :inventory_collection + + delegate :each, :size, :to => :data + + delegate :find, + :primary_index, + :build_primary_index_for, + :build_secondary_indexes_for, + :to => :index_proxy + + delegate :builder_params, + :inventory_object?, + :inventory_object_lazy?, + :manager_ref, + :new_inventory_object, + :to => :inventory_collection + + def initialize(inventory_collection, secondary_refs) + @inventory_collection = inventory_collection + @data = [] + + @index_proxy = ManagerRefresh::InventoryCollection::Index::Proxy.new(inventory_collection, secondary_refs) + end + + def <<(inventory_object) + unless primary_index.find(inventory_object.manager_uuid) + data << inventory_object + + # TODO(lsmola) Maybe we do not need the secondary indexes here? + # Maybe we should index it like LocalDb indexes, on demand, and storing what was + # indexed? Maybe we should allow only lazy access and no direct find from a parser. Since for streaming + # refresh, things won't be parsed together and no full state will be taken. + build_primary_index_for(inventory_object) + build_secondary_indexes_for(inventory_object) + end + inventory_collection + end + + alias push << + + def find_or_build(manager_uuid) + raise "The uuid consists of #{manager_ref.size} attributes, please find_or_build_by method" if manager_ref.size > 1 + + find_or_build_by(manager_ref.first => manager_uuid) + end + + def find_or_build_by(manager_uuid_hash) + if !manager_uuid_hash.keys.all? { |x| manager_ref.include?(x) } || manager_uuid_hash.keys.size != manager_ref.size + raise "Allowed find_or_build_by keys are #{manager_ref}" + end + + # Not using find by since if could take record from db, then any changes would be ignored, since such record will + # not be stored to DB, maybe we should rethink this? + primary_index.find(manager_uuid_hash) || build(manager_uuid_hash) + end + + def build(hash) + hash = builder_params.merge(hash) + inventory_object = new_inventory_object(hash) + + uuid = inventory_object.manager_uuid + # Each InventoryObject must be able to build an UUID, return nil if it can't + return nil if uuid.blank? + # Return existing InventoryObject if we have it + return primary_index.find(uuid) if primary_index.find(uuid) + # Store new InventoryObject and return it + push(inventory_object) + inventory_object + end + + def to_a + data + end + + # Import/export methods + def from_raw_data(inventory_objects_data, available_inventory_collections) + inventory_objects_data.each do |inventory_object_data| + hash = inventory_object_data.each_with_object({}) do |(key, value), result| + result[key.to_sym] = if value.kind_of?(Array) + value.map { |x| from_raw_value(x, available_inventory_collections) } + else + from_raw_value(value, available_inventory_collections) + end + end + build(hash) + end + end + + def from_raw_value(value, available_inventory_collections) + if value.kind_of?(Hash) && (value['type'] || value[:type]) == "ManagerRefresh::InventoryObjectLazy" + value.transform_keys!(&:to_s) + end + + if value.kind_of?(Hash) && value['type'] == "ManagerRefresh::InventoryObjectLazy" + inventory_collection = available_inventory_collections[value['inventory_collection_name'].try(:to_sym)] + raise "Couldn't build lazy_link #{value} the inventory_collection_name was not found" if inventory_collection.blank? + inventory_collection.lazy_find(value['ems_ref'], :key => value['key'], :default => value['default']) + else + value + end + end + + def to_raw_data + data.map do |inventory_object| + inventory_object.data.transform_values do |value| + if inventory_object_lazy?(value) + value.to_raw_lazy_relation + elsif value.kind_of?(Array) && (inventory_object_lazy?(value.compact.first) || inventory_object?(value.compact.first)) + value.compact.map(&:to_raw_lazy_relation) + elsif inventory_object?(value) + value.to_raw_lazy_relation + else + value + end + end + end + end + end + end +end diff --git a/app/models/manager_refresh/inventory_collection/index/proxy.rb b/app/models/manager_refresh/inventory_collection/index/proxy.rb new file mode 100644 index 00000000000..6783dd2cd4c --- /dev/null +++ b/app/models/manager_refresh/inventory_collection/index/proxy.rb @@ -0,0 +1,164 @@ +module ManagerRefresh + class InventoryCollection + module Index + class Proxy + include Vmdb::Logging + + def initialize(inventory_collection, secondary_refs = {}) + @inventory_collection = inventory_collection + + @primary_ref = {:manager_ref => @inventory_collection.manager_ref} + @secondary_refs = secondary_refs + @all_refs = @primary_ref.merge(@secondary_refs) + + @data_indexes = {} + @local_db_indexes = {} + + @all_refs.each do |index_name, attribute_names| + @data_indexes[index_name] = ManagerRefresh::InventoryCollection::Index::Type::Data.new( + inventory_collection, + attribute_names + ) + + @local_db_indexes[index_name] = ManagerRefresh::InventoryCollection::Index::Type::LocalDb.new( + inventory_collection, + attribute_names, + @data_indexes[index_name] + ) + end + end + + def build_primary_index_for(inventory_object) + # Building the object, we need to provide all keys of a primary index + assert_index(inventory_object.data, primary_index_ref) + primary_index.store_index_for(inventory_object) + end + + def build_secondary_indexes_for(inventory_object) + secondary_refs.keys.each do |ref| + data_index(ref).store_index_for(inventory_object) + end + end + + def reindex_secondary_indexes! + data_indexes.each do |ref, index| + next if ref == primary_index_ref + + index.reindex! + end + end + + def primary_index_ref + :manager_ref + end + + def primary_index + data_index(primary_index_ref) + end + + def find(manager_uuid, ref: :manager_ref) + # TODO(lsmola) lazy_find will support only hash, then we can remove the _by variant + return if manager_uuid.nil? + + manager_uuid = stringify_index_value(manager_uuid, ref) + + return unless assert_index(manager_uuid, ref) + + case strategy + when :local_db_find_references, :local_db_cache_all + local_db_index(ref).find(manager_uuid) + when :local_db_find_missing_references + data_index(ref).find(manager_uuid) || local_db_index(ref).find(manager_uuid) + else + data_index(ref).find(manager_uuid) + end + end + + def find_by(manager_uuid_hash, ref: :manager_ref) + # TODO(lsmola) deprecate this, it's enough to have find method + find(manager_uuid_hash, :ref => ref) + end + + def lazy_find_by(manager_uuid_hash, ref: :manager_ref, key: nil, default: nil) + # TODO(lsmola) deprecate this, it's enough to have lazy_find method + + lazy_find(manager_uuid_hash, :ref => ref, :key => key, :default => default) + end + + def lazy_find(manager_uuid, ref: :manager_ref, key: nil, default: nil) + # TODO(lsmola) also, it should be enough to have only 1 find method, everything can be lazy, until we try to + # access the data + # TODO(lsmola) lazy_find will support only hash, then we can remove the _by variant + return if manager_uuid.nil? + return unless assert_index(manager_uuid, ref) + + ::ManagerRefresh::InventoryObjectLazy.new(inventory_collection, + stringify_index_value(manager_uuid, ref), + manager_uuid, + :ref => ref, :key => key, :default => default) + end + + private + + delegate :strategy, :hash_index_with_keys, :to => :inventory_collection + + attr_reader :all_refs, :data_indexes, :inventory_collection, :primary_ref, :local_db_indexes, :secondary_refs + + def stringify_index_value(index_value, ref) + # TODO(lsmola) !!!!!!!!!! Important, move this inside of the index. We should be passing around a full hash + # index. Then all references should be turned into {stringified_index => full_index} hash. So that way, we can + # keep fast indexing using string, but we can use references to write queries autmatically (targeted, + # db_based, etc.) + # We can also save {stringified_index => full_index}, so we don't have to compute it twice. + if index_value.kind_of?(Hash) + hash_index_with_keys(named_ref(ref), index_value) + else + # TODO(lsmola) raise deprecation warning, we want to use only hash indexes + index_value + end + end + + def data_index(name) + data_indexes[name] || raise("Index #{name} not defined for #{inventory_collection}") + end + + def local_db_index(name) + local_db_indexes[name] || raise("Index #{name} not defined for #{inventory_collection}") + end + + def named_ref(ref) + all_refs[ref] + end + + def missing_keys(data_keys, ref) + named_ref(ref) - data_keys + end + + def required_index_keys_present?(data_keys, ref) + missing_keys(data_keys, ref).empty? + end + + def assert_index(manager_uuid, ref) + if manager_uuid.kind_of?(Hash) + # Test we are sending all keys required for the index + unless required_index_keys_present?(manager_uuid.keys, ref) + missing_keys = missing_keys(manager_uuid.keys, ref) + + if !Rails.env.production? + raise "Invalid index for '#{inventory_collection}' using #{manager_uuid}. Missing keys for index #{ref} are #{missing_keys}" + else + _log.error("Invalid index for '#{inventory_collection}' using #{manager_uuid}. Missing keys for index #{ref} are #{missing_keys}") + return false + end + end + end + + true + rescue => e + _log.error("Error when asserting index: #{manager_uuid}, with ref: #{ref} of #{inventory_collection}") + raise e + end + end + end + end +end diff --git a/app/models/manager_refresh/inventory_collection/index/type/base.rb b/app/models/manager_refresh/inventory_collection/index/type/base.rb new file mode 100644 index 00000000000..0c2760ac4c2 --- /dev/null +++ b/app/models/manager_refresh/inventory_collection/index/type/base.rb @@ -0,0 +1,48 @@ +module ManagerRefresh + class InventoryCollection + module Index + module Type + class Base + include Vmdb::Logging + + def initialize(inventory_collection, attribute_names, *_args) + @index = {} + + @inventory_collection = inventory_collection + @attribute_names = attribute_names + end + + delegate :keys, :to => :index + + def store_index_for(inventory_object) + index[inventory_object.manager_uuid(attribute_names)] = inventory_object + end + + def reindex! + self.index = {} + data.each do |inventory_object| + store_index_for(inventory_object) + end + end + + # Find value based on index_value + # + # @param _index_value [String] a index_value of the InventoryObject we search for + def find(_index_value) + raise "Implement in subclass" + end + + protected + + attr_reader :attribute_names, :index, :inventory_collection + + private + + attr_writer :index + + delegate :data, :to => :inventory_collection + end + end + end + end +end diff --git a/app/models/manager_refresh/inventory_collection/index/type/data.rb b/app/models/manager_refresh/inventory_collection/index/type/data.rb new file mode 100644 index 00000000000..af5c7505483 --- /dev/null +++ b/app/models/manager_refresh/inventory_collection/index/type/data.rb @@ -0,0 +1,16 @@ +module ManagerRefresh + class InventoryCollection + module Index + module Type + class Data < ManagerRefresh::InventoryCollection::Index::Type::Base + # Find value based on index_value + # + # @param index_value [String] a index_value of the InventoryObject we search in data + def find(index_value) + index[index_value] + end + end + end + end + end +end diff --git a/app/models/manager_refresh/inventory_collection/index/type/local_db.rb b/app/models/manager_refresh/inventory_collection/index/type/local_db.rb new file mode 100644 index 00000000000..888be570f27 --- /dev/null +++ b/app/models/manager_refresh/inventory_collection/index/type/local_db.rb @@ -0,0 +1,162 @@ +module ManagerRefresh + class InventoryCollection + module Index + module Type + class LocalDb < ManagerRefresh::InventoryCollection::Index::Type::Base + def initialize(inventory_collection, attribute_names, data_index) + super + + @index = nil + @loaded_references = Set.new + @data_index = data_index + end + + # Finds index_value in the DB. Using a configured strategy we cache obtained data in the index, so the + # same find will not hit database twice. Also if we use lazy_links and this is called when + # data_collection_finalized?, we load all data from the DB, referenced by lazy_links, in one query. + # + # @param index_value [String] a index_value of the InventoryObject we search in the local DB + def find(index_value) + # Use the cached index only data_collection_finalized?, meaning no new reference can occur + if data_collection_finalized? && index + return index[index_value] + else + return index[index_value] if index && index[index_value] + # We haven't found the reference, lets add it to the list of references and load it + references << index_value if index_value + end + + # Put our existing data_index keys into loaded references + loaded_references.merge(data_index.keys) + # Load the rest of the references from the DB + populate_index! + + index[index_value] + end + + private + + attr_reader :data_index, :loaded_references + attr_writer :index + + delegate :arel, + :association, + :association_to_base_class_mapping, + :association_to_foreign_key_mapping, + :association_to_foreign_type_mapping, + :attribute_references, + :build_multi_selection_condition, + :custom_manager_uuid, + :custom_db_finder, + :data_collection_finalized?, + :db_relation, + :model_class, + :new_inventory_object, + :parent, + :references, + :strategy, + :stringify_joiner, + :stringify_reference, + :to => :inventory_collection + + # Fills index with InventoryObjects obtained from the DB + def populate_index! + # Load only new references from the DB + new_references = references - loaded_references + # And store which references we've already loaded + loaded_references.merge(new_references) + + # Initialize index in nil + self.index ||= {} + + return if new_references.blank? # Return if all references are already loaded + + # TODO(lsmola) selected need to contain also :keys used in other InventoryCollections pointing to this one, once + # we get list of all keys for each InventoryCollection ,we can uncomnent + # selected = [:id] + attribute_names.map { |x| model_class.reflect_on_association(x).try(:foreign_key) || x } + # selected << :type if model_class.new.respond_to? :type + # load_from_db.select(selected).find_each do |record| + + # Return the the correct relation based on strategy and selection&projection + selection = extract_references(new_references) unless strategy == :local_db_cache_all + projection = nil + + db_relation(selection, projection).find_each do |record| + process_db_record!(record) + end + end + + # Return a Rails relation or array that will be used to obtain the records we need to load from the DB + # + # @param selection [Hash] A selection hash resulting in Select operation (in Relation algebra terms) + # @param projection [Array] A projection array resulting in Project operation (in Relation algebra terms) + def db_relation(selection = nil, projection = nil) + relation = if !custom_db_finder.blank? + custom_db_finder.call(self, selection, projection) + else + rel = if !parent.nil? && !association.nil? + parent.send(association) + elsif !arel.nil? + arel + end + rel = rel.where(build_multi_selection_condition(selection)) if rel && selection + rel = rel.select(projection) if rel && projection + rel + end + + relation || model_class.none + end + + # Extracting references to a relation friendly format, or a format processable by a custom_db_finder + # + # @param new_references [Array] array of index_values of the InventoryObjects + def extract_references(new_references = []) + hash_uuids_by_ref = [] + + new_references.each do |index_value| + next if index_value.nil? + # TODO(lsmola) no need when hashes are the original hashes + uuids = index_value.split(stringify_joiner) + + reference = {} + attribute_names.each_with_index do |ref, uuid_value| + reference[ref] = uuids[uuid_value] + end + hash_uuids_by_ref << reference + end + hash_uuids_by_ref + end + + # Takes ApplicationRecord record, converts it to the InventoryObject and places it to index + # + # @param record [ApplicationRecord] ApplicationRecord record we want to place to the index + def process_db_record!(record) + # TODO(lsmola) rethink this. If references will be the full Hash references, we can construct this automatically + index_value = if custom_manager_uuid.nil? + inventory_collection.object_index_with_keys(attribute_names, record) + else + # TODO(lsmola) hm this will not really work for the secondary indexes anyway + stringify_reference(custom_manager_uuid.call(record)) + end + + attributes = record.attributes.symbolize_keys + attribute_references.each do |ref| + # We need to fill all references that are relations, we will use a ManagerRefresh::ApplicationRecordReference which + # can be used for filling a relation and we don't need to do any query here. + # TODO(lsmola) maybe loading all, not just referenced here? Otherwise this will have issue for db_cache_all + # and find used in parser + # TODO(lsmola) the last usage of this should be lazy_find_by with :key specified, maybe we can get rid of this? + next unless (foreign_key = association_to_foreign_key_mapping[ref]) + base_class_name = attributes[association_to_foreign_type_mapping[ref].try(:to_sym)] || association_to_base_class_mapping[ref] + id = attributes[foreign_key.to_sym] + attributes[ref] = ManagerRefresh::ApplicationRecordReference.new(base_class_name, id) + end + + index[index_value] = new_inventory_object(attributes) + index[index_value].id = record.id + end + end + end + end + end +end diff --git a/app/models/manager_refresh/inventory_object.rb b/app/models/manager_refresh/inventory_object.rb index cb4b3414268..2ad837115d0 100644 --- a/app/models/manager_refresh/inventory_object.rb +++ b/app/models/manager_refresh/inventory_object.rb @@ -3,7 +3,7 @@ class InventoryObject attr_accessor :object, :id attr_reader :inventory_collection, :data - delegate :manager_ref, :base_class_name, :model_class, :to => :inventory_collection + delegate :manager_ref, :base_class_name, :model_class, :hash_index_with_keys, :to => :inventory_collection delegate :[], :[]=, :to => :data def initialize(inventory_collection, data) @@ -14,14 +14,8 @@ def initialize(inventory_collection, data) @allowed_attributes_index = nil end - def manager_uuid - # TODO(lsmola) should we have a separate function for uuid containing foreign keys? Probably yes, since it could - # speed up the ID fetching. - id_with_keys(manager_ref) - end - - def id_with_keys(keys) - keys.map { |attribute| data[attribute].try(:id) || data[attribute].to_s }.join("__") + def manager_uuid(keys = manager_ref) + hash_index_with_keys(keys, data) end def to_raw_lazy_relation diff --git a/app/models/manager_refresh/inventory_object_lazy.rb b/app/models/manager_refresh/inventory_object_lazy.rb index ccfa317f3e4..7586d313f13 100644 --- a/app/models/manager_refresh/inventory_object_lazy.rb +++ b/app/models/manager_refresh/inventory_object_lazy.rb @@ -5,9 +5,10 @@ class InventoryObjectLazy attr_reader :ems_ref, :ref, :inventory_collection, :key, :default # TODO: ems_ref is inaccurate name, doubly so if it depends on ref. - def initialize(inventory_collection, ems_ref, ref: :manager_ref, key: nil, default: nil) + def initialize(inventory_collection, ems_ref, full_index, ref: :manager_ref, key: nil, default: nil) @ems_ref = ems_ref @ref = ref + @full_index = full_index @inventory_collection = inventory_collection @key = key @default = default diff --git a/app/models/manager_refresh/save_collection/saver/base.rb b/app/models/manager_refresh/save_collection/saver/base.rb index 5a5bd18a142..0babe507cb8 100644 --- a/app/models/manager_refresh/save_collection/saver/base.rb +++ b/app/models/manager_refresh/save_collection/saver/base.rb @@ -78,7 +78,7 @@ def save!(association) inventory_objects_index = {} inventory_collection.each do |inventory_object| attributes = inventory_object.attributes(inventory_collection) - index = inventory_object.manager_uuid + index = inventory_collection.hash_index_with_keys(unique_index_keys, attributes) attributes_index[index] = attributes inventory_objects_index[index] = inventory_object diff --git a/app/models/manager_refresh/save_collection/saver/concurrent_safe_batch.rb b/app/models/manager_refresh/save_collection/saver/concurrent_safe_batch.rb index fdbf5b23809..170af1b6ec0 100644 --- a/app/models/manager_refresh/save_collection/saver/concurrent_safe_batch.rb +++ b/app/models/manager_refresh/save_collection/saver/concurrent_safe_batch.rb @@ -51,8 +51,7 @@ def save!(association) inventory_collection.each do |inventory_object| attributes = inventory_object.attributes_with_keys(inventory_collection, all_attribute_keys) - # TODO(lsmola) unify this behavior with object_index_with_keys method in InventoryCollection - index = unique_index_keys.map { |key| attributes[key].to_s }.join(inventory_collection.stringify_joiner) + index = inventory_collection.hash_index_with_keys(unique_index_keys, attributes) # Interesting fact: not building attributes_index and using only inventory_objects_index doesn't do much # of a difference, since the most objects inside are shared.