inventory_refresh 0.3.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|