inventory_refresh 0.3.3 → 1.0.0
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 +4 -4
- data/.codeclimate.yml +25 -30
- data/.github/workflows/ci.yaml +47 -0
- data/.rubocop.yml +3 -3
- data/.rubocop_cc.yml +3 -4
- data/.rubocop_local.yml +5 -2
- data/.whitesource +3 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile +10 -4
- data/README.md +1 -2
- data/Rakefile +2 -2
- data/inventory_refresh.gemspec +9 -10
- data/lib/inventory_refresh/application_record_iterator.rb +25 -12
- data/lib/inventory_refresh/graph/topological_sort.rb +24 -26
- data/lib/inventory_refresh/graph.rb +2 -2
- data/lib/inventory_refresh/inventory_collection/builder.rb +37 -15
- data/lib/inventory_refresh/inventory_collection/data_storage.rb +9 -0
- data/lib/inventory_refresh/inventory_collection/helpers/initialize_helper.rb +147 -38
- data/lib/inventory_refresh/inventory_collection/helpers/questions_helper.rb +49 -5
- data/lib/inventory_refresh/inventory_collection/index/proxy.rb +35 -3
- data/lib/inventory_refresh/inventory_collection/index/type/base.rb +8 -0
- data/lib/inventory_refresh/inventory_collection/index/type/local_db.rb +2 -0
- data/lib/inventory_refresh/inventory_collection/index/type/skeletal.rb +1 -0
- data/lib/inventory_refresh/inventory_collection/reference.rb +1 -0
- data/lib/inventory_refresh/inventory_collection/references_storage.rb +17 -0
- data/lib/inventory_refresh/inventory_collection/scanner.rb +91 -3
- data/lib/inventory_refresh/inventory_collection/serialization.rb +16 -10
- data/lib/inventory_refresh/inventory_collection.rb +122 -64
- data/lib/inventory_refresh/inventory_object.rb +74 -40
- data/lib/inventory_refresh/inventory_object_lazy.rb +17 -10
- data/lib/inventory_refresh/null_logger.rb +2 -2
- data/lib/inventory_refresh/persister.rb +31 -65
- data/lib/inventory_refresh/save_collection/base.rb +4 -2
- data/lib/inventory_refresh/save_collection/saver/base.rb +114 -15
- data/lib/inventory_refresh/save_collection/saver/batch.rb +17 -0
- data/lib/inventory_refresh/save_collection/saver/concurrent_safe_batch.rb +129 -51
- data/lib/inventory_refresh/save_collection/saver/default.rb +57 -0
- data/lib/inventory_refresh/save_collection/saver/partial_upsert_helper.rb +2 -19
- data/lib/inventory_refresh/save_collection/saver/retention_helper.rb +68 -3
- data/lib/inventory_refresh/save_collection/saver/sql_helper.rb +125 -0
- data/lib/inventory_refresh/save_collection/saver/sql_helper_update.rb +10 -6
- data/lib/inventory_refresh/save_collection/saver/sql_helper_upsert.rb +28 -16
- data/lib/inventory_refresh/save_collection/sweeper.rb +17 -93
- data/lib/inventory_refresh/save_collection/topological_sort.rb +5 -5
- data/lib/inventory_refresh/save_inventory.rb +5 -12
- data/lib/inventory_refresh/target.rb +73 -0
- data/lib/inventory_refresh/target_collection.rb +92 -0
- data/lib/inventory_refresh/version.rb +1 -1
- data/lib/inventory_refresh.rb +2 -0
- metadata +42 -39
- data/.travis.yml +0 -23
- data/lib/inventory_refresh/exception.rb +0 -8
@@ -42,12 +42,64 @@ module InventoryRefresh
|
|
42
42
|
# Transforms InventoryObject object data into hash format with keys that are column names and resolves correct
|
43
43
|
# values of the foreign keys (even the polymorphic ones)
|
44
44
|
#
|
45
|
-
# @param
|
45
|
+
# @param inventory_collection_scope [InventoryRefresh::InventoryCollection] parent InventoryCollection object
|
46
|
+
# @return [Hash] Data in DB format
|
47
|
+
def attributes(inventory_collection_scope = nil)
|
48
|
+
# We should explicitly pass a scope, since the inventory_object can be mapped to more InventoryCollections with
|
49
|
+
# different blacklist and whitelist. The generic code always passes a scope.
|
50
|
+
inventory_collection_scope ||= inventory_collection
|
51
|
+
|
52
|
+
attributes_for_saving = {}
|
53
|
+
# First transform the values
|
54
|
+
data.each do |key, value|
|
55
|
+
if !allowed?(inventory_collection_scope, key)
|
56
|
+
next
|
57
|
+
elsif value.kind_of?(Array) && value.any? { |x| loadable?(x) }
|
58
|
+
# Lets fill also the original data, so other InventoryObject referring to this attribute gets the right
|
59
|
+
# result
|
60
|
+
data[key] = value.compact.map(&:load).compact
|
61
|
+
# We can use built in _ids methods to assign array of ids into has_many relations. So e.g. the :key_pairs=
|
62
|
+
# relation setter will become :key_pair_ids=
|
63
|
+
attributes_for_saving["#{key.to_s.singularize}_ids".to_sym] = data[key].map(&:id).compact.uniq
|
64
|
+
elsif loadable?(value) || inventory_collection_scope.association_to_foreign_key_mapping[key]
|
65
|
+
# Lets fill also the original data, so other InventoryObject referring to this attribute gets the right
|
66
|
+
# result
|
67
|
+
data[key] = value.load if value.respond_to?(:load)
|
68
|
+
if (foreign_key = inventory_collection_scope.association_to_foreign_key_mapping[key])
|
69
|
+
# We have an association to fill, lets fill also the :key, cause some other InventoryObject can refer to it
|
70
|
+
record_id = data[key].try(:id)
|
71
|
+
attributes_for_saving[foreign_key.to_sym] = record_id
|
72
|
+
|
73
|
+
if (foreign_type = inventory_collection_scope.association_to_foreign_type_mapping[key])
|
74
|
+
# If we have a polymorphic association, we need to also fill a base class name, but we want to nullify it
|
75
|
+
# if record_id is missing
|
76
|
+
base_class = data[key].try(:base_class_name) || data[key].class.try(:base_class).try(:name)
|
77
|
+
attributes_for_saving[foreign_type.to_sym] = record_id ? base_class : nil
|
78
|
+
end
|
79
|
+
elsif data[key].kind_of?(::InventoryRefresh::InventoryObject)
|
80
|
+
# We have an association to fill but not an Activerecord association, so e.g. Ancestry, lets just load
|
81
|
+
# it here. This way of storing ancestry is ineffective in DB call count, but RAM friendly
|
82
|
+
attributes_for_saving[key.to_sym] = data[key].base_class_name.constantize.find_by(:id => data[key].id)
|
83
|
+
else
|
84
|
+
# We have a normal attribute to fill
|
85
|
+
attributes_for_saving[key.to_sym] = data[key]
|
86
|
+
end
|
87
|
+
else
|
88
|
+
attributes_for_saving[key.to_sym] = value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
attributes_for_saving
|
93
|
+
end
|
94
|
+
|
95
|
+
# Transforms InventoryObject object data into hash format with keys that are column names and resolves correct
|
96
|
+
# values of the foreign keys (even the polymorphic ones)
|
97
|
+
#
|
46
98
|
# @param inventory_collection_scope [InventoryRefresh::InventoryCollection] parent InventoryCollection object
|
47
99
|
# @param all_attribute_keys [Array<Symbol>] Attribute keys we will modify based on object's data
|
48
100
|
# @param inventory_object [InventoryRefresh::InventoryObject] InventoryObject object owning these attributes
|
49
101
|
# @return [Hash] Data in DB format
|
50
|
-
def
|
102
|
+
def attributes_with_keys(inventory_collection_scope = nil, all_attribute_keys = [], inventory_object = nil)
|
51
103
|
# We should explicitly pass a scope, since the inventory_object can be mapped to more InventoryCollections with
|
52
104
|
# different blacklist and whitelist. The generic code always passes a scope.
|
53
105
|
inventory_collection_scope ||= inventory_collection
|
@@ -96,8 +148,8 @@ module InventoryRefresh
|
|
96
148
|
def assign_attributes(attributes)
|
97
149
|
attributes.each do |k, v|
|
98
150
|
# We don't want timestamps or resource versions to be overwritten here, since those are driving the conditions
|
99
|
-
next if %i
|
100
|
-
next if %i
|
151
|
+
next if %i[resource_timestamps resource_timestamps_max resource_timestamp].include?(k)
|
152
|
+
next if %i[resource_counters resource_counters_max resource_counter].include?(k)
|
101
153
|
|
102
154
|
if data[:resource_timestamp] && attributes[:resource_timestamp]
|
103
155
|
assign_only_newest(:resource_timestamp, :resource_timestamps, attributes, data, k, v)
|
@@ -146,44 +198,14 @@ module InventoryRefresh
|
|
146
198
|
end
|
147
199
|
end
|
148
200
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
201
|
+
next if defined_methods.include?(attr.to_sym)
|
202
|
+
|
203
|
+
define_method(attr) do
|
204
|
+
data[attr]
|
153
205
|
end
|
154
206
|
end
|
155
207
|
end
|
156
208
|
|
157
|
-
# Return true if the attribute is allowed to be saved into the DB
|
158
|
-
#
|
159
|
-
# @param inventory_collection_scope [InventoryRefresh::InventoryCollection] InventoryCollection object owning the
|
160
|
-
# attribute
|
161
|
-
# @param key [Symbol] attribute name
|
162
|
-
# @return true if the attribute is allowed to be saved into the DB
|
163
|
-
def self.allowed?(inventory_collection_scope, key)
|
164
|
-
foreign_to_association = (inventory_collection_scope.foreign_key_to_association_mapping[key] ||
|
165
|
-
inventory_collection_scope.foreign_type_to_association_mapping[key])
|
166
|
-
|
167
|
-
return false if inventory_collection_scope.attributes_blacklist.present? &&
|
168
|
-
(inventory_collection_scope.attributes_blacklist.include?(key) ||
|
169
|
-
(foreign_to_association && inventory_collection_scope.attributes_blacklist.include?(foreign_to_association)))
|
170
|
-
|
171
|
-
return false if inventory_collection_scope.attributes_whitelist.present? &&
|
172
|
-
(!inventory_collection_scope.attributes_whitelist.include?(key) &&
|
173
|
-
(!foreign_to_association || (foreign_to_association && inventory_collection_scope.attributes_whitelist.include?(foreign_to_association))))
|
174
|
-
|
175
|
-
true
|
176
|
-
end
|
177
|
-
|
178
|
-
# Return true if the object is loadable, which we determine by a list of loadable classes.
|
179
|
-
#
|
180
|
-
# @param value [Object] object we test
|
181
|
-
# @return true if the object is loadable
|
182
|
-
def self.loadable?(value)
|
183
|
-
value.kind_of?(::InventoryRefresh::InventoryObjectLazy) || value.kind_of?(::InventoryRefresh::InventoryObject) ||
|
184
|
-
value.kind_of?(::InventoryRefresh::ApplicationRecordReference)
|
185
|
-
end
|
186
|
-
|
187
209
|
private
|
188
210
|
|
189
211
|
# Assigns value based on the version attributes. If versions are specified, it asigns attribute only if it's
|
@@ -261,7 +283,18 @@ module InventoryRefresh
|
|
261
283
|
# @param key [Symbol] attribute name
|
262
284
|
# @return true if the attribute is allowed to be saved into the DB
|
263
285
|
def allowed?(inventory_collection_scope, key)
|
264
|
-
|
286
|
+
foreign_to_association = inventory_collection_scope.foreign_key_to_association_mapping[key] ||
|
287
|
+
inventory_collection_scope.foreign_type_to_association_mapping[key]
|
288
|
+
|
289
|
+
return false if inventory_collection_scope.attributes_blacklist.present? &&
|
290
|
+
(inventory_collection_scope.attributes_blacklist.include?(key) ||
|
291
|
+
(foreign_to_association && inventory_collection_scope.attributes_blacklist.include?(foreign_to_association)))
|
292
|
+
|
293
|
+
return false if inventory_collection_scope.attributes_whitelist.present? &&
|
294
|
+
(!inventory_collection_scope.attributes_whitelist.include?(key) &&
|
295
|
+
(!foreign_to_association || (foreign_to_association && inventory_collection_scope.attributes_whitelist.include?(foreign_to_association))))
|
296
|
+
|
297
|
+
true
|
265
298
|
end
|
266
299
|
|
267
300
|
# Return true if the object is loadable, which we determine by a list of loadable classes.
|
@@ -269,7 +302,8 @@ module InventoryRefresh
|
|
269
302
|
# @param value [Object] object we test
|
270
303
|
# @return true if the object is loadable
|
271
304
|
def loadable?(value)
|
272
|
-
|
305
|
+
value.kind_of?(::InventoryRefresh::InventoryObjectLazy) || value.kind_of?(::InventoryRefresh::InventoryObject) ||
|
306
|
+
value.kind_of?(::InventoryRefresh::ApplicationRecordReference)
|
273
307
|
end
|
274
308
|
end
|
275
309
|
end
|
@@ -30,6 +30,7 @@ module InventoryRefresh
|
|
30
30
|
|
31
31
|
# @return [String] stringified reference
|
32
32
|
def to_s
|
33
|
+
# TODO(lsmola) do we need this method?
|
33
34
|
stringified_reference
|
34
35
|
end
|
35
36
|
|
@@ -70,6 +71,10 @@ module InventoryRefresh
|
|
70
71
|
|
71
72
|
# @return [Boolean] true if the key is an association on inventory_collection_scope model class
|
72
73
|
def association?(key)
|
74
|
+
# TODO(lsmola) remove this if there will be better dependency scan, probably with transitive dependencies filled
|
75
|
+
# in a second pass, then we can get rid of this hardcoded symbols. Right now we are not able to introspect these.
|
76
|
+
return true if [:parent, :genealogy_parent].include?(key)
|
77
|
+
|
73
78
|
inventory_collection.dependency_attributes.key?(key) ||
|
74
79
|
!inventory_collection.association_to_foreign_key_mapping[key].nil?
|
75
80
|
end
|
@@ -95,7 +100,7 @@ module InventoryRefresh
|
|
95
100
|
|
96
101
|
private
|
97
102
|
|
98
|
-
delegate :saved?, :saver_strategy, :skeletal_primary_index, :to => :inventory_collection
|
103
|
+
delegate :parallel_safe?, :saved?, :saver_strategy, :skeletal_primary_index, :targeted?, :to => :inventory_collection
|
99
104
|
delegate :nested_secondary_index?, :primary?, :full_reference, :keys, :primary?, :to => :reference
|
100
105
|
|
101
106
|
attr_writer :reference
|
@@ -107,18 +112,20 @@ module InventoryRefresh
|
|
107
112
|
#
|
108
113
|
# @return [InventoryRefresh::InventoryObject, NilClass] Returns pre-created InventoryObject or nil
|
109
114
|
def skeletal_precreate!
|
115
|
+
# We can do skeletal pre-create only for strategies using unique indexes. Since this can build records out of
|
116
|
+
# the given :arel scope, we will always attempt to create the recod, so we need unique index to avoid duplication
|
117
|
+
# of records.
|
118
|
+
return unless parallel_safe?
|
110
119
|
# Pre-create only for strategies that will be persisting data, i.e. are not saved already
|
111
120
|
return if saved?
|
112
121
|
# We can only do skeletal pre-create for primary index reference, since that is needed to create DB unique index
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
#
|
118
|
-
#
|
119
|
-
|
120
|
-
# smartly, since having nil means we will need to use different unique index for the upsert/update query.
|
121
|
-
return if keys.any? { |x| full_reference[x].nil? }
|
122
|
+
return unless primary?
|
123
|
+
# Full reference must be present
|
124
|
+
return if full_reference.blank?
|
125
|
+
|
126
|
+
# To avoid pre-creating invalid records all fields of a primary key must have value
|
127
|
+
# TODO(lsmola) for composite keys, it's still valid to have one of the keys nil, figure out how to allow this
|
128
|
+
return if keys.any? { |x| full_reference[x].blank? }
|
122
129
|
|
123
130
|
skeletal_primary_index.build(full_reference)
|
124
131
|
end
|
@@ -3,13 +3,15 @@ module InventoryRefresh
|
|
3
3
|
require 'json'
|
4
4
|
require 'yaml'
|
5
5
|
|
6
|
-
attr_reader :manager, :collections
|
6
|
+
attr_reader :manager, :target, :collections
|
7
7
|
|
8
8
|
attr_accessor :refresh_state_uuid, :refresh_state_part_uuid, :total_parts, :sweep_scope, :retry_count, :retry_max
|
9
9
|
|
10
10
|
# @param manager [ManageIQ::Providers::BaseManager] A manager object
|
11
|
-
|
11
|
+
# @param target [Object] A refresh Target object
|
12
|
+
def initialize(manager, target = nil)
|
12
13
|
@manager = manager
|
14
|
+
@target = target
|
13
15
|
|
14
16
|
@collections = {}
|
15
17
|
|
@@ -43,6 +45,9 @@ module InventoryRefresh
|
|
43
45
|
&block)
|
44
46
|
|
45
47
|
builder.add_properties(extra_properties) if extra_properties.present?
|
48
|
+
|
49
|
+
builder.add_properties({:manager_uuids => target.try(:references, collection_name) || []}, :if_missing) if targeted?
|
50
|
+
|
46
51
|
builder.evaluate_lambdas!(self)
|
47
52
|
|
48
53
|
collections[collection_name] = builder.to_inventory_collection
|
@@ -61,7 +66,7 @@ module InventoryRefresh
|
|
61
66
|
# @return [InventoryRefresh::InventoryCollection] returns a defined InventoryCollection or undefined method
|
62
67
|
def method_missing(method_name, *arguments, &block)
|
63
68
|
if inventory_collections_names.include?(method_name)
|
64
|
-
|
69
|
+
define_collections_reader(method_name)
|
65
70
|
send(method_name)
|
66
71
|
else
|
67
72
|
super
|
@@ -91,7 +96,7 @@ module InventoryRefresh
|
|
91
96
|
|
92
97
|
# Returns serialized Persisted object to JSON
|
93
98
|
# @return [String] serialized Persisted object to JSON
|
94
|
-
def to_json
|
99
|
+
def to_json(*_args)
|
95
100
|
JSON.dump(to_hash)
|
96
101
|
end
|
97
102
|
|
@@ -99,6 +104,8 @@ module InventoryRefresh
|
|
99
104
|
def to_hash
|
100
105
|
collections_data = collections.map do |_, collection|
|
101
106
|
next if collection.data.blank? &&
|
107
|
+
collection.targeted_scope.primary_references.blank? &&
|
108
|
+
collection.all_manager_uuids.nil? &&
|
102
109
|
collection.skeletal_primary_index.index_data.blank?
|
103
110
|
|
104
111
|
collection.to_hash
|
@@ -110,7 +117,7 @@ module InventoryRefresh
|
|
110
117
|
:retry_count => retry_count,
|
111
118
|
:retry_max => retry_max,
|
112
119
|
:total_parts => total_parts,
|
113
|
-
:sweep_scope =>
|
120
|
+
:sweep_scope => sweep_scope,
|
114
121
|
:collections => collections_data,
|
115
122
|
}
|
116
123
|
end
|
@@ -120,19 +127,22 @@ module InventoryRefresh
|
|
120
127
|
#
|
121
128
|
# @param json_data [String] input JSON data
|
122
129
|
# @return [ManageIQ::Providers::Inventory::Persister] Persister object loaded from a passed JSON
|
123
|
-
def from_json(json_data, manager)
|
124
|
-
from_hash(JSON.parse(json_data), manager)
|
130
|
+
def from_json(json_data, manager, target = nil)
|
131
|
+
from_hash(JSON.parse(json_data), manager, target)
|
125
132
|
end
|
126
133
|
|
127
134
|
# Returns Persister object built from serialized data
|
128
135
|
#
|
129
136
|
# @param persister_data [Hash] serialized Persister object in hash
|
130
137
|
# @return [ManageIQ::Providers::Inventory::Persister] Persister object built from serialized data
|
131
|
-
def from_hash(persister_data, manager)
|
132
|
-
|
138
|
+
def from_hash(persister_data, manager, target = nil)
|
139
|
+
# TODO(lsmola) we need to pass serialized targeted scope here
|
140
|
+
target ||= InventoryRefresh::TargetCollection.new(:manager => manager)
|
141
|
+
|
142
|
+
new(manager, target).tap do |persister|
|
133
143
|
persister_data['collections'].each do |collection|
|
134
144
|
inventory_collection = persister.collections[collection['name'].try(:to_sym)]
|
135
|
-
raise "Unrecognized InventoryCollection name: #{
|
145
|
+
raise "Unrecognized InventoryCollection name: #{inventory_collection}" if inventory_collection.blank?
|
136
146
|
|
137
147
|
inventory_collection.from_hash(collection, persister.collections)
|
138
148
|
end
|
@@ -142,44 +152,9 @@ module InventoryRefresh
|
|
142
152
|
persister.retry_count = persister_data['retry_count']
|
143
153
|
persister.retry_max = persister_data['retry_max']
|
144
154
|
persister.total_parts = persister_data['total_parts']
|
145
|
-
persister.sweep_scope =
|
155
|
+
persister.sweep_scope = persister_data['sweep_scope']
|
146
156
|
end
|
147
157
|
end
|
148
|
-
|
149
|
-
private
|
150
|
-
|
151
|
-
def assert_sweep_scope!(sweep_scope)
|
152
|
-
return unless sweep_scope
|
153
|
-
|
154
|
-
allowed_format_message = "Allowed format of sweep scope is Array<String> or Hash{String => Hash}, got #{sweep_scope}"
|
155
|
-
|
156
|
-
if sweep_scope.kind_of?(Array)
|
157
|
-
return if sweep_scope.all? { |x| x.kind_of?(String) || x.kind_of?(Symbol) }
|
158
|
-
|
159
|
-
raise InventoryRefresh::Exception::SweeperScopeBadFormat, allowed_format_message
|
160
|
-
elsif sweep_scope.kind_of?(Hash)
|
161
|
-
return if sweep_scope.values.all? { |x| x.kind_of?(Array) }
|
162
|
-
|
163
|
-
raise InventoryRefresh::Exception::SweeperScopeBadFormat, allowed_format_message
|
164
|
-
end
|
165
|
-
|
166
|
-
raise InventoryRefresh::Exception::SweeperScopeBadFormat, allowed_format_message
|
167
|
-
end
|
168
|
-
|
169
|
-
def sweep_scope_from_hash(sweep_scope, available_inventory_collections)
|
170
|
-
assert_sweep_scope!(sweep_scope)
|
171
|
-
|
172
|
-
return sweep_scope unless sweep_scope.kind_of?(Hash)
|
173
|
-
|
174
|
-
sweep_scope.each_with_object({}) do |(k, v), obj|
|
175
|
-
inventory_collection = available_inventory_collections[k.try(:to_sym)]
|
176
|
-
raise "Unrecognized InventoryCollection name: #{k}" if inventory_collection.blank?
|
177
|
-
|
178
|
-
serializer = InventoryRefresh::InventoryCollection::Serialization.new(inventory_collection)
|
179
|
-
|
180
|
-
obj[k] = serializer.sweep_scope_from_hash(v, available_inventory_collections)
|
181
|
-
end.symbolize_keys!
|
182
|
-
end
|
183
158
|
end
|
184
159
|
|
185
160
|
protected
|
@@ -210,37 +185,28 @@ module InventoryRefresh
|
|
210
185
|
nil
|
211
186
|
end
|
212
187
|
|
213
|
-
def
|
188
|
+
def saver_strategy
|
189
|
+
:default
|
190
|
+
end
|
191
|
+
|
192
|
+
# Persisters for targeted refresh can override to true
|
193
|
+
def targeted?
|
214
194
|
false
|
215
195
|
end
|
216
196
|
|
217
|
-
def
|
218
|
-
|
197
|
+
def assert_graph_integrity?
|
198
|
+
false
|
219
199
|
end
|
220
200
|
|
221
201
|
# @return [Hash] kwargs shared for all InventoryCollection objects
|
222
202
|
def shared_options
|
223
203
|
{
|
204
|
+
:saver_strategy => saver_strategy,
|
224
205
|
:strategy => strategy,
|
206
|
+
:targeted => targeted?,
|
225
207
|
:parent => manager.presence,
|
226
208
|
:assert_graph_integrity => assert_graph_integrity?,
|
227
|
-
:use_ar_object => use_ar_object?,
|
228
209
|
}
|
229
210
|
end
|
230
|
-
|
231
|
-
private
|
232
|
-
|
233
|
-
def sweep_scope_to_hash(sweep_scope)
|
234
|
-
return sweep_scope unless sweep_scope.kind_of?(Hash)
|
235
|
-
|
236
|
-
sweep_scope.each_with_object({}) do |(k, v), obj|
|
237
|
-
inventory_collection = collections[k.try(:to_sym)]
|
238
|
-
raise "Unrecognized InventoryCollection name: #{k}" if inventory_collection.blank?
|
239
|
-
|
240
|
-
serializer = InventoryRefresh::InventoryCollection::Serialization.new(inventory_collection)
|
241
|
-
|
242
|
-
obj[k] = serializer.sweep_scope_to_hash(v)
|
243
|
-
end
|
244
|
-
end
|
245
211
|
end
|
246
212
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require "inventory_refresh/logging"
|
2
|
+
require "inventory_refresh/save_collection/saver/batch"
|
2
3
|
require "inventory_refresh/save_collection/saver/concurrent_safe_batch"
|
4
|
+
require "inventory_refresh/save_collection/saver/default"
|
3
5
|
|
4
6
|
module InventoryRefresh::SaveCollection
|
5
7
|
class Base
|
@@ -14,7 +16,7 @@ module InventoryRefresh::SaveCollection
|
|
14
16
|
return if skip?(inventory_collection)
|
15
17
|
|
16
18
|
logger.debug("----- BEGIN ----- Saving collection #{inventory_collection} of size #{inventory_collection.size} to"\
|
17
|
-
" the database, for the manager: '#{ems.
|
19
|
+
" the database, for the manager: '#{ems.name}'...")
|
18
20
|
|
19
21
|
if inventory_collection.custom_save_block.present?
|
20
22
|
logger.debug("Saving collection #{inventory_collection} using a custom save block")
|
@@ -22,7 +24,7 @@ module InventoryRefresh::SaveCollection
|
|
22
24
|
else
|
23
25
|
save_inventory(inventory_collection)
|
24
26
|
end
|
25
|
-
logger.debug("----- END ----- Saving collection #{inventory_collection}, for the manager: '#{ems.
|
27
|
+
logger.debug("----- END ----- Saving collection #{inventory_collection}, for the manager: '#{ems.name}'...Complete")
|
26
28
|
inventory_collection.saved = true
|
27
29
|
end
|
28
30
|
|
@@ -12,6 +12,8 @@ module InventoryRefresh::SaveCollection
|
|
12
12
|
# @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object we will be saving
|
13
13
|
def initialize(inventory_collection)
|
14
14
|
@inventory_collection = inventory_collection
|
15
|
+
# TODO(lsmola) do I need to reload every time? Also it should be enough to clear the associations.
|
16
|
+
inventory_collection.parent&.reload
|
15
17
|
@association = inventory_collection.db_collection_for_comparison
|
16
18
|
|
17
19
|
# Private attrs
|
@@ -19,17 +21,21 @@ module InventoryRefresh::SaveCollection
|
|
19
21
|
@table_name = @model_class.table_name
|
20
22
|
@q_table_name = get_connection.quote_table_name(@table_name)
|
21
23
|
@primary_key = @model_class.primary_key
|
22
|
-
@arel_primary_key = @model_class.
|
24
|
+
@arel_primary_key = @model_class.arel_table[@primary_key]
|
23
25
|
@unique_index_keys = inventory_collection.unique_index_keys
|
24
26
|
@unique_index_keys_to_s = inventory_collection.manager_ref_to_cols.map(&:to_s)
|
25
27
|
@select_keys = [@primary_key] + @unique_index_keys_to_s + internal_columns.map(&:to_s)
|
26
28
|
@unique_db_primary_keys = Set.new
|
27
29
|
@unique_db_indexes = Set.new
|
28
30
|
|
31
|
+
# Right now ApplicationRecordIterator in association is used for targeted refresh. Given the small amount of
|
32
|
+
# records flowing through there, we probably don't need to optimize that association to fetch a pure SQL.
|
33
|
+
@pure_sql_records_fetching = !inventory_collection.use_ar_object? && !@association.kind_of?(InventoryRefresh::ApplicationRecordIterator)
|
34
|
+
|
29
35
|
@batch_size_for_persisting = inventory_collection.batch_size_pure_sql
|
30
|
-
@batch_size = inventory_collection.use_ar_object? ? @batch_size_for_persisting : inventory_collection.batch_size
|
31
36
|
|
32
|
-
@
|
37
|
+
@batch_size = @pure_sql_records_fetching ? @batch_size_for_persisting : inventory_collection.batch_size
|
38
|
+
@record_key_method = @pure_sql_records_fetching ? :pure_sql_record_key : :ar_record_key
|
33
39
|
@select_keys_indexes = @select_keys.each_with_object({}).with_index { |(key, obj), index| obj[key.to_s] = index }
|
34
40
|
@pg_types = @model_class.attribute_names.each_with_object({}) do |key, obj|
|
35
41
|
obj[key.to_sym] = inventory_collection.model_class.columns_hash[key]
|
@@ -69,8 +75,14 @@ module InventoryRefresh::SaveCollection
|
|
69
75
|
|
70
76
|
# Saves the InventoryCollection
|
71
77
|
def save_inventory_collection!
|
78
|
+
# If we have a targeted InventoryCollection that wouldn't do anything, quickly skip it
|
79
|
+
return if inventory_collection.noop?
|
80
|
+
|
81
|
+
# Delete_complement strategy using :all_manager_uuids attribute
|
82
|
+
delete_complement unless inventory_collection.delete_complement_noop?
|
83
|
+
|
72
84
|
# Create/Update/Archive/Delete records based on InventoryCollection data and scope
|
73
|
-
save!(association)
|
85
|
+
save!(association) unless inventory_collection.saving_noop?
|
74
86
|
end
|
75
87
|
|
76
88
|
protected
|
@@ -89,8 +101,6 @@ module InventoryRefresh::SaveCollection
|
|
89
101
|
# @param attributes [Hash] attributes hash
|
90
102
|
# @return [Hash] modified hash from parameter attributes with casted values
|
91
103
|
def values_for_database!(all_attribute_keys, attributes)
|
92
|
-
# TODO(lsmola) we'll need to fill default value from the DB to the NOT_NULL columns here, since sending NULL
|
93
|
-
# to column with NOT_NULL constraint always fails, even if there is a default value
|
94
104
|
all_attribute_keys.each do |key|
|
95
105
|
next unless attributes.key?(key)
|
96
106
|
|
@@ -102,7 +112,11 @@ module InventoryRefresh::SaveCollection
|
|
102
112
|
end
|
103
113
|
|
104
114
|
def transform_to_hash!(all_attribute_keys, hash)
|
105
|
-
if
|
115
|
+
if inventory_collection.use_ar_object?
|
116
|
+
record = inventory_collection.model_class.new(hash)
|
117
|
+
values_for_database!(all_attribute_keys,
|
118
|
+
record.attributes.slice(*record.changed_attributes.keys).symbolize_keys)
|
119
|
+
elsif serializable_keys?
|
106
120
|
values_for_database!(all_attribute_keys,
|
107
121
|
hash)
|
108
122
|
else
|
@@ -113,15 +127,99 @@ module InventoryRefresh::SaveCollection
|
|
113
127
|
private
|
114
128
|
|
115
129
|
attr_reader :unique_index_keys, :unique_index_keys_to_s, :select_keys, :unique_db_primary_keys, :unique_db_indexes,
|
116
|
-
:primary_key, :arel_primary_key, :record_key_method, :select_keys_indexes,
|
130
|
+
:primary_key, :arel_primary_key, :record_key_method, :pure_sql_records_fetching, :select_keys_indexes,
|
117
131
|
:batch_size, :batch_size_for_persisting, :model_class, :serializable_keys, :deserializable_keys, :pg_types, :table_name,
|
118
132
|
:q_table_name
|
119
133
|
|
120
134
|
delegate :supports_column?, :to => :inventory_collection
|
121
135
|
|
136
|
+
# Saves the InventoryCollection
|
137
|
+
#
|
138
|
+
# @param association [Symbol] An existing association on manager
|
139
|
+
def save!(association)
|
140
|
+
attributes_index = {}
|
141
|
+
inventory_objects_index = {}
|
142
|
+
inventory_collection.each do |inventory_object|
|
143
|
+
attributes = inventory_object.attributes(inventory_collection)
|
144
|
+
index = build_stringified_reference(attributes, unique_index_keys)
|
145
|
+
|
146
|
+
attributes_index[index] = attributes
|
147
|
+
inventory_objects_index[index] = inventory_object
|
148
|
+
end
|
149
|
+
|
150
|
+
logger.debug("Processing #{inventory_collection} of size #{inventory_collection.size}...")
|
151
|
+
# Records that are in the DB, we will be updating or deleting them.
|
152
|
+
ActiveRecord::Base.transaction do
|
153
|
+
association.find_each do |record|
|
154
|
+
index = build_stringified_reference_for_record(record, unique_index_keys)
|
155
|
+
|
156
|
+
next unless assert_distinct_relation(record.id)
|
157
|
+
next unless assert_unique_record(record, index)
|
158
|
+
|
159
|
+
inventory_object = inventory_objects_index.delete(index)
|
160
|
+
hash = attributes_index.delete(index)
|
161
|
+
|
162
|
+
if inventory_object.nil?
|
163
|
+
# Record was found in the DB but not sent for saving, that means it doesn't exist anymore and we should
|
164
|
+
# delete it from the DB.
|
165
|
+
delete_record!(record) if inventory_collection.delete_allowed?
|
166
|
+
elsif assert_referential_integrity(hash)
|
167
|
+
# Record was found in the DB and sent for saving, we will be updating the DB.
|
168
|
+
update_record!(record, hash, inventory_object)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
inventory_collection.custom_reconnect_block&.call(inventory_collection, inventory_objects_index, attributes_index)
|
174
|
+
|
175
|
+
# Records that were not found in the DB but sent for saving, we will be creating these in the DB.
|
176
|
+
if inventory_collection.create_allowed?
|
177
|
+
ActiveRecord::Base.transaction do
|
178
|
+
inventory_objects_index.each do |index, inventory_object|
|
179
|
+
hash = attributes_index.delete(index)
|
180
|
+
|
181
|
+
create_record!(hash, inventory_object) if assert_referential_integrity(hash)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
logger.debug("Processing #{inventory_collection}, "\
|
186
|
+
"created=#{inventory_collection.created_records.count}, "\
|
187
|
+
"updated=#{inventory_collection.updated_records.count}, "\
|
188
|
+
"deleted=#{inventory_collection.deleted_records.count}...Complete")
|
189
|
+
rescue => e
|
190
|
+
logger.error("Error when saving #{inventory_collection} with #{inventory_collection_details}. Message: #{e.message}")
|
191
|
+
raise e
|
192
|
+
end
|
193
|
+
|
122
194
|
# @return [String] a string for logging purposes
|
123
195
|
def inventory_collection_details
|
124
|
-
"strategy: #{inventory_collection.strategy}, saver_strategy: #{inventory_collection.saver_strategy}"
|
196
|
+
"strategy: #{inventory_collection.strategy}, saver_strategy: #{inventory_collection.saver_strategy}, targeted: #{inventory_collection.targeted?}"
|
197
|
+
end
|
198
|
+
|
199
|
+
# @param record [ApplicationRecord] ApplicationRecord object
|
200
|
+
# @param key [Symbol] A key that is an attribute of the AR object
|
201
|
+
# @return [Object] Value of attribute name :key on the :record
|
202
|
+
def record_key(record, key)
|
203
|
+
record.public_send(key)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Deletes a complement of referenced data
|
207
|
+
def delete_complement
|
208
|
+
raise(":delete_complement method is supported only for :saver_strategy => [:batch, :concurrent_safe_batch]")
|
209
|
+
end
|
210
|
+
|
211
|
+
# Deletes/soft-deletes a given record
|
212
|
+
#
|
213
|
+
# @param [ApplicationRecord] record we want to delete
|
214
|
+
def delete_record!(record)
|
215
|
+
record.public_send(inventory_collection.delete_method)
|
216
|
+
inventory_collection.store_deleted_records(record)
|
217
|
+
end
|
218
|
+
|
219
|
+
# @return [TrueClass] always return true, this method is redefined in default saver
|
220
|
+
def assert_unique_record(_record, _index)
|
221
|
+
# TODO(lsmola) can go away once we indexed our DB with unique indexes
|
222
|
+
true
|
125
223
|
end
|
126
224
|
|
127
225
|
# Check if relation provided is distinct, i.e. the relation should not return the same primary key value twice.
|
@@ -134,12 +232,12 @@ module InventoryRefresh::SaveCollection
|
|
134
232
|
# Change the InventoryCollection's :association or :arel parameter to return distinct results. The :through
|
135
233
|
# relations can return the same record multiple times. We don't want to do SELECT DISTINCT by default, since
|
136
234
|
# it can be very slow.
|
137
|
-
|
235
|
+
if inventory_collection.assert_graph_integrity
|
236
|
+
raise("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. ")
|
237
|
+
else
|
138
238
|
logger.warn("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. "\
|
139
239
|
" The duplicate value is being ignored.")
|
140
240
|
return false
|
141
|
-
else
|
142
|
-
raise("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. ")
|
143
241
|
end
|
144
242
|
else
|
145
243
|
unique_db_primary_keys << primary_key_value
|
@@ -157,14 +255,15 @@ module InventoryRefresh::SaveCollection
|
|
157
255
|
def assert_referential_integrity(hash)
|
158
256
|
inventory_collection.fixed_foreign_keys.each do |x|
|
159
257
|
next unless hash[x].nil?
|
258
|
+
|
160
259
|
subject = "#{hash} of #{inventory_collection} because of missing foreign key #{x} for "\
|
161
260
|
"#{inventory_collection.parent.class.name}:"\
|
162
261
|
"#{inventory_collection.parent.try(:id)}"
|
163
|
-
|
262
|
+
if inventory_collection.assert_graph_integrity
|
263
|
+
raise("Referential integrity check violated for #{subject}")
|
264
|
+
else
|
164
265
|
logger.warn("Referential integrity check violated, ignoring #{subject}")
|
165
266
|
return false
|
166
|
-
else
|
167
|
-
raise("Referential integrity check violated for #{subject}")
|
168
267
|
end
|
169
268
|
end
|
170
269
|
true
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "inventory_refresh/save_collection/saver/concurrent_safe_batch"
|
2
|
+
|
3
|
+
module InventoryRefresh::SaveCollection
|
4
|
+
module Saver
|
5
|
+
class Batch < InventoryRefresh::SaveCollection::Saver::ConcurrentSafeBatch
|
6
|
+
private
|
7
|
+
|
8
|
+
# Just returning manager ref transformed to column names, for strategies that do not expect to have unique DB
|
9
|
+
# indexes.
|
10
|
+
#
|
11
|
+
# @return [Array<Symbol>] manager ref transformed to column names
|
12
|
+
def unique_index_columns
|
13
|
+
inventory_collection.manager_ref_to_cols.map(&:to_sym)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|