inventory_refresh 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|