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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +25 -30
  3. data/.github/workflows/ci.yaml +47 -0
  4. data/.rubocop.yml +3 -3
  5. data/.rubocop_cc.yml +3 -4
  6. data/.rubocop_local.yml +5 -2
  7. data/.whitesource +3 -0
  8. data/CHANGELOG.md +19 -0
  9. data/Gemfile +10 -4
  10. data/README.md +1 -2
  11. data/Rakefile +2 -2
  12. data/inventory_refresh.gemspec +9 -10
  13. data/lib/inventory_refresh/application_record_iterator.rb +25 -12
  14. data/lib/inventory_refresh/graph/topological_sort.rb +24 -26
  15. data/lib/inventory_refresh/graph.rb +2 -2
  16. data/lib/inventory_refresh/inventory_collection/builder.rb +37 -15
  17. data/lib/inventory_refresh/inventory_collection/data_storage.rb +9 -0
  18. data/lib/inventory_refresh/inventory_collection/helpers/initialize_helper.rb +147 -38
  19. data/lib/inventory_refresh/inventory_collection/helpers/questions_helper.rb +49 -5
  20. data/lib/inventory_refresh/inventory_collection/index/proxy.rb +35 -3
  21. data/lib/inventory_refresh/inventory_collection/index/type/base.rb +8 -0
  22. data/lib/inventory_refresh/inventory_collection/index/type/local_db.rb +2 -0
  23. data/lib/inventory_refresh/inventory_collection/index/type/skeletal.rb +1 -0
  24. data/lib/inventory_refresh/inventory_collection/reference.rb +1 -0
  25. data/lib/inventory_refresh/inventory_collection/references_storage.rb +17 -0
  26. data/lib/inventory_refresh/inventory_collection/scanner.rb +91 -3
  27. data/lib/inventory_refresh/inventory_collection/serialization.rb +16 -10
  28. data/lib/inventory_refresh/inventory_collection.rb +122 -64
  29. data/lib/inventory_refresh/inventory_object.rb +74 -40
  30. data/lib/inventory_refresh/inventory_object_lazy.rb +17 -10
  31. data/lib/inventory_refresh/null_logger.rb +2 -2
  32. data/lib/inventory_refresh/persister.rb +31 -65
  33. data/lib/inventory_refresh/save_collection/base.rb +4 -2
  34. data/lib/inventory_refresh/save_collection/saver/base.rb +114 -15
  35. data/lib/inventory_refresh/save_collection/saver/batch.rb +17 -0
  36. data/lib/inventory_refresh/save_collection/saver/concurrent_safe_batch.rb +129 -51
  37. data/lib/inventory_refresh/save_collection/saver/default.rb +57 -0
  38. data/lib/inventory_refresh/save_collection/saver/partial_upsert_helper.rb +2 -19
  39. data/lib/inventory_refresh/save_collection/saver/retention_helper.rb +68 -3
  40. data/lib/inventory_refresh/save_collection/saver/sql_helper.rb +125 -0
  41. data/lib/inventory_refresh/save_collection/saver/sql_helper_update.rb +10 -6
  42. data/lib/inventory_refresh/save_collection/saver/sql_helper_upsert.rb +28 -16
  43. data/lib/inventory_refresh/save_collection/sweeper.rb +17 -93
  44. data/lib/inventory_refresh/save_collection/topological_sort.rb +5 -5
  45. data/lib/inventory_refresh/save_inventory.rb +5 -12
  46. data/lib/inventory_refresh/target.rb +73 -0
  47. data/lib/inventory_refresh/target_collection.rb +92 -0
  48. data/lib/inventory_refresh/version.rb +1 -1
  49. data/lib/inventory_refresh.rb +2 -0
  50. metadata +42 -39
  51. data/.travis.yml +0 -23
  52. 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 data [Array<Object>] Array of objects that we want to map to DB table columns
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 self.attributes_with_keys(data, inventory_collection_scope = nil, all_attribute_keys = [], inventory_object = nil)
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(resource_timestamps resource_timestamps_max resource_timestamp).include?(k)
100
- next if %i(resource_counters resource_counters_max resource_counter).include?(k)
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
- unless defined_methods.include?(attr.to_sym)
150
- define_method(attr) do
151
- data[attr]
152
- end
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
- self.class.allowed?(inventory_collection_scope, key)
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
- self.class.loadable?(value)
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
- # and full reference must be present
114
- return if !primary? || full_reference.blank?
115
-
116
- # To avoid pre-creating invalid records all fields of a primary key must have non null value
117
- # TODO(lsmola) for composite keys, it's still valid to have one of the keys nil, figure out how to allow this. We
118
- # will need to scan the DB for NOT NULL constraint and allow it based on that. So we would move this check to
119
- # saving code, but this will require bigger change, since having the column nil means we will have to batch it
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,10 +3,10 @@ require "logger"
3
3
  module InventoryRefresh
4
4
  class NullLogger < Logger
5
5
  def initialize(*_args)
6
- end
6
+ end
7
7
 
8
8
  def add(*_args, &_block)
9
- end
9
+ end
10
10
 
11
11
  def debug?
12
12
  false
@@ -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
- def initialize(manager)
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
- self.define_collections_reader(method_name)
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 => sweep_scope_to_hash(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
- new(manager).tap do |persister|
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: #{collection['name']}" if inventory_collection.blank?
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 = sweep_scope_from_hash(persister_data['sweep_scope'], persister.collections)
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 assert_graph_integrity?
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 use_ar_object?
218
- true
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.id}'...")
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.id}'...Complete")
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.arel_attribute(@primary_key)
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
- @record_key_method = inventory_collection.pure_sql_record_fetching? ? :pure_sql_record_key : :ar_record_key
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 serializable_keys?
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
- unless inventory_collection.assert_graph_integrity
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
- unless inventory_collection.assert_graph_integrity
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