inventory_refresh 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +47 -0
- data/.gitignore +13 -0
- data/.rspec +4 -0
- data/.rspec_ci +4 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_cc.yml +5 -0
- data/.rubocop_local.yml +2 -0
- data/.travis.yml +12 -0
- data/.yamllint +12 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +6 -0
- data/LICENSE +202 -0
- data/README.md +35 -0
- data/Rakefile +47 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/inventory_refresh.gemspec +34 -0
- data/lib/inventory_refresh.rb +11 -0
- data/lib/inventory_refresh/application_record_iterator.rb +56 -0
- data/lib/inventory_refresh/application_record_reference.rb +15 -0
- data/lib/inventory_refresh/graph.rb +157 -0
- data/lib/inventory_refresh/graph/topological_sort.rb +66 -0
- data/lib/inventory_refresh/inventory_collection.rb +1175 -0
- data/lib/inventory_refresh/inventory_collection/data_storage.rb +178 -0
- data/lib/inventory_refresh/inventory_collection/graph.rb +170 -0
- data/lib/inventory_refresh/inventory_collection/index/proxy.rb +230 -0
- data/lib/inventory_refresh/inventory_collection/index/type/base.rb +80 -0
- data/lib/inventory_refresh/inventory_collection/index/type/data.rb +26 -0
- data/lib/inventory_refresh/inventory_collection/index/type/local_db.rb +286 -0
- data/lib/inventory_refresh/inventory_collection/index/type/skeletal.rb +116 -0
- data/lib/inventory_refresh/inventory_collection/reference.rb +96 -0
- data/lib/inventory_refresh/inventory_collection/references_storage.rb +106 -0
- data/lib/inventory_refresh/inventory_collection/scanner.rb +117 -0
- data/lib/inventory_refresh/inventory_collection/serialization.rb +140 -0
- data/lib/inventory_refresh/inventory_object.rb +303 -0
- data/lib/inventory_refresh/inventory_object_lazy.rb +151 -0
- data/lib/inventory_refresh/save_collection/base.rb +38 -0
- data/lib/inventory_refresh/save_collection/recursive.rb +52 -0
- data/lib/inventory_refresh/save_collection/saver/base.rb +390 -0
- data/lib/inventory_refresh/save_collection/saver/batch.rb +17 -0
- data/lib/inventory_refresh/save_collection/saver/concurrent_safe.rb +71 -0
- data/lib/inventory_refresh/save_collection/saver/concurrent_safe_batch.rb +632 -0
- data/lib/inventory_refresh/save_collection/saver/default.rb +57 -0
- data/lib/inventory_refresh/save_collection/saver/sql_helper.rb +85 -0
- data/lib/inventory_refresh/save_collection/saver/sql_helper_update.rb +120 -0
- data/lib/inventory_refresh/save_collection/saver/sql_helper_upsert.rb +196 -0
- data/lib/inventory_refresh/save_collection/topological_sort.rb +38 -0
- data/lib/inventory_refresh/save_inventory.rb +38 -0
- data/lib/inventory_refresh/target.rb +73 -0
- data/lib/inventory_refresh/target_collection.rb +80 -0
- data/lib/inventory_refresh/version.rb +3 -0
- data/tools/ci/create_db_user.sh +3 -0
- metadata +207 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "inventory_refresh/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "inventory_refresh"
|
8
|
+
spec.version = InventoryRefresh::VERSION
|
9
|
+
spec.authors = ["ManageIQ Developers"]
|
10
|
+
|
11
|
+
spec.summary = %q{Topological Inventory Persister}
|
12
|
+
spec.description = %q{Topological Inventory Persister}
|
13
|
+
spec.homepage = "https://github.com/ManageIQ/inventory_refresh"
|
14
|
+
spec.licenses = ["Apache-2.0"]
|
15
|
+
|
16
|
+
# Specify which files should be added to the gem when it is released.
|
17
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
end
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_dependency "activerecord", "~> 5.0.6"
|
26
|
+
spec.add_dependency "more_core_extensions", "~> 3.5"
|
27
|
+
spec.add_dependency "pg", "~> 0.18.2"
|
28
|
+
|
29
|
+
spec.add_development_dependency "ancestry"
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
31
|
+
spec.add_development_dependency "factory_girl", "~> 4.5.0"
|
32
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
34
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require "inventory_refresh/graph"
|
2
|
+
require "inventory_refresh/inventory_collection"
|
3
|
+
require "inventory_refresh/inventory_object"
|
4
|
+
require "inventory_refresh/inventory_object_lazy"
|
5
|
+
require "inventory_refresh/save_inventory"
|
6
|
+
require "inventory_refresh/target"
|
7
|
+
require "inventory_refresh/target_collection"
|
8
|
+
require "inventory_refresh/version"
|
9
|
+
|
10
|
+
module InventoryRefresh
|
11
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module InventoryRefresh
|
2
|
+
class ApplicationRecordIterator
|
3
|
+
attr_reader :inventory_collection, :manager_uuids_set, :iterator, :query
|
4
|
+
|
5
|
+
# An iterator that can fetch batches of the AR objects based on a set of manager refs, or just mimics AR relation
|
6
|
+
# when given an iterator. Or given query, acts as iterator by selecting batches.
|
7
|
+
#
|
8
|
+
# @param inventory_collection [InventoryRefresh::InventoryCollection] Inventory collection owning the iterator
|
9
|
+
# @param manager_uuids_set [Array<InventoryRefresh::InventoryCollection::Reference>] Array of references we want to
|
10
|
+
# fetch from the DB
|
11
|
+
# @param iterator [Proc] Block based iterator
|
12
|
+
# @query query [ActiveRecord::Relation] Existing query we want to use for querying the db
|
13
|
+
def initialize(inventory_collection: nil, manager_uuids_set: nil, iterator: nil, query: nil)
|
14
|
+
@inventory_collection = inventory_collection
|
15
|
+
@manager_uuids_set = manager_uuids_set
|
16
|
+
@iterator = iterator
|
17
|
+
@query = query
|
18
|
+
end
|
19
|
+
|
20
|
+
# Iterator that mimics find_in_batches of ActiveRecord::Relation. This iterator serves for making more optimized query
|
21
|
+
# since e.g. having 1500 ids if objects we want to return. Doing relation.where(:id => 1500ids).find_each would
|
22
|
+
# always search for all 1500 ids, then return on limit 1000.
|
23
|
+
#
|
24
|
+
# With this iterator we build queries using only batch of ids, so find_each will cause relation.where(:id => 1000ids)
|
25
|
+
# and relation.where(:id => 500ids)
|
26
|
+
#
|
27
|
+
# @param batch_size [Integer] A batch size we want to fetch from DB
|
28
|
+
# @yield Code processing the batches
|
29
|
+
def find_in_batches(batch_size: 1000)
|
30
|
+
if iterator
|
31
|
+
iterator.call do |batch|
|
32
|
+
yield(batch)
|
33
|
+
end
|
34
|
+
elsif query
|
35
|
+
manager_uuids_set.each_slice(batch_size) do |batch|
|
36
|
+
yield(query.where(inventory_collection.targeted_selection_for(batch)))
|
37
|
+
end
|
38
|
+
else
|
39
|
+
manager_uuids_set.each_slice(batch_size) do |batch|
|
40
|
+
yield(inventory_collection.db_collection_for_comparison_for(batch))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Iterator that mimics find_each of ActiveRecord::Relation using find_in_batches (see #find_in_batches)
|
46
|
+
#
|
47
|
+
# @yield Code processing the batches
|
48
|
+
def find_each
|
49
|
+
find_in_batches do |batch|
|
50
|
+
batch.each do |item|
|
51
|
+
yield(item)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module InventoryRefresh
|
2
|
+
class ApplicationRecordReference
|
3
|
+
attr_reader :base_class_name, :id
|
4
|
+
|
5
|
+
# ApplicationRecord is very bloaty in memory, so this class server for storing base_class and primary key
|
6
|
+
# of the ApplicationRecord, which is just enough for filling up relationships
|
7
|
+
#
|
8
|
+
# @param base_class_name [String] A base class of the ApplicationRecord object
|
9
|
+
# @param id [Bigint] Primary key value of the ApplicationRecord object
|
10
|
+
def initialize(base_class_name, id)
|
11
|
+
@base_class_name = base_class_name
|
12
|
+
@id = id
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require "inventory_refresh/graph/topological_sort"
|
2
|
+
|
3
|
+
module InventoryRefresh
|
4
|
+
class Graph
|
5
|
+
attr_reader :nodes, :edges, :fixed_edges
|
6
|
+
|
7
|
+
# @param nodes [Array<InventoryRefresh::InventoryCollection>] List of Inventory collection nodes
|
8
|
+
def initialize(nodes)
|
9
|
+
@nodes = nodes
|
10
|
+
@edges = []
|
11
|
+
@fixed_edges = []
|
12
|
+
|
13
|
+
construct_graph!(@nodes)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns graph in GraphViz format, as a string. So it can be displayed.
|
17
|
+
#
|
18
|
+
# @param layers [Array<Array>] Array of arrays(layers) of InventoryCollection objects
|
19
|
+
# @return [String] Graph in GraphViz format
|
20
|
+
def to_graphviz(layers: nil)
|
21
|
+
node_names = friendly_unique_node_names
|
22
|
+
s = []
|
23
|
+
|
24
|
+
s << "digraph {"
|
25
|
+
(layers || [nodes]).each_with_index do |layer_nodes, i|
|
26
|
+
s << " subgraph cluster_#{i} { label = \"Layer #{i}\";" unless layers.nil?
|
27
|
+
|
28
|
+
layer_nodes.each do |n|
|
29
|
+
s << " #{node_names[n]}; \t// #{n.inspect}"
|
30
|
+
end
|
31
|
+
|
32
|
+
s << " }" unless layers.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
s << " // edges:"
|
36
|
+
edges.each do |from, to|
|
37
|
+
s << " #{node_names[from]} -> #{node_names[to]};"
|
38
|
+
end
|
39
|
+
s << "}"
|
40
|
+
s.join("\n") + "\n"
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
attr_writer :nodes, :edges, :fixed_edges
|
46
|
+
|
47
|
+
# Given array of InventoryCollection objects as nodes, we construct a graph with nodes and edges
|
48
|
+
#
|
49
|
+
# @param nodes [Array<InventoryRefresh::InventoryCollection>] List of Inventory collection nodes
|
50
|
+
# @return [InventoryRefresh::Graph] Constructed graph
|
51
|
+
def construct_graph!(nodes)
|
52
|
+
self.nodes = nodes
|
53
|
+
self.edges, self.fixed_edges = build_edges(nodes)
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
# Checks that there are no cycles in the graph
|
58
|
+
#
|
59
|
+
# @param fixed_edges [Array<Array>] List of edges, where edge is defined as [InventoryCollection, InventoryCollection],
|
60
|
+
# fixed edges are those that can't be moved
|
61
|
+
def assert_graph!(fixed_edges)
|
62
|
+
fixed_edges.each do |edge|
|
63
|
+
detect_cycle(edge, fixed_edges - [edge], :exception)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Builds a feedback edge set, which is a set of edges creating a cycle
|
68
|
+
#
|
69
|
+
# @param edges [Array<Array>] List of edges, where edge is defined as [InventoryCollection, InventoryCollection],
|
70
|
+
# these are all edges except fixed_edges
|
71
|
+
# @param fixed_edges [Array<Array>] List of edges, where edge is defined as [InventoryCollection, InventoryCollection],
|
72
|
+
# fixed edges are those that can't be moved
|
73
|
+
def build_feedback_edge_set(edges, fixed_edges)
|
74
|
+
edges = edges.dup
|
75
|
+
acyclic_edges = fixed_edges.dup
|
76
|
+
feedback_edge_set = []
|
77
|
+
|
78
|
+
while edges.present?
|
79
|
+
edge = edges.shift
|
80
|
+
if detect_cycle(edge, acyclic_edges)
|
81
|
+
feedback_edge_set << edge
|
82
|
+
else
|
83
|
+
acyclic_edges << edge
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
feedback_edge_set
|
88
|
+
end
|
89
|
+
|
90
|
+
# Detects a cycle. Based on escalation returns true or raises exception if there is a cycle
|
91
|
+
#
|
92
|
+
# @param edge [Array(InventoryRefresh::InventoryCollection, InventoryRefresh::InventoryCollection)] Edge we are
|
93
|
+
# inspecting for cycle
|
94
|
+
# @param acyclic_edges [Array<Array>] Starting with fixed edges that can't have cycle, these are edges without cycle
|
95
|
+
# @param escalation [Symbol] If :exception, this method throws exception when it finds a cycle
|
96
|
+
# @return [Boolean, Exception] Based on escalation returns true or raises exception if there is a cycle
|
97
|
+
def detect_cycle(edge, acyclic_edges, escalation = nil)
|
98
|
+
# Test if adding edge creates a cycle, ew will traverse the graph from edge Node, through all it's
|
99
|
+
# dependencies
|
100
|
+
starting_node = edge.second
|
101
|
+
edges = [edge] + acyclic_edges
|
102
|
+
traverse_dependecies([], starting_node, starting_node, edges, node_edges(edges, starting_node), escalation)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Recursive method for traversing dependencies and finding a cycle
|
106
|
+
#
|
107
|
+
# @param traversed_nodes [Array<InventoryRefresh::InventoryCollection> Already traversed nodes
|
108
|
+
# @param starting_node [InventoryRefresh::InventoryCollection] Node we've started the traversal on
|
109
|
+
# @param current_node [InventoryRefresh::InventoryCollection] Node we are currently on
|
110
|
+
# @param edges [Array<Array>] All graph edges
|
111
|
+
# @param dependencies [Array<InventoryRefresh::InventoryCollection> Dependencies of the current node
|
112
|
+
# @param escalation [Symbol] If :exception, this method throws exception when it finds a cycle
|
113
|
+
# @return [Boolean, Exception] Based on escalation returns true or raises exception if there is a cycle
|
114
|
+
def traverse_dependecies(traversed_nodes, starting_node, current_node, edges, dependencies, escalation)
|
115
|
+
dependencies.each do |node_edge|
|
116
|
+
node = node_edge.first
|
117
|
+
traversed_nodes << node
|
118
|
+
if traversed_nodes.include?(starting_node)
|
119
|
+
if escalation == :exception
|
120
|
+
raise "Cycle from #{current_node} to #{node}, starting from #{starting_node} passing #{traversed_nodes}"
|
121
|
+
else
|
122
|
+
return true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
return true if traverse_dependecies(traversed_nodes, starting_node, node, edges, node_edges(edges, node), escalation)
|
126
|
+
end
|
127
|
+
|
128
|
+
false
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns dependencies of the node, i.e. nodes that are connected by edge
|
132
|
+
#
|
133
|
+
# @param edges [Array<Array>] All graph edges
|
134
|
+
# @param node [InventoryRefresh::InventoryCollection] Node we are inspecting
|
135
|
+
# @return [Array<InventoryRefresh::InventoryCollection>] Nodes that are connected to the inspected node
|
136
|
+
def node_edges(edges, node)
|
137
|
+
edges.select { |e| e.second == node }
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns Hash of {node => name}, appending numbers if needed to make unique, quoted if needed. Used for the
|
141
|
+
# GraphViz format
|
142
|
+
#
|
143
|
+
# @return [Hash] Hash of {node => name}
|
144
|
+
def friendly_unique_node_names
|
145
|
+
node_names = {}
|
146
|
+
# Try to use shorter .name method that InventoryCollection has.
|
147
|
+
nodes.group_by { |n| n.respond_to?(:name) ? n.name.to_s : n.to_s }.each do |base_name, ns|
|
148
|
+
ns.each_with_index do |n, i|
|
149
|
+
name = ns.size == 1 ? base_name : "#{base_name}_#{i}"
|
150
|
+
name = '"' + name.gsub(/["\\]/) { |c| "\\" + c } + '"' unless name =~ /^[A-Za-z0-9_]+$/
|
151
|
+
node_names[n] = name
|
152
|
+
end
|
153
|
+
end
|
154
|
+
node_names
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module InventoryRefresh
|
2
|
+
class Graph
|
3
|
+
class TopologicalSort
|
4
|
+
attr_reader :graph
|
5
|
+
|
6
|
+
# @param graph [InventoryRefresh::Graph] graph object we want to sort
|
7
|
+
def initialize(graph)
|
8
|
+
@graph = graph
|
9
|
+
end
|
10
|
+
|
11
|
+
################################################################################################################
|
12
|
+
# Topological sort of the graph of the DTO collections to find the right order of saving DTO collections and
|
13
|
+
# identify what DTO collections can be saved in parallel.
|
14
|
+
# Does not mutate graph.
|
15
|
+
#
|
16
|
+
# @return [Array<Array>] Array of arrays(layers) of InventoryCollection objects
|
17
|
+
################################################################################################################
|
18
|
+
# The expected input here is the directed acyclic Graph G (inventory_collections), consisting of Vertices(Nodes) V and
|
19
|
+
# Edges E:
|
20
|
+
# G = (V, E)
|
21
|
+
#
|
22
|
+
# The directed edge is defined as (u, v), where u is the dependency of v, i.e. arrow comes from u to v:
|
23
|
+
# (u, v) ∈ E and u,v ∈ V
|
24
|
+
#
|
25
|
+
# S0 is a layer that has no dependencies:
|
26
|
+
# S0 = { v ∈ V ∣ ∀u ∈ V.(u,v) ∉ E}
|
27
|
+
#
|
28
|
+
# Si+1 is a layer whose dependencies are in the sum of the previous layers from i to 0, cannot write
|
29
|
+
# mathematical sum using U in text editor, so there is an alternative format using _(sum)
|
30
|
+
# Si+1 = { v ∈ V ∣ ∀u ∈ V.(u,v) ∈ E → u ∈ _(sum(S0..Si))_ }
|
31
|
+
#
|
32
|
+
# Then each Si can have their Vertices(DTO collections) processed in parallel. This algorithm cannot
|
33
|
+
# identify independent sub-graphs inside of the layers Si, so we can make the processing even more effective.
|
34
|
+
################################################################################################################
|
35
|
+
def topological_sort
|
36
|
+
nodes = graph.nodes.dup
|
37
|
+
edges = graph.edges
|
38
|
+
sets = []
|
39
|
+
i = 0
|
40
|
+
sets[0], nodes = nodes.partition { |v| !edges.detect { |e| e.second == v } }
|
41
|
+
|
42
|
+
max_depth = 1000
|
43
|
+
while nodes.present?
|
44
|
+
i += 1
|
45
|
+
max_depth -= 1
|
46
|
+
if max_depth <= 0
|
47
|
+
message = "Max depth reached while doing topological sort, your graph probably contains a cycle"
|
48
|
+
#$log.error("#{message}:\n#{graph.to_graphviz}")
|
49
|
+
raise "#{message} (see log)"
|
50
|
+
end
|
51
|
+
|
52
|
+
set, nodes = nodes.partition { |v| edges.select { |e| e.second == v }.all? { |e| sets.flatten.include?(e.first) } }
|
53
|
+
if set.blank?
|
54
|
+
message = "Blank dependency set while doing topological sort, your graph probably contains a cycle"
|
55
|
+
#$log.error("#{message}:\n#{graph.to_graphviz}")
|
56
|
+
raise "#{message} (see log)"
|
57
|
+
end
|
58
|
+
|
59
|
+
sets[i] = set
|
60
|
+
end
|
61
|
+
|
62
|
+
sets
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,1175 @@
|
|
1
|
+
require "inventory_refresh/inventory_collection/data_storage"
|
2
|
+
require "inventory_refresh/inventory_collection/index/proxy"
|
3
|
+
require "inventory_refresh/inventory_collection/reference"
|
4
|
+
require "inventory_refresh/inventory_collection/references_storage"
|
5
|
+
require "inventory_refresh/inventory_collection/scanner"
|
6
|
+
require "inventory_refresh/inventory_collection/serialization"
|
7
|
+
|
8
|
+
require "active_support/core_ext/module/delegation"
|
9
|
+
|
10
|
+
module InventoryRefresh
|
11
|
+
# For more usage examples please follow spec examples in
|
12
|
+
# * spec/models/inventory_refresh/save_inventory/single_inventory_collection_spec.rb
|
13
|
+
# * spec/models/inventory_refresh/save_inventory/acyclic_graph_of_inventory_collections_spec.rb
|
14
|
+
# * spec/models/inventory_refresh/save_inventory/graph_of_inventory_collections_spec.rb
|
15
|
+
# * spec/models/inventory_refresh/save_inventory/graph_of_inventory_collections_targeted_refresh_spec.rb
|
16
|
+
# * spec/models/inventory_refresh/save_inventory/strategies_and_references_spec.rb
|
17
|
+
#
|
18
|
+
# @example storing Vm model data into the DB
|
19
|
+
#
|
20
|
+
# @ems = ManageIQ::Providers::BaseManager.first
|
21
|
+
# puts @ems.vms.collect(&:ems_ref) # => []
|
22
|
+
#
|
23
|
+
# # Init InventoryCollection
|
24
|
+
# vms_inventory_collection = ::InventoryRefresh::InventoryCollection.new(
|
25
|
+
# :model_class => ManageIQ::Providers::CloudManager::Vm, :parent => @ems, :association => :vms
|
26
|
+
# )
|
27
|
+
#
|
28
|
+
# # Fill InventoryCollection with data
|
29
|
+
# # Starting with no vms, lets add vm1 and vm2
|
30
|
+
# vms_inventory_collection.build(:ems_ref => "vm1", :name => "vm1")
|
31
|
+
# vms_inventory_collection.build(:ems_ref => "vm2", :name => "vm2")
|
32
|
+
#
|
33
|
+
# # Save InventoryCollection to the db
|
34
|
+
# InventoryRefresh::SaveInventory.save_inventory(@ems, [vms_inventory_collection])
|
35
|
+
#
|
36
|
+
# # The result in the DB is that vm1 and vm2 were created
|
37
|
+
# puts @ems.vms.collect(&:ems_ref) # => ["vm1", "vm2"]
|
38
|
+
#
|
39
|
+
# @example In another refresh, vm1 does not exist anymore and vm3 was added
|
40
|
+
# # Init InventoryCollection
|
41
|
+
# vms_inventory_collection = ::InventoryRefresh::InventoryCollection.new(
|
42
|
+
# :model_class => ManageIQ::Providers::CloudManager::Vm, :parent => @ems, :association => :vms
|
43
|
+
# )
|
44
|
+
#
|
45
|
+
# # Fill InventoryCollection with data
|
46
|
+
# vms_inventory_collection.build(:ems_ref => "vm2", :name => "vm2")
|
47
|
+
# vms_inventory_collection.build(:ems_ref => "vm3", :name => "vm3")
|
48
|
+
#
|
49
|
+
# # Save InventoryCollection to the db
|
50
|
+
# InventoryRefresh::SaveInventory.save_inventory(@ems, [vms_inventory_collection])
|
51
|
+
#
|
52
|
+
# # The result in the DB is that vm1 was deleted, vm2 was updated and vm3 was created
|
53
|
+
# puts @ems.vms.collect(&:ems_ref) # => ["vm2", "vm3"]
|
54
|
+
#
|
55
|
+
class InventoryCollection
|
56
|
+
# @return [Boolean] A true value marks that we collected all the data of the InventoryCollection,
|
57
|
+
# meaning we also collected all the references.
|
58
|
+
attr_accessor :data_collection_finalized
|
59
|
+
|
60
|
+
# @return [InventoryRefresh::InventoryCollection::DataStorage] An InventoryCollection encapsulating all data with
|
61
|
+
# indexes
|
62
|
+
attr_accessor :data_storage
|
63
|
+
|
64
|
+
# @return [Boolean] true if this collection is already saved into the DB. E.g. InventoryCollections with
|
65
|
+
# DB only strategy are marked as saved. This causes InventoryCollection not being a dependency for any other
|
66
|
+
# InventoryCollection, since it is already persisted into the DB.
|
67
|
+
attr_accessor :saved
|
68
|
+
|
69
|
+
# If present, InventoryCollection switches into delete_complement mode, where it will
|
70
|
+
# delete every record from the DB, that is not present in this list. This is used for the batch processing,
|
71
|
+
# where we don't know which InventoryObject should be deleted, but we know all manager_uuids of all
|
72
|
+
# InventoryObject objects that exists in the provider.
|
73
|
+
#
|
74
|
+
# @return [Array, nil] nil or a list of all :manager_uuids that are present in the Provider's InventoryCollection.
|
75
|
+
attr_accessor :all_manager_uuids
|
76
|
+
|
77
|
+
# @return [Set] A set of InventoryCollection objects that depends on this InventoryCollection object.
|
78
|
+
attr_accessor :dependees
|
79
|
+
|
80
|
+
# @return [Array<Symbol>] @see #parent_inventory_collections documentation of InventoryCollection.new kwargs
|
81
|
+
# parameters
|
82
|
+
attr_accessor :parent_inventory_collections
|
83
|
+
|
84
|
+
attr_reader :model_class, :strategy, :attributes_blacklist, :attributes_whitelist, :custom_save_block, :parent,
|
85
|
+
:internal_attributes, :delete_method, :dependency_attributes, :manager_ref, :create_only,
|
86
|
+
:association, :complete, :update_only, :transitive_dependency_attributes, :check_changed, :arel,
|
87
|
+
:inventory_object_attributes, :name, :saver_strategy, :targeted_scope, :default_values,
|
88
|
+
:targeted_arel, :targeted, :manager_ref_allowed_nil, :use_ar_object,
|
89
|
+
:created_records, :updated_records, :deleted_records,
|
90
|
+
:custom_reconnect_block, :batch_extra_attributes, :references_storage
|
91
|
+
|
92
|
+
delegate :<<,
|
93
|
+
:build,
|
94
|
+
:build_partial,
|
95
|
+
:data,
|
96
|
+
:each,
|
97
|
+
:find_or_build,
|
98
|
+
:find_or_build_by,
|
99
|
+
:from_hash,
|
100
|
+
:index_proxy,
|
101
|
+
:push,
|
102
|
+
:size,
|
103
|
+
:to_a,
|
104
|
+
:to_hash,
|
105
|
+
:to => :data_storage
|
106
|
+
|
107
|
+
delegate :add_reference,
|
108
|
+
:attribute_references,
|
109
|
+
:build_reference,
|
110
|
+
:references,
|
111
|
+
:build_stringified_reference,
|
112
|
+
:build_stringified_reference_for_record,
|
113
|
+
:to => :references_storage
|
114
|
+
|
115
|
+
delegate :find,
|
116
|
+
:find_by,
|
117
|
+
:lazy_find,
|
118
|
+
:lazy_find_by,
|
119
|
+
:named_ref,
|
120
|
+
:primary_index,
|
121
|
+
:reindex_secondary_indexes!,
|
122
|
+
:skeletal_primary_index,
|
123
|
+
:to => :index_proxy
|
124
|
+
|
125
|
+
delegate :table_name,
|
126
|
+
:to => :model_class
|
127
|
+
|
128
|
+
# @param model_class [Class] A class of an ApplicationRecord model, that we want to persist into the DB or load from
|
129
|
+
# the DB.
|
130
|
+
# @param manager_ref [Array] Array of Symbols, that are keys of the InventoryObject's data, inserted into this
|
131
|
+
# InventoryCollection. Using these keys, we need to be able to uniquely identify each of the InventoryObject
|
132
|
+
# objects inside.
|
133
|
+
# @param association [Symbol] A Rails association callable on a :parent attribute is used for comparing with the
|
134
|
+
# objects in the DB, to decide if the InventoryObjects will be created/deleted/updated or used for obtaining
|
135
|
+
# the data from a DB, if a DB strategy is used. It returns objects of the :model_class class or its sub STI.
|
136
|
+
# @param parent [ApplicationRecord] An ApplicationRecord object that has a callable :association method returning
|
137
|
+
# the objects of a :model_class.
|
138
|
+
# @param strategy [Symbol] A strategy of the InventoryCollection that will be used for saving/loading of the
|
139
|
+
# InventoryObject objects.
|
140
|
+
# Allowed strategies are:
|
141
|
+
# - nil => InventoryObject objects of the InventoryCollection will be saved to the DB, only these objects
|
142
|
+
# will be referable from the other InventoryCollection objects.
|
143
|
+
# - :local_db_cache_all => Loads InventoryObject objects from the database, it loads all the objects that
|
144
|
+
# are a result of a [<:parent>.<:association>, :arel] taking
|
145
|
+
# first defined in this order. This strategy will not save any objects in the DB.
|
146
|
+
# - :local_db_find_references => Loads InventoryObject objects from the database, it loads only objects that
|
147
|
+
# were referenced by the other InventoryCollections using a filtered result
|
148
|
+
# of a [<:parent>.<:association>, :arel] taking first
|
149
|
+
# defined in this order. This strategy will not save any objects in the DB.
|
150
|
+
# - :local_db_find_missing_references => InventoryObject objects of the InventoryCollection will be saved to
|
151
|
+
# the DB. Then if we reference an object that is not present, it will
|
152
|
+
# load them from the db using :local_db_find_references strategy.
|
153
|
+
# @param custom_save_block [Proc] A custom lambda/proc for persisting in the DB, for cases where it's not enough
|
154
|
+
# to just save every InventoryObject inside by the defined rules and default saving algorithm.
|
155
|
+
#
|
156
|
+
# Example1 - saving SomeModel in my own ineffective way :-) :
|
157
|
+
#
|
158
|
+
# custom_save = lambda do |_ems, inventory_collection|
|
159
|
+
# inventory_collection.each |inventory_object| do
|
160
|
+
# hash = inventory_object.attributes # Loads possible dependencies into saveable hash
|
161
|
+
# obj = SomeModel.find_by(:attr => hash[:attr]) # Note: doing find_by for many models produces N+1
|
162
|
+
# # queries, avoid this, this is just a simple example :-)
|
163
|
+
# obj.update_attributes(hash) if obj
|
164
|
+
# obj ||= SomeModel.create(hash)
|
165
|
+
# inventory_object.id = obj.id # If this InventoryObject is referenced elsewhere, we need to store its
|
166
|
+
# primary key back to the InventoryObject
|
167
|
+
# end
|
168
|
+
#
|
169
|
+
# Example2 - saving parent OrchestrationStack in a more effective way, than the default saving algorithm can
|
170
|
+
# achieve. Ancestry gem requires an ActiveRecord object for association and is not defined as a proper
|
171
|
+
# ActiveRecord association. That leads in N+1 queries in the default saving algorithm, so we can do better
|
172
|
+
# with custom saving for now. The InventoryCollection is defined as a custom dependencies processor,
|
173
|
+
# without its own :model_class and InventoryObjects inside:
|
174
|
+
#
|
175
|
+
# InventoryRefresh::InventoryCollection.new({
|
176
|
+
# :association => :orchestration_stack_ancestry,
|
177
|
+
# :custom_save_block => orchestration_stack_ancestry_save_block,
|
178
|
+
# :dependency_attributes => {
|
179
|
+
# :orchestration_stacks => [collections[:orchestration_stacks]],
|
180
|
+
# :orchestration_stacks_resources => [collections[:orchestration_stacks_resources]]
|
181
|
+
# }
|
182
|
+
# })
|
183
|
+
#
|
184
|
+
# And the lambda is defined as:
|
185
|
+
#
|
186
|
+
# orchestration_stack_ancestry_save_block = lambda do |_ems, inventory_collection|
|
187
|
+
# stacks_inventory_collection = inventory_collection.dependency_attributes[:orchestration_stacks].try(:first)
|
188
|
+
#
|
189
|
+
# return if stacks_inventory_collection.blank?
|
190
|
+
#
|
191
|
+
# stacks_parents = stacks_inventory_collection.data.each_with_object({}) do |x, obj|
|
192
|
+
# parent_id = x.data[:parent].load.try(:id)
|
193
|
+
# obj[x.id] = parent_id if parent_id
|
194
|
+
# end
|
195
|
+
#
|
196
|
+
# model_class = stacks_inventory_collection.model_class
|
197
|
+
#
|
198
|
+
# stacks_parents_indexed = model_class
|
199
|
+
# .select([:id, :ancestry])
|
200
|
+
# .where(:id => stacks_parents.values).find_each.index_by(&:id)
|
201
|
+
#
|
202
|
+
# model_class
|
203
|
+
# .select([:id, :ancestry])
|
204
|
+
# .where(:id => stacks_parents.keys).find_each do |stack|
|
205
|
+
# parent = stacks_parents_indexed[stacks_parents[stack.id]]
|
206
|
+
# stack.update_attribute(:parent, parent)
|
207
|
+
# end
|
208
|
+
# end
|
209
|
+
# @param custom_reconnect_block [Proc] A custom lambda for reconnect logic of previously disconnected records
|
210
|
+
#
|
211
|
+
# Example - Reconnect disconnected Vms
|
212
|
+
# InventoryRefresh::InventoryCollection.new({
|
213
|
+
# :association => :orchestration_stack_ancestry,
|
214
|
+
# :custom_reconnect_block => vms_custom_reconnect_block,
|
215
|
+
# })
|
216
|
+
#
|
217
|
+
# And the lambda is defined as:
|
218
|
+
#
|
219
|
+
# vms_custom_reconnect_block = lambda do |inventory_collection, inventory_objects_index, attributes_index|
|
220
|
+
# inventory_objects_index.each_slice(1000) do |batch|
|
221
|
+
# Vm.where(:ems_ref => batch.map(&:second).map(&:manager_uuid)).each do |record|
|
222
|
+
# index = inventory_collection.object_index_with_keys(inventory_collection.manager_ref_to_cols, record)
|
223
|
+
#
|
224
|
+
# # We need to delete the record from the inventory_objects_index and attributes_index, otherwise it
|
225
|
+
# # would be sent for create.
|
226
|
+
# inventory_object = inventory_objects_index.delete(index)
|
227
|
+
# hash = attributes_index.delete(index)
|
228
|
+
#
|
229
|
+
# record.assign_attributes(hash.except(:id, :type))
|
230
|
+
# if !inventory_collection.check_changed? || record.changed?
|
231
|
+
# record.save!
|
232
|
+
# inventory_collection.store_updated_records(record)
|
233
|
+
# end
|
234
|
+
#
|
235
|
+
# inventory_object.id = record.id
|
236
|
+
# end
|
237
|
+
# end
|
238
|
+
# @param delete_method [Symbol] A delete method that will be used for deleting of the InventoryObject, if the
|
239
|
+
# object is marked for deletion. A default is :destroy, the instance method must be defined on the
|
240
|
+
# :model_class.
|
241
|
+
# @param dependency_attributes [Hash] Manually defined dependencies of this InventoryCollection. We can use this
|
242
|
+
# by manually place the InventoryCollection into the graph, to make sure the saving is invoked after the
|
243
|
+
# dependencies were saved. The dependencies itself are InventoryCollection objects. For a common use-cases
|
244
|
+
# we do not need to define dependencies manually, since those are inferred automatically by scanning of the
|
245
|
+
# data.
|
246
|
+
#
|
247
|
+
# Example:
|
248
|
+
# :dependency_attributes => {
|
249
|
+
# :orchestration_stacks => [collections[:orchestration_stacks]],
|
250
|
+
# :orchestration_stacks_resources => [collections[:orchestration_stacks_resources]]
|
251
|
+
# }
|
252
|
+
# This example is used in Example2 of the <param custom_save_block> and it means that our :custom_save_block
|
253
|
+
# will be invoked after the InventoryCollection :orchestration_stacks and :orchestration_stacks_resources
|
254
|
+
# are saved.
|
255
|
+
# @param attributes_blacklist [Array] Attributes we do not want to include into saving. We cannot blacklist an
|
256
|
+
# attribute that is needed for saving of the object.
|
257
|
+
# Note: attributes_blacklist is also used for internal resolving of the cycles in the graph.
|
258
|
+
#
|
259
|
+
# In the Example2 of the <param custom_save_block>, we have a custom saving code, that saves a :parent
|
260
|
+
# attribute of the OrchestrationStack. That means we don't want that attribute saved as a part of
|
261
|
+
# InventoryCollection for OrchestrationStack, so we would set :attributes_blacklist => [:parent]. Then the
|
262
|
+
# :parent will be ignored while saving.
|
263
|
+
# @param attributes_whitelist [Array] Same usage as the :attributes_blacklist, but defining full set of attributes
|
264
|
+
# that should be saved. Attributes that are part of :manager_ref and needed validations are automatically
|
265
|
+
# added.
|
266
|
+
# @param complete [Boolean] By default true, :complete is marking we are sending a complete dataset and therefore
|
267
|
+
# we can create/update/delete the InventoryObject objects. If :complete is false we will only do
|
268
|
+
# create/update without delete.
|
269
|
+
# @param update_only [Boolean] By default false. If true we only update the InventoryObject objects, if false we do
|
270
|
+
# create/update/delete.
|
271
|
+
# @param check_changed [Boolean] By default true. If true, before updating the InventoryObject, we call Rails
|
272
|
+
# 'changed?' method. This can optimize speed of updates heavily, but it can fail to recognize the change for
|
273
|
+
# e.g. Ancestry and Relationship based columns. If false, we always update the InventoryObject.
|
274
|
+
# @param arel [ActiveRecord::Associations::CollectionProxy|Arel::SelectManager] Instead of :parent and :association
|
275
|
+
# we can provide Arel directly to say what records should be compared to check if InventoryObject will be
|
276
|
+
# doing create/update/delete.
|
277
|
+
#
|
278
|
+
# Example:
|
279
|
+
# for a targeted refresh, we want to delete/update/create only a list of vms specified with a list of
|
280
|
+
# ems_refs:
|
281
|
+
# :arel => manager.vms.where(:ems_ref => manager_refs)
|
282
|
+
# Then we want to do the same for the hardwares of only those vms:
|
283
|
+
# :arel => manager.hardwares.joins(:vm_or_template).where(
|
284
|
+
# 'vms' => {:ems_ref => manager_refs}
|
285
|
+
# )
|
286
|
+
# And etc. for the other Vm related records.
|
287
|
+
# @param default_values [Hash] A hash of an attributes that will be added to every inventory object created by
|
288
|
+
# inventory_collection.build(hash)
|
289
|
+
#
|
290
|
+
# Example: Given
|
291
|
+
# inventory_collection = InventoryCollection.new({
|
292
|
+
# :model_class => ::Vm,
|
293
|
+
# :arel => @ems.vms,
|
294
|
+
# :default_values => {:ems_id => 10}
|
295
|
+
# })
|
296
|
+
# And building the inventory_object like:
|
297
|
+
# inventory_object = inventory_collection.build(:ems_ref => "vm_1", :name => "vm1")
|
298
|
+
# The inventory_object.data will look like:
|
299
|
+
# {:ems_ref => "vm_1", :name => "vm1", :ems_id => 10}
|
300
|
+
# @param inventory_object_attributes [Array] Array of attribute names that will be exposed as readers/writers on the
|
301
|
+
# InventoryObject objects inside.
|
302
|
+
#
|
303
|
+
# Example: Given
|
304
|
+
# inventory_collection = InventoryCollection.new({
|
305
|
+
# :model_class => ::Vm,
|
306
|
+
# :arel => @ems.vms,
|
307
|
+
# :inventory_object_attributes => [:name, :label]
|
308
|
+
# })
|
309
|
+
# And building the inventory_object like:
|
310
|
+
# inventory_object = inventory_collection.build(:ems_ref => "vm1", :name => "vm1")
|
311
|
+
# We can use inventory_object_attributes as setters and getters:
|
312
|
+
# inventory_object.name = "Name"
|
313
|
+
# inventory_object.label = inventory_object.name
|
314
|
+
# Which would be equivalent to less nicer way:
|
315
|
+
# inventory_object[:name] = "Name"
|
316
|
+
# inventory_object[:label] = inventory_object[:name]
|
317
|
+
# So by using inventory_object_attributes, we will be guarding the allowed attributes and will have an
|
318
|
+
# explicit list of allowed attributes, that can be used also for documentation purposes.
|
319
|
+
# @param name [Symbol] A unique name of the InventoryCollection under a Persister. If not provided, the :association
|
320
|
+
# attribute is used. If :association is nil as well, the :name will be inferred from the :model_class.
|
321
|
+
# @param saver_strategy [Symbol] A strategy that will be used for InventoryCollection persisting into the DB.
|
322
|
+
# Allowed saver strategies are:
|
323
|
+
# - :default => Using Rails saving methods, this way is not safe to run in multiple workers concurrently,
|
324
|
+
# since it will lead to non consistent data.
|
325
|
+
# - :batch => Using batch SQL queries, this way is not safe to run in multiple workers
|
326
|
+
# concurrently, since it will lead to non consistent data.
|
327
|
+
# - :concurrent_safe => This method is designed for concurrent saving. It uses atomic upsert to avoid
|
328
|
+
# data duplication and it uses timestamp based atomic checks to avoid new data being overwritten by the
|
329
|
+
# the old data.
|
330
|
+
# - :concurrent_safe_batch => Same as :concurrent_safe, but the upsert/update queries are executed as
|
331
|
+
# batched SQL queries, instead of sending 1 query per record.
|
332
|
+
# @param parent_inventory_collections [Array] Array of symbols having a name pointing to the
|
333
|
+
# InventoryRefresh::InventoryCollection objects, that serve as parents to this InventoryCollection. There are
|
334
|
+
# several scenarios to consider, when deciding if InventoryCollection has parent collections, see the example.
|
335
|
+
#
|
336
|
+
# Example:
|
337
|
+
# taking inventory collections :vms and :disks (local disks), if we write that:
|
338
|
+
# inventory_collection = InventoryCollection.new({
|
339
|
+
# :model_class => ::Disk,
|
340
|
+
# :association => :disks,
|
341
|
+
# :manager_ref => [:vm, :location]
|
342
|
+
# :parent_inventory_collection => [:vms],
|
343
|
+
# })
|
344
|
+
#
|
345
|
+
# Then the decision for having :parent_inventory_collection => [:vms] was probably driven by these
|
346
|
+
# points:
|
347
|
+
# 1. We can get list of all disks only by doing SQL query through the parent object (so there will be join
|
348
|
+
# from vms to disks table).
|
349
|
+
# 2. There is no API query for getting all disks from the provider API, we get them inside VM data, or as
|
350
|
+
# a Vm subquery
|
351
|
+
# 3. Part of the manager_ref of the IC is the VM object (foreign key), so the disk's location is unique
|
352
|
+
# only under 1 Vm. (In current models, this modeled going through Hardware model)
|
353
|
+
# 4. In targeted refresh, we always expect that each Vm will be saved with all its disks.
|
354
|
+
#
|
355
|
+
# Then having the above points, adding :parent_inventory_collection => [:vms], will bring these
|
356
|
+
# implications:
|
357
|
+
# 1. By archiving/deleting Vm, we can no longer see the disk, because those were owned by the Vm. Any
|
358
|
+
# archival/deletion of the Disk model, must be then done by cascade delete/hooks logic.
|
359
|
+
# 2. Having Vm as a parent ensures we always process it first. So e.g. when providing no Vms for saving
|
360
|
+
# we would have no graph dependency (no data --> no edges --> no dependencies) and Disk could be
|
361
|
+
# archived/removed before the Vm, while we always want to archive the VM first.
|
362
|
+
# 3. For targeted refresh, we always expect that all disks are saved with a VM. So for targeting :disks,
|
363
|
+
# we are not using #manager_uuids attribute, since the scope is "all disks of all targeted VMs", so we
|
364
|
+
# always use #manager_uuids of the parent. (that is why :parent_inventory_collections and
|
365
|
+
# :manager_uuids are mutually exclusive attributes)
|
366
|
+
# 4. For automatically building the #targeted_arel query, we need the parent to know what is the root node.
|
367
|
+
# While this information can be introspected from the data, it creates a scope for create&update&delete,
|
368
|
+
# which means it has to work with no data provided (causing delete all). So with no data we cannot
|
369
|
+
# introspect anything.
|
370
|
+
# @param manager_uuids [Array|Proc] Array of manager_uuids of the InventoryObjects we want to create/update/delete. Using
|
371
|
+
# this attribute, the db_collection_for_comparison will be automatically limited by the manager_uuids, in a
|
372
|
+
# case of a simple relation. In a case of a complex relation, we can leverage :manager_uuids in a
|
373
|
+
# custom :targeted_arel. We can pass also lambda, for lazy_evaluation.
|
374
|
+
# @param all_manager_uuids [Array] Array of all manager_uuids of the InventoryObjects. With the :targeted true,
|
375
|
+
# having this parameter defined will invoke only :delete_method on a complement of this set, making sure
|
376
|
+
# the DB has only this set of data after. This :attribute serves for deleting of top level
|
377
|
+
# InventoryCollections, i.e. InventoryCollections having parent_inventory_collections nil. The deleting of
|
378
|
+
# child collections is already handled by the scope of the parent_inventory_collections and using Rails
|
379
|
+
# :dependent => :destroy,
|
380
|
+
# @param targeted_arel [Proc] A callable block that receives this InventoryCollection as a first argument. In there
|
381
|
+
# we can leverage a :parent_inventory_collections or :manager_uuids to limit the query based on the
|
382
|
+
# manager_uuids available.
|
383
|
+
# Example:
|
384
|
+
# targeted_arel = lambda do |inventory_collection|
|
385
|
+
# # Getting ems_refs of parent :vms and :miq_templates
|
386
|
+
# manager_uuids = inventory_collection.parent_inventory_collections.collect(&:manager_uuids).flatten
|
387
|
+
# inventory_collection.db_collection_for_comparison.hardwares.joins(:vm_or_template).where(
|
388
|
+
# 'vms' => {:ems_ref => manager_uuids}
|
389
|
+
# )
|
390
|
+
# end
|
391
|
+
#
|
392
|
+
# inventory_collection = InventoryCollection.new({
|
393
|
+
# :model_class => ::Hardware,
|
394
|
+
# :association => :hardwares,
|
395
|
+
# :parent_inventory_collection => [:vms, :miq_templates],
|
396
|
+
# :targeted_arel => targeted_arel,
|
397
|
+
# })
|
398
|
+
# @param targeted [Boolean] True if the collection is targeted, in that case it will be leveraging :manager_uuids
|
399
|
+
# :parent_inventory_collections and :targeted_arel to save a subgraph of a data.
|
400
|
+
# @param manager_ref_allowed_nil [Array] Array of symbols having manager_ref columns, that are a foreign key an can
|
401
|
+
# be nil. Given the table are shared by many providers, it can happen, that the table is used only partially.
|
402
|
+
# Then it can happen we want to allow certain foreign keys to be nil, while being sure the referential
|
403
|
+
# integrity is not broken. Of course the DB Foreign Key can't be created in this case, so we should try to
|
404
|
+
# avoid this usecase by a proper modeling.
|
405
|
+
# @param use_ar_object [Boolean] True or False. Whether we need to initialize AR object as part of the saving
|
406
|
+
# it's needed if the model have special setters, serialize of columns, etc. This setting is relevant only
|
407
|
+
# for the batch saver strategy.
|
408
|
+
# @param batch_extra_attributes [Array] Array of symbols marking which extra attributes we want to store into the
|
409
|
+
# db. These extra attributes might be a product of :use_ar_object assignment and we need to specify them
|
410
|
+
# manually, if we want to use a batch saving strategy and we have models that populate attributes as a side
|
411
|
+
# effect.
|
412
|
+
def initialize(model_class: nil, manager_ref: nil, association: nil, parent: nil, strategy: nil,
|
413
|
+
custom_save_block: nil, delete_method: nil, dependency_attributes: nil,
|
414
|
+
attributes_blacklist: nil, attributes_whitelist: nil, complete: nil, update_only: nil,
|
415
|
+
check_changed: nil, arel: nil, default_values: {}, create_only: nil,
|
416
|
+
inventory_object_attributes: nil, name: nil, saver_strategy: nil,
|
417
|
+
parent_inventory_collections: nil, manager_uuids: [], all_manager_uuids: nil, targeted_arel: nil,
|
418
|
+
targeted: nil, manager_ref_allowed_nil: nil, secondary_refs: {}, use_ar_object: nil,
|
419
|
+
custom_reconnect_block: nil, batch_extra_attributes: [])
|
420
|
+
@model_class = model_class
|
421
|
+
@manager_ref = manager_ref || [:ems_ref]
|
422
|
+
@secondary_refs = secondary_refs
|
423
|
+
@association = association
|
424
|
+
@parent = parent || nil
|
425
|
+
@arel = arel
|
426
|
+
@dependency_attributes = dependency_attributes || {}
|
427
|
+
@strategy = process_strategy(strategy)
|
428
|
+
@delete_method = delete_method || :destroy
|
429
|
+
@custom_save_block = custom_save_block
|
430
|
+
@custom_reconnect_block = custom_reconnect_block
|
431
|
+
@check_changed = check_changed.nil? ? true : check_changed
|
432
|
+
@internal_attributes = %i(__feedback_edge_set_parent __parent_inventory_collections)
|
433
|
+
@complete = complete.nil? ? true : complete
|
434
|
+
@update_only = update_only.nil? ? false : update_only
|
435
|
+
@create_only = create_only.nil? ? false : create_only
|
436
|
+
@default_values = default_values
|
437
|
+
@name = name || association || model_class.to_s.demodulize.tableize
|
438
|
+
@saver_strategy = process_saver_strategy(saver_strategy)
|
439
|
+
@use_ar_object = use_ar_object || false
|
440
|
+
@batch_extra_attributes = batch_extra_attributes
|
441
|
+
|
442
|
+
@manager_ref_allowed_nil = manager_ref_allowed_nil || []
|
443
|
+
|
444
|
+
# Targeted mode related attributes
|
445
|
+
# TODO(lsmola) Should we refactor this to use references too?
|
446
|
+
@all_manager_uuids = all_manager_uuids
|
447
|
+
@parent_inventory_collections = parent_inventory_collections
|
448
|
+
@targeted_arel = targeted_arel
|
449
|
+
@targeted = !!targeted
|
450
|
+
|
451
|
+
@inventory_object_attributes = inventory_object_attributes
|
452
|
+
|
453
|
+
@saved ||= false
|
454
|
+
@attributes_blacklist = Set.new
|
455
|
+
@attributes_whitelist = Set.new
|
456
|
+
@transitive_dependency_attributes = Set.new
|
457
|
+
@dependees = Set.new
|
458
|
+
@data_storage = ::InventoryRefresh::InventoryCollection::DataStorage.new(self, secondary_refs)
|
459
|
+
@references_storage = ::InventoryRefresh::InventoryCollection::ReferencesStorage.new(index_proxy)
|
460
|
+
@targeted_scope = ::InventoryRefresh::InventoryCollection::ReferencesStorage.new(index_proxy).merge!(manager_uuids)
|
461
|
+
|
462
|
+
@created_records = []
|
463
|
+
@updated_records = []
|
464
|
+
@deleted_records = []
|
465
|
+
|
466
|
+
blacklist_attributes!(attributes_blacklist) if attributes_blacklist.present?
|
467
|
+
whitelist_attributes!(attributes_whitelist) if attributes_whitelist.present?
|
468
|
+
end
|
469
|
+
|
470
|
+
# Caches what records were created, for later use, e.g. post provision behavior
|
471
|
+
#
|
472
|
+
# @param records [Array<ApplicationRecord, Hash>] list of stored records
|
473
|
+
def store_created_records(records)
|
474
|
+
@created_records.concat(records_identities(records))
|
475
|
+
end
|
476
|
+
|
477
|
+
# Caches what records were updated, for later use, e.g. post provision behavior
|
478
|
+
#
|
479
|
+
# @param records [Array<ApplicationRecord, Hash>] list of stored records
|
480
|
+
def store_updated_records(records)
|
481
|
+
@updated_records.concat(records_identities(records))
|
482
|
+
end
|
483
|
+
|
484
|
+
# Caches what records were deleted/soft-deleted, for later use, e.g. post provision behavior
|
485
|
+
#
|
486
|
+
# @param records [Array<ApplicationRecord, Hash>] list of stored records
|
487
|
+
def store_deleted_records(records)
|
488
|
+
@deleted_records.concat(records_identities(records))
|
489
|
+
end
|
490
|
+
|
491
|
+
# Processes passed saver strategy
|
492
|
+
#
|
493
|
+
# @param saver_strategy [Symbol] Passed saver strategy
|
494
|
+
# @return [Symbol] Returns back the passed strategy if supported, or raises exception
|
495
|
+
def process_saver_strategy(saver_strategy)
|
496
|
+
return :default unless saver_strategy
|
497
|
+
|
498
|
+
saver_strategy = saver_strategy.to_sym
|
499
|
+
case saver_strategy
|
500
|
+
when :default, :batch, :concurrent_safe, :concurrent_safe_batch
|
501
|
+
saver_strategy
|
502
|
+
else
|
503
|
+
raise "Unknown InventoryCollection saver strategy: :#{saver_strategy}, allowed strategies are "\
|
504
|
+
":default, :batch, :concurrent_safe and :concurrent_safe_batch"
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# Processes passed strategy, modifies :data_collection_finalized and :saved attributes for db only strategies
|
509
|
+
#
|
510
|
+
# @param strategy_name [Symbol] Passed saver strategy
|
511
|
+
# @return [Symbol] Returns back the passed strategy if supported, or raises exception
|
512
|
+
def process_strategy(strategy_name)
|
513
|
+
self.data_collection_finalized = false
|
514
|
+
|
515
|
+
return unless strategy_name
|
516
|
+
|
517
|
+
strategy_name = strategy_name.to_sym
|
518
|
+
case strategy_name
|
519
|
+
when :local_db_cache_all
|
520
|
+
self.data_collection_finalized = true
|
521
|
+
self.saved = true
|
522
|
+
when :local_db_find_references
|
523
|
+
self.saved = true
|
524
|
+
when :local_db_find_missing_references
|
525
|
+
else
|
526
|
+
raise "Unknown InventoryCollection strategy: :#{strategy_name}, allowed strategies are :local_db_cache_all, "\
|
527
|
+
":local_db_find_references and :local_db_find_missing_references."
|
528
|
+
end
|
529
|
+
strategy_name
|
530
|
+
end
|
531
|
+
|
532
|
+
# @return [Boolean] true means we want to call .changed? on every ActiveRecord object before saving it
|
533
|
+
def check_changed?
|
534
|
+
check_changed
|
535
|
+
end
|
536
|
+
|
537
|
+
# @return [Boolean] true means we want to use ActiveRecord object for writing attributes and we want to perform
|
538
|
+
# casting on all columns
|
539
|
+
def use_ar_object?
|
540
|
+
use_ar_object
|
541
|
+
end
|
542
|
+
|
543
|
+
# @return [Boolean] true means the data is not complete, leading to only creating and updating data
|
544
|
+
def complete?
|
545
|
+
complete
|
546
|
+
end
|
547
|
+
|
548
|
+
# @return [Boolean] true means we want to only update data
|
549
|
+
def update_only?
|
550
|
+
update_only
|
551
|
+
end
|
552
|
+
|
553
|
+
# @return [Boolean] true means we will delete/soft-delete data
|
554
|
+
def delete_allowed?
|
555
|
+
complete? && !update_only?
|
556
|
+
end
|
557
|
+
|
558
|
+
# @return [Boolean] true means we will delete/soft-delete data
|
559
|
+
def create_allowed?
|
560
|
+
!update_only?
|
561
|
+
end
|
562
|
+
|
563
|
+
# @return [Boolean] true means that only create of new data is allowed
|
564
|
+
def create_only?
|
565
|
+
create_only
|
566
|
+
end
|
567
|
+
|
568
|
+
# @return [Boolean] true if the whole InventoryCollection object has all data persisted
|
569
|
+
def saved?
|
570
|
+
saved
|
571
|
+
end
|
572
|
+
|
573
|
+
# @return [Boolean] true if all dependencies have all data persisted
|
574
|
+
def saveable?
|
575
|
+
dependencies.all?(&:saved?)
|
576
|
+
end
|
577
|
+
|
578
|
+
# @return [Boolean] true if we are using a saver strategy that allows saving in parallel processes
|
579
|
+
def parallel_safe?
|
580
|
+
@parallel_safe_cache ||= %i(concurrent_safe concurrent_safe_batch).include?(saver_strategy)
|
581
|
+
end
|
582
|
+
|
583
|
+
# @return [Boolean] true if the model_class supports STI
|
584
|
+
def supports_sti?
|
585
|
+
@supports_sti_cache = model_class.column_names.include?("type") if @supports_sti_cache.nil?
|
586
|
+
@supports_sti_cache
|
587
|
+
end
|
588
|
+
|
589
|
+
# @return [Boolean] true if the model_class has created_on column
|
590
|
+
def supports_created_on?
|
591
|
+
if @supports_created_on_cache.nil?
|
592
|
+
@supports_created_on_cache = (model_class.column_names.include?("created_on") && ActiveRecord::Base.record_timestamps)
|
593
|
+
end
|
594
|
+
@supports_created_on_cache
|
595
|
+
end
|
596
|
+
|
597
|
+
# @return [Boolean] true if the model_class has updated_on column
|
598
|
+
def supports_updated_on?
|
599
|
+
if @supports_updated_on_cache.nil?
|
600
|
+
@supports_updated_on_cache = (model_class.column_names.include?("updated_on") && ActiveRecord::Base.record_timestamps)
|
601
|
+
end
|
602
|
+
@supports_updated_on_cache
|
603
|
+
end
|
604
|
+
|
605
|
+
# @return [Boolean] true if the model_class has created_at column
|
606
|
+
def supports_created_at?
|
607
|
+
if @supports_created_at_cache.nil?
|
608
|
+
@supports_created_at_cache = (model_class.column_names.include?("created_at") && ActiveRecord::Base.record_timestamps)
|
609
|
+
end
|
610
|
+
@supports_created_at_cache
|
611
|
+
end
|
612
|
+
|
613
|
+
# @return [Boolean] true if the model_class has updated_at column
|
614
|
+
def supports_updated_at?
|
615
|
+
if @supports_updated_at_cache.nil?
|
616
|
+
@supports_updated_at_cache = (model_class.column_names.include?("updated_at") && ActiveRecord::Base.record_timestamps)
|
617
|
+
end
|
618
|
+
@supports_updated_at_cache
|
619
|
+
end
|
620
|
+
|
621
|
+
# @return [Boolean] true if the model_class has resource_timestamps_max column
|
622
|
+
def supports_resource_timestamps_max?
|
623
|
+
@supports_resource_timestamps_max_cache ||= model_class.column_names.include?("resource_timestamps_max")
|
624
|
+
end
|
625
|
+
|
626
|
+
# @return [Boolean] true if the model_class has resource_timestamps column
|
627
|
+
def supports_resource_timestamps?
|
628
|
+
@supports_resource_timestamps_cache ||= model_class.column_names.include?("resource_timestamps")
|
629
|
+
end
|
630
|
+
|
631
|
+
# @return [Boolean] true if the model_class has resource_timestamp column
|
632
|
+
def supports_resource_timestamp?
|
633
|
+
@supports_resource_timestamp_cache ||= model_class.column_names.include?("resource_timestamp")
|
634
|
+
end
|
635
|
+
|
636
|
+
# @return [Boolean] true if the model_class has resource_versions_max column
|
637
|
+
def supports_resource_versions_max?
|
638
|
+
@supports_resource_versions_max_cache ||= model_class.column_names.include?("resource_versions_max")
|
639
|
+
end
|
640
|
+
|
641
|
+
# @return [Boolean] true if the model_class has resource_versions column
|
642
|
+
def supports_resource_versions?
|
643
|
+
@supports_resource_versions_cache ||= model_class.column_names.include?("resource_versions")
|
644
|
+
end
|
645
|
+
|
646
|
+
# @return [Boolean] true if the model_class has resource_version column
|
647
|
+
def supports_resource_version?
|
648
|
+
@supports_resource_version_cache ||= model_class.column_names.include?("resource_version")
|
649
|
+
end
|
650
|
+
|
651
|
+
# @return [Array<Symbol>] all columns that are part of the best fit unique index
|
652
|
+
def unique_index_columns
|
653
|
+
return @unique_index_columns if @unique_index_columns
|
654
|
+
|
655
|
+
@unique_index_columns = unique_index_for(unique_index_keys).columns.map(&:to_sym)
|
656
|
+
end
|
657
|
+
|
658
|
+
def unique_index_keys
|
659
|
+
@unique_index_keys ||= manager_ref_to_cols.map(&:to_sym)
|
660
|
+
end
|
661
|
+
|
662
|
+
# @return [Array<ActiveRecord::ConnectionAdapters::IndexDefinition>] array of all unique indexes known to model
|
663
|
+
def unique_indexes
|
664
|
+
@unique_indexes_cache if @unique_indexes_cache
|
665
|
+
|
666
|
+
@unique_indexes_cache = model_class.connection.indexes(model_class.table_name).select(&:unique)
|
667
|
+
|
668
|
+
if @unique_indexes_cache.blank?
|
669
|
+
raise "#{self} and its table #{model_class.table_name} must have a unique index defined, to"\
|
670
|
+
" be able to use saver_strategy :concurrent_safe or :concurrent_safe_batch."
|
671
|
+
end
|
672
|
+
|
673
|
+
@unique_indexes_cache
|
674
|
+
end
|
675
|
+
|
676
|
+
# Finds an index that fits the list of columns (keys) the best
|
677
|
+
#
|
678
|
+
# @param keys [Array<Symbol>]
|
679
|
+
# @raise [Exception] if the unique index for the columns was not found
|
680
|
+
# @return [ActiveRecord::ConnectionAdapters::IndexDefinition] unique index fitting the keys
|
681
|
+
def unique_index_for(keys)
|
682
|
+
@unique_index_for_keys_cache ||= {}
|
683
|
+
@unique_index_for_keys_cache[keys] if @unique_index_for_keys_cache[keys]
|
684
|
+
|
685
|
+
# Find all uniq indexes that that are covering our keys
|
686
|
+
uniq_key_candidates = unique_indexes.each_with_object([]) { |i, obj| obj << i if (keys - i.columns.map(&:to_sym)).empty? }
|
687
|
+
|
688
|
+
if @unique_indexes_cache.blank?
|
689
|
+
raise "#{self} and its table #{model_class.table_name} must have a unique index defined "\
|
690
|
+
"covering columns #{keys} to be able to use saver_strategy :concurrent_safe or :concurrent_safe_batch."
|
691
|
+
end
|
692
|
+
|
693
|
+
# Take the uniq key having the least number of columns
|
694
|
+
@unique_index_for_keys_cache[keys] = uniq_key_candidates.min_by { |x| x.columns.count }
|
695
|
+
end
|
696
|
+
|
697
|
+
def internal_columns
|
698
|
+
return @internal_columns if @internal_columns
|
699
|
+
|
700
|
+
@internal_columns = [] + internal_timestamp_columns
|
701
|
+
@internal_columns << :type if supports_sti?
|
702
|
+
@internal_columns << :resource_timestamps_max if supports_resource_timestamps_max?
|
703
|
+
@internal_columns << :resource_timestamps if supports_resource_timestamps?
|
704
|
+
@internal_columns << :resource_timestamp if supports_resource_timestamp?
|
705
|
+
@internal_columns << :resource_versions_max if supports_resource_versions_max?
|
706
|
+
@internal_columns << :resource_versions if supports_resource_versions?
|
707
|
+
@internal_columns << :resource_version if supports_resource_version?
|
708
|
+
@internal_columns
|
709
|
+
end
|
710
|
+
|
711
|
+
def internal_timestamp_columns
|
712
|
+
return @internal_timestamp_columns if @internal_timestamp_columns
|
713
|
+
|
714
|
+
@internal_timestamp_columns = []
|
715
|
+
@internal_timestamp_columns << :created_on if supports_created_on?
|
716
|
+
@internal_timestamp_columns << :created_at if supports_created_at?
|
717
|
+
@internal_timestamp_columns << :updated_on if supports_updated_on?
|
718
|
+
@internal_timestamp_columns << :updated_at if supports_updated_at?
|
719
|
+
|
720
|
+
@internal_timestamp_columns
|
721
|
+
end
|
722
|
+
|
723
|
+
def base_columns
|
724
|
+
@base_columns ||= unique_index_columns + internal_columns
|
725
|
+
end
|
726
|
+
|
727
|
+
# @return [Boolean] true if no more data will be added to this InventoryCollection object, that usually happens
|
728
|
+
# after the parsing step is finished
|
729
|
+
def data_collection_finalized?
|
730
|
+
data_collection_finalized
|
731
|
+
end
|
732
|
+
|
733
|
+
# @param value [Object] Object we want to test
|
734
|
+
# @return [Boolean] true is value is kind of InventoryRefresh::InventoryObject
|
735
|
+
def inventory_object?(value)
|
736
|
+
value.kind_of?(::InventoryRefresh::InventoryObject)
|
737
|
+
end
|
738
|
+
|
739
|
+
# @param value [Object] Object we want to test
|
740
|
+
# @return [Boolean] true is value is kind of InventoryRefresh::InventoryObjectLazy
|
741
|
+
def inventory_object_lazy?(value)
|
742
|
+
value.kind_of?(::InventoryRefresh::InventoryObjectLazy)
|
743
|
+
end
|
744
|
+
|
745
|
+
# Builds string uuid from passed Object and keys
|
746
|
+
#
|
747
|
+
# @param keys [Array<Symbol>] Indexes into the Hash data
|
748
|
+
# @param record [ApplicationRecord] ActiveRecord record
|
749
|
+
# @return [String] Concatenated values on keys from data
|
750
|
+
def object_index_with_keys(keys, record)
|
751
|
+
# TODO(lsmola) remove, last usage is in k8s reconnect logic
|
752
|
+
build_stringified_reference_for_record(record, keys)
|
753
|
+
end
|
754
|
+
|
755
|
+
# True if processing of this InventoryCollection object would lead to no operations. Then we use this marker to
|
756
|
+
# stop processing of the InventoryCollector object very soon, to avoid a lot of unnecessary Db queries, etc.
|
757
|
+
#
|
758
|
+
# @return [Boolean] true if processing of this InventoryCollection object would lead to no operations.
|
759
|
+
def noop?
|
760
|
+
# If this InventoryCollection doesn't do anything. it can easily happen for targeted/batched strategies.
|
761
|
+
if targeted?
|
762
|
+
if parent_inventory_collections.nil? && targeted_scope.primary_references.blank? &&
|
763
|
+
all_manager_uuids.nil? && parent_inventory_collections.blank? && custom_save_block.nil? &&
|
764
|
+
skeletal_primary_index.blank?
|
765
|
+
# It's a noop Parent targeted InventoryCollection
|
766
|
+
true
|
767
|
+
elsif !parent_inventory_collections.nil? && parent_inventory_collections.all? { |x| x.targeted_scope.primary_references.blank? } &&
|
768
|
+
skeletal_primary_index.blank?
|
769
|
+
# It's a noop Child targeted InventoryCollection
|
770
|
+
true
|
771
|
+
else
|
772
|
+
false
|
773
|
+
end
|
774
|
+
elsif data.blank? && !delete_allowed? && skeletal_primary_index.blank?
|
775
|
+
# If we have no data to save and delete is not allowed, we can just skip
|
776
|
+
true
|
777
|
+
else
|
778
|
+
false
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
# @return [Boolean] true is processing of this InventoryCollection will be in targeted mode
|
783
|
+
def targeted?
|
784
|
+
targeted
|
785
|
+
end
|
786
|
+
|
787
|
+
# Convert manager_ref list of attributes to list of DB columns
|
788
|
+
#
|
789
|
+
# @return [Array<String>] true is processing of this InventoryCollection will be in targeted mode
|
790
|
+
def manager_ref_to_cols
|
791
|
+
# TODO(lsmola) this should contain the polymorphic _type, otherwise the IC with polymorphic unique key will get
|
792
|
+
# conflicts
|
793
|
+
manager_ref.map do |ref|
|
794
|
+
association_to_foreign_key_mapping[ref] || ref
|
795
|
+
end
|
796
|
+
end
|
797
|
+
|
798
|
+
# List attributes causing a dependency and filters them by attributes_blacklist and attributes_whitelist
|
799
|
+
#
|
800
|
+
# @return [Hash{Symbol => Set}] attributes causing a dependency and filtered by blacklist and whitelist
|
801
|
+
def filtered_dependency_attributes
|
802
|
+
filtered_attributes = dependency_attributes
|
803
|
+
|
804
|
+
if attributes_blacklist.present?
|
805
|
+
filtered_attributes = filtered_attributes.reject { |key, _value| attributes_blacklist.include?(key) }
|
806
|
+
end
|
807
|
+
|
808
|
+
if attributes_whitelist.present?
|
809
|
+
filtered_attributes = filtered_attributes.select { |key, _value| attributes_whitelist.include?(key) }
|
810
|
+
end
|
811
|
+
|
812
|
+
filtered_attributes
|
813
|
+
end
|
814
|
+
|
815
|
+
# Attributes that are needed to be able to save the record, i.e. attributes that are part of the unique index
|
816
|
+
# and attributes with presence validation or NOT NULL constraint
|
817
|
+
#
|
818
|
+
# @return [Array<Symbol>] attributes that are needed for saving of the record
|
819
|
+
def fixed_attributes
|
820
|
+
if model_class
|
821
|
+
presence_validators = model_class.validators.detect { |x| x.kind_of?(ActiveRecord::Validations::PresenceValidator) }
|
822
|
+
end
|
823
|
+
# Attributes that has to be always on the entity, so attributes making unique index of the record + attributes
|
824
|
+
# that have presence validation
|
825
|
+
fixed_attributes = manager_ref
|
826
|
+
fixed_attributes += presence_validators.attributes if presence_validators.present?
|
827
|
+
fixed_attributes
|
828
|
+
end
|
829
|
+
|
830
|
+
# Returns fixed dependencies, which are the ones we can't move, because we wouldn't be able to save the data
|
831
|
+
#
|
832
|
+
# @returns [Set<InventoryRefresh::InventoryCollection>] all unique non saved fixed dependencies
|
833
|
+
def fixed_dependencies
|
834
|
+
fixed_attrs = fixed_attributes
|
835
|
+
|
836
|
+
filtered_dependency_attributes.each_with_object(Set.new) do |(key, value), fixed_deps|
|
837
|
+
fixed_deps.merge(value) if fixed_attrs.include?(key)
|
838
|
+
end.reject(&:saved?)
|
839
|
+
end
|
840
|
+
|
841
|
+
# @return [Array<InventoryRefresh::InventoryCollection>] all unique non saved dependencies
|
842
|
+
def dependencies
|
843
|
+
filtered_dependency_attributes.values.map(&:to_a).flatten.uniq.reject(&:saved?)
|
844
|
+
end
|
845
|
+
|
846
|
+
# Returns what attributes are causing a dependencies to certain InventoryCollection objects.
|
847
|
+
#
|
848
|
+
# @param inventory_collections [Array<InventoryRefresh::InventoryCollection>]
|
849
|
+
# @return [Array<InventoryRefresh::InventoryCollection>] attributes causing the dependencies to certain
|
850
|
+
# InventoryCollection objects
|
851
|
+
def dependency_attributes_for(inventory_collections)
|
852
|
+
attributes = Set.new
|
853
|
+
inventory_collections.each do |inventory_collection|
|
854
|
+
attributes += filtered_dependency_attributes.select { |_key, value| value.include?(inventory_collection) }.keys
|
855
|
+
end
|
856
|
+
attributes
|
857
|
+
end
|
858
|
+
|
859
|
+
# Add passed attributes to blacklist. The manager_ref attributes cannot be blacklisted, otherwise we will not
|
860
|
+
# be able to identify the inventory_object. We do not automatically remove attributes causing fixed dependencies,
|
861
|
+
# so beware that without them, you won't be able to create the record.
|
862
|
+
#
|
863
|
+
# @param attributes [Array<Symbol>] Attributes we want to blacklist
|
864
|
+
# @return [Array<Symbol>] All blacklisted attributes
|
865
|
+
def blacklist_attributes!(attributes)
|
866
|
+
self.attributes_blacklist += attributes - (fixed_attributes + internal_attributes)
|
867
|
+
end
|
868
|
+
|
869
|
+
# Add passed attributes to whitelist. The manager_ref attributes always needs to be in the white list, otherwise
|
870
|
+
# we will not be able to identify theinventory_object. We do not automatically add attributes causing fixed
|
871
|
+
# dependencies, so beware that without them, you won't be able to create the record.
|
872
|
+
#
|
873
|
+
# @param attributes [Array<Symbol>] Attributes we want to whitelist
|
874
|
+
# @return [Array<Symbol>] All whitelisted attributes
|
875
|
+
def whitelist_attributes!(attributes)
|
876
|
+
self.attributes_whitelist += attributes + (fixed_attributes + internal_attributes)
|
877
|
+
end
|
878
|
+
|
879
|
+
# @return [InventoryCollection] a shallow copy of InventoryCollection, the copy will share data_storage of the
|
880
|
+
# original collection, otherwise we would be copying a lot of records in memory.
|
881
|
+
def clone
|
882
|
+
cloned = self.class.new(:model_class => model_class,
|
883
|
+
:manager_ref => manager_ref,
|
884
|
+
:association => association,
|
885
|
+
:parent => parent,
|
886
|
+
:arel => arel,
|
887
|
+
:strategy => strategy,
|
888
|
+
:saver_strategy => saver_strategy,
|
889
|
+
:custom_save_block => custom_save_block,
|
890
|
+
# We want cloned IC to be update only, since this is used for cycle resolution
|
891
|
+
:update_only => true,
|
892
|
+
# Dependency attributes need to be a hard copy, since those will differ for each
|
893
|
+
# InventoryCollection
|
894
|
+
:dependency_attributes => dependency_attributes.clone)
|
895
|
+
|
896
|
+
cloned.data_storage = data_storage
|
897
|
+
cloned
|
898
|
+
end
|
899
|
+
|
900
|
+
# @return [Array<ActiveRecord::Reflection::BelongsToReflection">] All belongs_to associations
|
901
|
+
def belongs_to_associations
|
902
|
+
model_class.reflect_on_all_associations.select { |x| x.kind_of?(ActiveRecord::Reflection::BelongsToReflection) }
|
903
|
+
end
|
904
|
+
|
905
|
+
# @return [Hash{Symbol => String}] Hash with association name mapped to foreign key column name
|
906
|
+
def association_to_foreign_key_mapping
|
907
|
+
return {} unless model_class
|
908
|
+
|
909
|
+
@association_to_foreign_key_mapping ||= belongs_to_associations.each_with_object({}) do |x, obj|
|
910
|
+
obj[x.name] = x.foreign_key
|
911
|
+
end
|
912
|
+
end
|
913
|
+
|
914
|
+
# @return [Hash{String => Hash}] Hash with foreign_key column name mapped to association name
|
915
|
+
def foreign_key_to_association_mapping
|
916
|
+
return {} unless model_class
|
917
|
+
|
918
|
+
@foreign_key_to_association_mapping ||= belongs_to_associations.each_with_object({}) do |x, obj|
|
919
|
+
obj[x.foreign_key] = x.name
|
920
|
+
end
|
921
|
+
end
|
922
|
+
|
923
|
+
# @return [Hash{Symbol => String}] Hash with association name mapped to polymorphic foreign key type column name
|
924
|
+
def association_to_foreign_type_mapping
|
925
|
+
return {} unless model_class
|
926
|
+
|
927
|
+
@association_to_foreign_type_mapping ||= model_class.reflect_on_all_associations.each_with_object({}) do |x, obj|
|
928
|
+
obj[x.name] = x.foreign_type if x.polymorphic?
|
929
|
+
end
|
930
|
+
end
|
931
|
+
|
932
|
+
# @return [Hash{Symbol => String}] Hash with polymorphic foreign key type column name mapped to association name
|
933
|
+
def foreign_type_to_association_mapping
|
934
|
+
return {} unless model_class
|
935
|
+
|
936
|
+
@foreign_type_to_association_mapping ||= model_class.reflect_on_all_associations.each_with_object({}) do |x, obj|
|
937
|
+
obj[x.foreign_type] = x.name if x.polymorphic?
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
# @return [Hash{Symbol => String}] Hash with association name mapped to base class of the association
|
942
|
+
def association_to_base_class_mapping
|
943
|
+
return {} unless model_class
|
944
|
+
|
945
|
+
@association_to_base_class_mapping ||= model_class.reflect_on_all_associations.each_with_object({}) do |x, obj|
|
946
|
+
obj[x.name] = x.klass.base_class.name unless x.polymorphic?
|
947
|
+
end
|
948
|
+
end
|
949
|
+
|
950
|
+
# @return [Array<Symbol>] List of all column names that are foreign keys
|
951
|
+
def foreign_keys
|
952
|
+
return [] unless model_class
|
953
|
+
|
954
|
+
@foreign_keys_cache ||= belongs_to_associations.map(&:foreign_key).map!(&:to_sym)
|
955
|
+
end
|
956
|
+
|
957
|
+
# @return [Array<Symbol>] List of all column names that are foreign keys and cannot removed, otherwise we couldn't
|
958
|
+
# save the record
|
959
|
+
def fixed_foreign_keys
|
960
|
+
# Foreign keys that are part of a manager_ref must be present, otherwise the record would get lost. This is a
|
961
|
+
# minimum check we can do to not break a referential integrity.
|
962
|
+
return @fixed_foreign_keys_cache unless @fixed_foreign_keys_cache.nil?
|
963
|
+
|
964
|
+
manager_ref_set = (manager_ref - manager_ref_allowed_nil)
|
965
|
+
@fixed_foreign_keys_cache = manager_ref_set.map { |x| association_to_foreign_key_mapping[x] }.compact
|
966
|
+
@fixed_foreign_keys_cache += foreign_keys & manager_ref
|
967
|
+
@fixed_foreign_keys_cache.map!(&:to_sym)
|
968
|
+
@fixed_foreign_keys_cache
|
969
|
+
end
|
970
|
+
|
971
|
+
# @return [String] Base class name of the model_class of this InventoryCollection
|
972
|
+
def base_class_name
|
973
|
+
return "" unless model_class
|
974
|
+
|
975
|
+
@base_class_name ||= model_class.base_class.name
|
976
|
+
end
|
977
|
+
|
978
|
+
# @return [String] a concise form of the inventoryCollection for easy logging
|
979
|
+
def to_s
|
980
|
+
whitelist = ", whitelist: [#{attributes_whitelist.to_a.join(", ")}]" if attributes_whitelist.present?
|
981
|
+
blacklist = ", blacklist: [#{attributes_blacklist.to_a.join(", ")}]" if attributes_blacklist.present?
|
982
|
+
|
983
|
+
strategy_name = ", strategy: #{strategy}" if strategy
|
984
|
+
|
985
|
+
name = model_class || association
|
986
|
+
|
987
|
+
"InventoryCollection:<#{name}>#{whitelist}#{blacklist}#{strategy_name}"
|
988
|
+
end
|
989
|
+
|
990
|
+
# @return [String] a concise form of the InventoryCollection for easy logging
|
991
|
+
def inspect
|
992
|
+
to_s
|
993
|
+
end
|
994
|
+
|
995
|
+
# @return [Integer] default batch size for talking to the DB
|
996
|
+
def batch_size
|
997
|
+
# TODO(lsmola) mode to the settings
|
998
|
+
1000
|
999
|
+
end
|
1000
|
+
|
1001
|
+
# @return [Integer] default batch size for talking to the DB if not using ApplicationRecord objects
|
1002
|
+
def batch_size_pure_sql
|
1003
|
+
# TODO(lsmola) mode to the settings
|
1004
|
+
10_000
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
# Returns a list of stringified uuids of all scoped InventoryObjects, which is used for scoping in targeted mode
|
1008
|
+
#
|
1009
|
+
# @return [Array<String>] list of stringified uuids of all scoped InventoryObjects
|
1010
|
+
def manager_uuids
|
1011
|
+
# TODO(lsmola) LEGACY: this is still being used by :targetel_arel definitions and it expects array of strings
|
1012
|
+
raise "This works only for :manager_ref size 1" if manager_ref.size > 1
|
1013
|
+
key = manager_ref.first
|
1014
|
+
transform_references_to_hashes(targeted_scope.primary_references).map { |x| x[key] }
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
# Builds a multiselection conditions like (table1.a = a1 AND table2.b = b1) OR (table1.a = a2 AND table2.b = b2)
|
1018
|
+
#
|
1019
|
+
# @param hashes [Array<Hash>] data we want to use for the query
|
1020
|
+
# @param keys [Array<Symbol>] keys of attributes involved
|
1021
|
+
# @return [String] A condition usable in .where of an ActiveRecord relation
|
1022
|
+
def build_multi_selection_condition(hashes, keys = manager_ref)
|
1023
|
+
arel_table = model_class.arel_table
|
1024
|
+
# We do pure SQL OR, since Arel is nesting every .or into another parentheses, otherwise this would be just
|
1025
|
+
# inject(:or) instead of to_sql with .join(" OR ")
|
1026
|
+
hashes.map { |hash| "(#{keys.map { |key| arel_table[key].eq(hash[key]) }.inject(:and).to_sql})" }.join(" OR ")
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
# @return [ActiveRecord::Relation] A relation that can fetch all data of this InventoryCollection from the DB
|
1030
|
+
def db_collection_for_comparison
|
1031
|
+
if targeted?
|
1032
|
+
if targeted_arel.respond_to?(:call)
|
1033
|
+
targeted_arel.call(self)
|
1034
|
+
elsif parent_inventory_collections.present?
|
1035
|
+
targeted_arel_default
|
1036
|
+
else
|
1037
|
+
targeted_iterator_for(targeted_scope.primary_references)
|
1038
|
+
end
|
1039
|
+
else
|
1040
|
+
full_collection_for_comparison
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
# Builds targeted query limiting the results by the :references defined in parent_inventory_collections
|
1045
|
+
#
|
1046
|
+
# @return [InventoryRefresh::ApplicationRecordIterator] an iterator for default targeted arel
|
1047
|
+
def targeted_arel_default
|
1048
|
+
if parent_inventory_collections.collect { |x| x.model_class.base_class }.uniq.count > 1
|
1049
|
+
raise "Multiple :parent_inventory_collections with different base class are not supported by default. Write "\
|
1050
|
+
":targeted_arel manually, or separate [#{self}] into 2 InventoryCollection objects."
|
1051
|
+
end
|
1052
|
+
parent_collection = parent_inventory_collections.first
|
1053
|
+
references = parent_inventory_collections.map { |x| x.targeted_scope.primary_references }.reduce({}, :merge!)
|
1054
|
+
|
1055
|
+
parent_collection.targeted_iterator_for(references, full_collection_for_comparison)
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
# Gets targeted references and transforms them into list of hashes
|
1059
|
+
#
|
1060
|
+
# @param references [Array, InventoryRefresh::Inventorycollection::TargetedScope] passed references
|
1061
|
+
# @return [Array<Hash>] References transformed into the array of hashes
|
1062
|
+
def transform_references_to_hashes(references)
|
1063
|
+
if references.kind_of?(Array)
|
1064
|
+
# Sliced InventoryRefresh::Inventorycollection::TargetedScope
|
1065
|
+
references.map { |x| x.second.full_reference }
|
1066
|
+
else
|
1067
|
+
references.values.map(&:full_reference)
|
1068
|
+
end
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
# Builds a multiselection conditions like (table1.a = a1 AND table2.b = b1) OR (table1.a = a2 AND table2.b = b2)
|
1072
|
+
# for passed references
|
1073
|
+
#
|
1074
|
+
# @param references [Hash{String => InventoryRefresh::InventoryCollection::Reference}] passed references
|
1075
|
+
# @return [String] A condition usable in .where of an ActiveRecord relation
|
1076
|
+
def targeted_selection_for(references)
|
1077
|
+
build_multi_selection_condition(transform_references_to_hashes(references))
|
1078
|
+
end
|
1079
|
+
|
1080
|
+
# Returns iterator for the passed references and a query
|
1081
|
+
#
|
1082
|
+
# @param references [Hash{String => InventoryRefresh::InventoryCollection::Reference}] Passed references
|
1083
|
+
# @param query [ActiveRecord::Relation] relation that can fetch all data of this InventoryCollection from the DB
|
1084
|
+
# @return [InventoryRefresh::ApplicationRecordIterator] Iterator for the references and query
|
1085
|
+
def targeted_iterator_for(references, query = nil)
|
1086
|
+
InventoryRefresh::ApplicationRecordIterator.new(
|
1087
|
+
:inventory_collection => self,
|
1088
|
+
:manager_uuids_set => references,
|
1089
|
+
:query => query
|
1090
|
+
)
|
1091
|
+
end
|
1092
|
+
|
1093
|
+
# Builds an ActiveRecord::Relation that can fetch all the references from the DB
|
1094
|
+
#
|
1095
|
+
# @param references [Hash{String => InventoryRefresh::InventoryCollection::Reference}] passed references
|
1096
|
+
# @return [ActiveRecord::Relation] relation that can fetch all the references from the DB
|
1097
|
+
def db_collection_for_comparison_for(references)
|
1098
|
+
full_collection_for_comparison.where(targeted_selection_for(references))
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
# Builds an ActiveRecord::Relation that can fetch complement of all the references from the DB
|
1102
|
+
#
|
1103
|
+
# @param manager_uuids_set [Array<String>] passed references
|
1104
|
+
# @return [ActiveRecord::Relation] relation that can fetch complement of all the references from the DB
|
1105
|
+
def db_collection_for_comparison_for_complement_of(manager_uuids_set)
|
1106
|
+
# TODO(lsmola) this should have the build_multi_selection_condition, like in the method above
|
1107
|
+
# TODO(lsmola) this query will be highly ineffective, we will try approach with updating a timestamp of all
|
1108
|
+
# records, then we can get list of all records that were not update. That would be equivalent to result of this
|
1109
|
+
# more effective query and without need of all manager_uuids
|
1110
|
+
full_collection_for_comparison.where.not(manager_ref.first => manager_uuids_set)
|
1111
|
+
end
|
1112
|
+
|
1113
|
+
# @return [ActiveRecord::Relation] relation that can fetch all the references from the DB
|
1114
|
+
def full_collection_for_comparison
|
1115
|
+
return arel unless arel.nil?
|
1116
|
+
parent.send(association)
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
# Creates InventoryRefresh::InventoryObject object from passed hash data
|
1120
|
+
#
|
1121
|
+
# @param hash [Hash] Object data
|
1122
|
+
# @return [InventoryRefresh::InventoryObject] Instantiated InventoryRefresh::InventoryObject
|
1123
|
+
def new_inventory_object(hash)
|
1124
|
+
manager_ref.each do |x|
|
1125
|
+
# TODO(lsmola) with some effort, we can do this, but it's complex
|
1126
|
+
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
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
inventory_object_class.new(self, hash)
|
1130
|
+
end
|
1131
|
+
|
1132
|
+
attr_writer :attributes_blacklist, :attributes_whitelist
|
1133
|
+
|
1134
|
+
private
|
1135
|
+
|
1136
|
+
# Creates dynamically a subclass of InventoryRefresh::InventoryObject, that will be used per InventoryCollection
|
1137
|
+
# object. This approach is needed because we want different InventoryObject's getters&setters for each
|
1138
|
+
# InventoryCollection.
|
1139
|
+
#
|
1140
|
+
# @return [InventoryRefresh::InventoryObject] new isolated subclass of InventoryRefresh::InventoryObject
|
1141
|
+
def inventory_object_class
|
1142
|
+
@inventory_object_class ||= begin
|
1143
|
+
klass = Class.new(::InventoryRefresh::InventoryObject)
|
1144
|
+
klass.add_attributes(inventory_object_attributes) if inventory_object_attributes
|
1145
|
+
klass
|
1146
|
+
end
|
1147
|
+
end
|
1148
|
+
|
1149
|
+
# Returns array of records identities
|
1150
|
+
#
|
1151
|
+
# @param records [Array<ApplicationRecord>, Array[Hash]] list of stored records
|
1152
|
+
# @return [Array<Hash>] array of records identities
|
1153
|
+
def records_identities(records)
|
1154
|
+
records = [records] unless records.respond_to?(:map)
|
1155
|
+
records.map { |record| record_identity(record) }
|
1156
|
+
end
|
1157
|
+
|
1158
|
+
# Returns a hash with a simple record identity
|
1159
|
+
#
|
1160
|
+
# @param record [ApplicationRecord, Hash] list of stored records
|
1161
|
+
# @return [Hash{Symbol => Bigint}] record identity
|
1162
|
+
def record_identity(record)
|
1163
|
+
identity = record.try(:[], :id) || record.try(:[], "id") || record.try(:id)
|
1164
|
+
raise "Cannot obtain identity of the #{record}" if identity.blank?
|
1165
|
+
{
|
1166
|
+
:id => identity
|
1167
|
+
}
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
# @return [Array<Symbol>] all association attributes and foreign keys of the model class
|
1171
|
+
def association_attributes
|
1172
|
+
model_class.reflect_on_all_associations.map { |x| [x.name, x.foreign_key] }.flatten.compact.map(&:to_sym)
|
1173
|
+
end
|
1174
|
+
end
|
1175
|
+
end
|