inventory_refresh 0.3.6 → 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 +8 -9
- 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 +48 -4
- 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 +43 -93
- 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 +34 -37
- data/.travis.yml +0 -23
- data/lib/inventory_refresh/exception.rb +0 -8
@@ -3,6 +3,28 @@ module InventoryRefresh::SaveCollection
|
|
3
3
|
module RetentionHelper
|
4
4
|
private
|
5
5
|
|
6
|
+
# Deletes a complement of referenced data
|
7
|
+
def delete_complement
|
8
|
+
return unless inventory_collection.delete_allowed?
|
9
|
+
|
10
|
+
all_manager_uuids_size = inventory_collection.all_manager_uuids.size
|
11
|
+
|
12
|
+
logger.debug("Processing :delete_complement of #{inventory_collection} of size "\
|
13
|
+
"#{all_manager_uuids_size}...")
|
14
|
+
|
15
|
+
query = complement_of!(inventory_collection.all_manager_uuids,
|
16
|
+
inventory_collection.all_manager_uuids_scope,
|
17
|
+
inventory_collection.all_manager_uuids_timestamp)
|
18
|
+
|
19
|
+
ids_of_non_active_entities = ActiveRecord::Base.connection.execute(query.to_sql).to_a
|
20
|
+
ids_of_non_active_entities.each_slice(10_000) do |batch|
|
21
|
+
destroy_records!(batch)
|
22
|
+
end
|
23
|
+
|
24
|
+
logger.debug("Processing :delete_complement of #{inventory_collection} of size "\
|
25
|
+
"#{all_manager_uuids_size}, deleted=#{inventory_collection.deleted_records.size}...Complete")
|
26
|
+
end
|
27
|
+
|
6
28
|
# Applies strategy based on :retention_strategy parameter, or fallbacks to legacy_destroy_records.
|
7
29
|
#
|
8
30
|
# @param records [Array<ApplicationRecord, Hash, Array>] Records we want to delete or archive
|
@@ -13,9 +35,13 @@ module InventoryRefresh::SaveCollection
|
|
13
35
|
return false unless inventory_collection.delete_allowed?
|
14
36
|
return if records.blank?
|
15
37
|
|
16
|
-
|
17
|
-
|
18
|
-
|
38
|
+
if inventory_collection.retention_strategy
|
39
|
+
ids = ids_array(records)
|
40
|
+
inventory_collection.store_deleted_records(ids)
|
41
|
+
send("#{inventory_collection.retention_strategy}_all_records!", ids)
|
42
|
+
else
|
43
|
+
legacy_destroy_records!(records)
|
44
|
+
end
|
19
45
|
end
|
20
46
|
|
21
47
|
# Convert records to list of ids in format [{:id => X}, {:id => Y}...]
|
@@ -45,6 +71,45 @@ module InventoryRefresh::SaveCollection
|
|
45
71
|
def destroy_all_records!(records)
|
46
72
|
inventory_collection.model_class.where(:id => records.map { |x| x[:id] }).delete_all
|
47
73
|
end
|
74
|
+
|
75
|
+
# Deletes or sof-deletes records. If the model_class supports a custom class delete method, we will use it for
|
76
|
+
# batch soft-delete. This is the legacy method doing either ineffective deletion/archiving or requiring a method
|
77
|
+
# on a class.
|
78
|
+
#
|
79
|
+
# @param records [Array<ApplicationRecord, Hash>] Records we want to delete. If we have only hashes, we need to
|
80
|
+
# to fetch ApplicationRecord objects from the DB
|
81
|
+
def legacy_destroy_records!(records)
|
82
|
+
# Is the delete_method rails standard deleting method?
|
83
|
+
rails_delete = %i[destroy delete].include?(inventory_collection.delete_method)
|
84
|
+
if !rails_delete && inventory_collection.model_class.respond_to?(inventory_collection.delete_method)
|
85
|
+
# We have custom delete method defined on a class, that means it supports batch destroy
|
86
|
+
inventory_collection.store_deleted_records(records.map { |x| {:id => record_key(x, primary_key)} })
|
87
|
+
inventory_collection.model_class.public_send(inventory_collection.delete_method, records.map { |x| record_key(x, primary_key) })
|
88
|
+
else
|
89
|
+
legacy_ineffective_destroy_records(records)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Very ineffective way of deleting records, but is needed if we want to invoke hooks.
|
94
|
+
#
|
95
|
+
# @param records [Array<ApplicationRecord, Hash>] Records we want to delete. If we have only hashes, we need to
|
96
|
+
# to fetch ApplicationRecord objects from the DB
|
97
|
+
def legacy_ineffective_destroy_records(records)
|
98
|
+
# We have either standard :destroy and :delete rails method, or custom instance level delete method
|
99
|
+
# Note: The standard :destroy and :delete rails method can't be batched because of the hooks and cascade destroy
|
100
|
+
ActiveRecord::Base.transaction do
|
101
|
+
if pure_sql_records_fetching
|
102
|
+
# For pure SQL fetching, we need to get the AR objects again, so we can call destroy
|
103
|
+
inventory_collection.model_class.where(:id => records.map { |x| record_key(x, primary_key) }).find_each do |record|
|
104
|
+
delete_record!(record)
|
105
|
+
end
|
106
|
+
else
|
107
|
+
records.each do |record|
|
108
|
+
delete_record!(record)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
48
113
|
end
|
49
114
|
end
|
50
115
|
end
|
@@ -8,6 +8,9 @@ module InventoryRefresh::SaveCollection
|
|
8
8
|
module SqlHelper
|
9
9
|
include InventoryRefresh::Logging
|
10
10
|
|
11
|
+
# TODO(lsmola) all below methods should be rewritten to arel, but we need to first extend arel to be able to do
|
12
|
+
# this
|
13
|
+
|
11
14
|
extend ActiveSupport::Concern
|
12
15
|
|
13
16
|
included do
|
@@ -80,6 +83,128 @@ module InventoryRefresh::SaveCollection
|
|
80
83
|
"#{value}::#{sql_type}"
|
81
84
|
end
|
82
85
|
end
|
86
|
+
|
87
|
+
# Effective way of doing multiselect
|
88
|
+
#
|
89
|
+
# If we use "(col1, col2) IN [(a,e), (b,f), (b,e)]" it's not great, just with 10k batch, we see
|
90
|
+
# *** ActiveRecord::StatementInvalid Exception: PG::StatementTooComplex: ERROR: stack depth limit exceeded
|
91
|
+
# HINT: Increase the configuration parameter "max_stack_depth" (currently 2048kB), after ensuring the
|
92
|
+
# platform's stack depth limit is adequate.
|
93
|
+
#
|
94
|
+
# If we use "(col1 = a AND col2 = e) OR (col1 = b AND col2 = f) OR (col1 = b AND col2 = e)" with 10k batch, it
|
95
|
+
# takes about 6s and consumes 300MB, with 100k it takes ~1h and consume 3GB in Postgre process
|
96
|
+
#
|
97
|
+
# The best way seems to be using CTE, where the list of values we want to map is turned to 'table' and we just
|
98
|
+
# do RIGHT OUTER JOIN to get the complement of given identifiers. Tested on getting complement of 100k items,
|
99
|
+
# using 2 cols (:ems_ref and :uid_ems) from total 150k rows. It takes ~1s and 350MB in Postgre process
|
100
|
+
#
|
101
|
+
# @param manager_uuids [Array<String>, Array[Hash]] Array with manager_uuids of entities. The keys have to match
|
102
|
+
# inventory_collection.manager_ref. We allow passing just array of strings, if manager_ref.size ==1, to
|
103
|
+
# spare some memory
|
104
|
+
# @return [Arel::SelectManager] Arel for getting complement of uuids. This method modifies the passed
|
105
|
+
# manager_uuids to spare some memory
|
106
|
+
def complement_of!(manager_uuids, all_manager_uuids_scope, all_manager_uuids_timestamp)
|
107
|
+
all_attribute_keys = inventory_collection.manager_ref
|
108
|
+
all_attribute_keys_array = inventory_collection.manager_ref.map(&:to_s)
|
109
|
+
|
110
|
+
active_entities = Arel::Table.new(:active_entities)
|
111
|
+
active_entities_cte = Arel::Nodes::As.new(
|
112
|
+
active_entities,
|
113
|
+
Arel.sql("(#{active_entities_query(all_attribute_keys_array, manager_uuids)})")
|
114
|
+
)
|
115
|
+
|
116
|
+
all_entities = Arel::Table.new(:all_entities)
|
117
|
+
all_entities_cte = Arel::Nodes::As.new(
|
118
|
+
all_entities,
|
119
|
+
Arel.sql("(#{all_entities_query(all_manager_uuids_scope, all_manager_uuids_timestamp).select(:id, *all_attribute_keys_array).to_sql})")
|
120
|
+
)
|
121
|
+
join_condition = all_attribute_keys.map { |key| active_entities[key].eq(all_entities[key]) }.inject(:and)
|
122
|
+
where_condition = all_attribute_keys.map { |key| active_entities[key].eq(nil) }.inject(:and)
|
123
|
+
|
124
|
+
active_entities
|
125
|
+
.project(all_entities[:id])
|
126
|
+
.join(all_entities, Arel::Nodes::RightOuterJoin)
|
127
|
+
.on(join_condition)
|
128
|
+
.with(active_entities_cte, all_entities_cte)
|
129
|
+
.where(where_condition)
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def all_entities_query(all_manager_uuids_scope, all_manager_uuids_timestamp)
|
135
|
+
all_entities_query = inventory_collection.full_collection_for_comparison
|
136
|
+
all_entities_query = all_entities_query.active if inventory_collection.retention_strategy == :archive
|
137
|
+
|
138
|
+
if all_manager_uuids_scope
|
139
|
+
scope_keys = all_manager_uuids_scope.first.keys.map { |x| association_to_foreign_key_mapping[x.to_sym] }.map(&:to_s)
|
140
|
+
scope = load_scope(all_manager_uuids_scope)
|
141
|
+
condition = inventory_collection.build_multi_selection_condition(scope, scope_keys)
|
142
|
+
all_entities_query = all_entities_query.where(condition)
|
143
|
+
end
|
144
|
+
|
145
|
+
if all_manager_uuids_timestamp && supports_column?(:resource_timestamp)
|
146
|
+
all_manager_uuids_timestamp = Time.parse(all_manager_uuids_timestamp).utc
|
147
|
+
|
148
|
+
date_field = model_class.arel_table[:resource_timestamp]
|
149
|
+
all_entities_query = all_entities_query.where(date_field.lt(all_manager_uuids_timestamp))
|
150
|
+
end
|
151
|
+
all_entities_query
|
152
|
+
end
|
153
|
+
|
154
|
+
def load_scope(all_manager_uuids_scope)
|
155
|
+
scope_keys = all_manager_uuids_scope.first.keys.to_set
|
156
|
+
|
157
|
+
all_manager_uuids_scope.map do |cond|
|
158
|
+
assert_scope!(scope_keys, cond)
|
159
|
+
|
160
|
+
cond.map do |key, value|
|
161
|
+
foreign_key = association_to_foreign_key_mapping[key.to_sym]
|
162
|
+
foreign_key_value = value.load&.id
|
163
|
+
|
164
|
+
assert_foreign_keys!(key, value, foreign_key, foreign_key_value)
|
165
|
+
|
166
|
+
[foreign_key, foreign_key_value]
|
167
|
+
end.to_h
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def assert_scope!(scope_keys, cond)
|
172
|
+
if cond.keys.to_set != scope_keys
|
173
|
+
raise "'#{inventory_collection}' expected keys for :all_manager_uuids_scope are #{scope_keys.to_a}, got"\
|
174
|
+
" #{cond.keys}. Keys must be the same for all scopes provided."
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def assert_foreign_keys!(key, value, foreign_key, foreign_key_value)
|
179
|
+
unless foreign_key
|
180
|
+
raise "'#{inventory_collection}' doesn't have relation :#{key} provided in :all_manager_uuids_scope."
|
181
|
+
end
|
182
|
+
|
183
|
+
unless foreign_key_value
|
184
|
+
raise "'#{inventory_collection}' couldn't load scope value :#{key} => #{value.inspect} provided in :all_manager_uuids_scope"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def active_entities_query(all_attribute_keys_array, manager_uuids)
|
189
|
+
connection = ActiveRecord::Base.connection
|
190
|
+
|
191
|
+
all_attribute_keys_array_q = all_attribute_keys_array.map { |x| quote_column_name(x) }
|
192
|
+
# For Postgre, only first set of values should contain the type casts
|
193
|
+
first_value = manager_uuids.shift.to_h
|
194
|
+
first_value = "(#{all_attribute_keys_array.map { |x| quote(connection, first_value[x], x, true) }.join(",")})"
|
195
|
+
|
196
|
+
# Rest of the values, without the type cast
|
197
|
+
values = manager_uuids.map! do |hash|
|
198
|
+
"(#{all_attribute_keys_array.map { |x| quote(connection, hash[x], x, false) }.join(",")})"
|
199
|
+
end.join(",")
|
200
|
+
|
201
|
+
values = values.blank? ? first_value : [first_value, values].join(",")
|
202
|
+
|
203
|
+
<<-SQL
|
204
|
+
SELECT *
|
205
|
+
FROM (VALUES #{values}) AS active_entities_table(#{all_attribute_keys_array_q.join(",")})
|
206
|
+
SQL
|
207
|
+
end
|
83
208
|
end
|
84
209
|
end
|
85
210
|
end
|
@@ -23,13 +23,13 @@ module InventoryRefresh::SaveCollection
|
|
23
23
|
connection = get_connection
|
24
24
|
|
25
25
|
# We want to ignore create timestamps when updating
|
26
|
-
all_attribute_keys_array = all_attribute_keys.to_a.delete_if { |x| %i
|
26
|
+
all_attribute_keys_array = all_attribute_keys.to_a.delete_if { |x| %i[created_at created_on].include?(x) }
|
27
27
|
all_attribute_keys_array << :id
|
28
28
|
|
29
29
|
# If there is not version attribute, the version conditions will be ignored
|
30
|
-
version_attribute = if supports_remote_data_timestamp?(all_attribute_keys)
|
30
|
+
version_attribute = if inventory_collection.parallel_safe? && supports_remote_data_timestamp?(all_attribute_keys)
|
31
31
|
:resource_timestamp
|
32
|
-
elsif supports_remote_data_version?(all_attribute_keys)
|
32
|
+
elsif inventory_collection.parallel_safe? && supports_remote_data_version?(all_attribute_keys)
|
33
33
|
:resource_counter
|
34
34
|
end
|
35
35
|
|
@@ -130,9 +130,13 @@ module InventoryRefresh::SaveCollection
|
|
130
130
|
end
|
131
131
|
|
132
132
|
def update_query_returning
|
133
|
-
|
134
|
-
|
135
|
-
|
133
|
+
if inventory_collection.parallel_safe?
|
134
|
+
<<-SQL
|
135
|
+
RETURNING updated_values.#{quote_column_name("id")}, #{unique_index_columns.map { |x| "updated_values.#{quote_column_name(x)}" }.join(",")}
|
136
|
+
SQL
|
137
|
+
else
|
138
|
+
""
|
139
|
+
end
|
136
140
|
end
|
137
141
|
end
|
138
142
|
end
|
@@ -19,7 +19,7 @@ module InventoryRefresh::SaveCollection
|
|
19
19
|
# columns of a row, :partial is when we save only few columns, so a partial row.
|
20
20
|
# @param on_conflict [Symbol, NilClass] defines behavior on conflict with unique index constraint, allowed values
|
21
21
|
# are :do_update, :do_nothing, nil
|
22
|
-
def build_insert_query(all_attribute_keys, hashes, on_conflict: nil,
|
22
|
+
def build_insert_query(all_attribute_keys, hashes, mode:, on_conflict: nil, column_name: nil)
|
23
23
|
logger.debug("Building insert query for #{inventory_collection} of size #{inventory_collection.size}...")
|
24
24
|
|
25
25
|
# Cache the connection for the batch
|
@@ -55,6 +55,8 @@ module InventoryRefresh::SaveCollection
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def insert_query_on_conflict_behavior(all_attribute_keys, on_conflict, mode, ignore_cols, column_name)
|
58
|
+
return "" unless inventory_collection.parallel_safe?
|
59
|
+
|
58
60
|
insert_query_on_conflict = insert_query_on_conflict_do(on_conflict)
|
59
61
|
if on_conflict == :do_update
|
60
62
|
insert_query_on_conflict += insert_query_on_conflict_update(all_attribute_keys, mode, ignore_cols, column_name)
|
@@ -63,11 +65,12 @@ module InventoryRefresh::SaveCollection
|
|
63
65
|
end
|
64
66
|
|
65
67
|
def insert_query_on_conflict_do(on_conflict)
|
66
|
-
|
68
|
+
case on_conflict
|
69
|
+
when :do_nothing
|
67
70
|
<<-SQL
|
68
71
|
ON CONFLICT DO NOTHING
|
69
72
|
SQL
|
70
|
-
|
73
|
+
when :do_update
|
71
74
|
index_where_condition = unique_index_for(unique_index_keys).where
|
72
75
|
where_to_sql = index_where_condition ? "WHERE #{index_where_condition}" : ""
|
73
76
|
|
@@ -92,6 +95,8 @@ module InventoryRefresh::SaveCollection
|
|
92
95
|
:resource_counter
|
93
96
|
end
|
94
97
|
|
98
|
+
# TODO(lsmola) should we add :deleted => false to the update clause? That should handle a reconnect, without a
|
99
|
+
# a need to list :deleted anywhere in the parser. We just need to check that a model has the :deleted attribute
|
95
100
|
query = <<-SQL
|
96
101
|
SET #{(all_attribute_keys - ignore_cols).map { |key| build_insert_set_cols(key) }.join(", ")}
|
97
102
|
SQL
|
@@ -103,10 +108,12 @@ module InventoryRefresh::SaveCollection
|
|
103
108
|
end
|
104
109
|
|
105
110
|
def insert_query_on_conflict_update_mode(mode, version_attribute, column_name)
|
106
|
-
|
111
|
+
case mode
|
112
|
+
when :full
|
107
113
|
full_update_condition(version_attribute)
|
108
|
-
|
114
|
+
when :partial
|
109
115
|
raise "Column name must be provided" unless column_name
|
116
|
+
|
110
117
|
partial_update_condition(version_attribute, column_name)
|
111
118
|
end
|
112
119
|
end
|
@@ -124,7 +131,7 @@ module InventoryRefresh::SaveCollection
|
|
124
131
|
, #{attr_partial} = '{}', #{attr_partial_max} = NULL
|
125
132
|
|
126
133
|
WHERE EXCLUDED.#{attr_full} IS NULL OR (
|
127
|
-
(#{q_table_name}.#{attr_full} IS NULL OR EXCLUDED.#{attr_full}
|
134
|
+
(#{q_table_name}.#{attr_full} IS NULL OR EXCLUDED.#{attr_full} > #{q_table_name}.#{attr_full}) AND
|
128
135
|
(#{q_table_name}.#{attr_partial_max} IS NULL OR EXCLUDED.#{attr_full} >= #{q_table_name}.#{attr_partial_max})
|
129
136
|
)
|
130
137
|
SQL
|
@@ -133,9 +140,10 @@ module InventoryRefresh::SaveCollection
|
|
133
140
|
def partial_update_condition(attr_full, column_name)
|
134
141
|
attr_partial = attr_full.to_s.pluralize # Changes resource_counter/timestamp to resource_counters/timestamps
|
135
142
|
attr_partial_max = "#{attr_partial}_max"
|
136
|
-
cast =
|
143
|
+
cast = case attr_full
|
144
|
+
when :resource_timestamp
|
137
145
|
"timestamp"
|
138
|
-
|
146
|
+
when :resource_counter
|
139
147
|
"integer"
|
140
148
|
end
|
141
149
|
|
@@ -150,9 +158,9 @@ module InventoryRefresh::SaveCollection
|
|
150
158
|
#{insert_query_set_jsonb_version(cast, attr_partial, attr_partial_max, column_name)}
|
151
159
|
, #{attr_partial_max} = greatest(#{q_table_name}.#{attr_partial_max}::#{cast}, EXCLUDED.#{attr_partial_max}::#{cast})
|
152
160
|
WHERE EXCLUDED.#{attr_partial_max} IS NULL OR (
|
153
|
-
(#{q_table_name}.#{attr_full} IS NULL OR EXCLUDED.#{attr_partial_max}
|
161
|
+
(#{q_table_name}.#{attr_full} IS NULL OR EXCLUDED.#{attr_partial_max} > #{q_table_name}.#{attr_full}) AND (
|
154
162
|
(#{q_table_name}.#{attr_partial}->>'#{column_name}')::#{cast} IS NULL OR
|
155
|
-
EXCLUDED.#{attr_partial_max}::#{cast}
|
163
|
+
EXCLUDED.#{attr_partial_max}::#{cast} > (#{q_table_name}.#{attr_partial}->>'#{column_name}')::#{cast}
|
156
164
|
)
|
157
165
|
)
|
158
166
|
SQL
|
@@ -179,12 +187,16 @@ module InventoryRefresh::SaveCollection
|
|
179
187
|
end
|
180
188
|
|
181
189
|
def insert_query_returning_timestamps
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
190
|
+
if inventory_collection.parallel_safe?
|
191
|
+
# For upsert, we'll return also created and updated timestamps, so we can recognize what was created and what
|
192
|
+
# updated
|
193
|
+
if inventory_collection.internal_timestamp_columns.present?
|
194
|
+
<<-SQL
|
195
|
+
, #{inventory_collection.internal_timestamp_columns.map { |x| quote_column_name(x) }.join(",")}
|
196
|
+
SQL
|
197
|
+
end
|
198
|
+
else
|
199
|
+
""
|
188
200
|
end
|
189
201
|
end
|
190
202
|
end
|
@@ -1,7 +1,5 @@
|
|
1
|
-
require "inventory_refresh/exception"
|
2
1
|
require "inventory_refresh/logging"
|
3
2
|
require "inventory_refresh/save_collection/saver/retention_helper"
|
4
|
-
require "inventory_refresh/inventory_collection/index/type/local_db"
|
5
3
|
|
6
4
|
module InventoryRefresh::SaveCollection
|
7
5
|
class Sweeper < InventoryRefresh::SaveCollection::Base
|
@@ -12,111 +10,39 @@ module InventoryRefresh::SaveCollection
|
|
12
10
|
# @param _ems [ActiveRecord] Manager owning the inventory_collections
|
13
11
|
# @param inventory_collections [Array<InventoryRefresh::InventoryCollection>] Array of InventoryCollection objects
|
14
12
|
# for sweeping
|
15
|
-
# @param sweep_scope [Array<String, Symbol, Hash>] Array of inventory collection names marking sweep. Or for
|
16
|
-
# targeted sweeping it's array of hashes, where key is inventory collection name pointing to an array of
|
17
|
-
# identifiers of inventory objects we want to target for sweeping.
|
18
13
|
# @param refresh_state [ActiveRecord] Record of :refresh_states
|
19
|
-
def sweep(_ems, inventory_collections,
|
20
|
-
scope_set = build_scope_set(sweep_scope)
|
21
|
-
|
14
|
+
def sweep(_ems, inventory_collections, refresh_state)
|
22
15
|
inventory_collections.each do |inventory_collection|
|
23
|
-
next unless sweep_possible?(inventory_collection,
|
16
|
+
next unless sweep_possible?(inventory_collection, refresh_state)
|
24
17
|
|
25
|
-
new(inventory_collection, refresh_state
|
18
|
+
new(inventory_collection, refresh_state).sweep
|
26
19
|
end
|
27
20
|
end
|
28
21
|
|
29
|
-
def sweep_possible?(inventory_collection,
|
30
|
-
inventory_collection.supports_column?(:last_seen_at) &&
|
31
|
-
|
32
|
-
|
33
|
-
def in_scope?(inventory_collection, scope_set)
|
34
|
-
scope_set.include?(inventory_collection&.name)
|
22
|
+
def sweep_possible?(inventory_collection, refresh_state)
|
23
|
+
inventory_collection.supports_column?(:last_seen_at) && inventory_collection.parallel_safe? &&
|
24
|
+
inventory_collection.strategy == :local_db_find_missing_references &&
|
25
|
+
in_scope?(inventory_collection, refresh_state.sweep_scope)
|
35
26
|
end
|
36
27
|
|
37
|
-
def
|
38
|
-
return
|
28
|
+
def in_scope?(inventory_collection, sweep_scope)
|
29
|
+
return true unless sweep_scope
|
30
|
+
return true if sweep_scope.kind_of?(Array) && sweep_scope.include?(inventory_collection&.name&.to_s)
|
39
31
|
|
40
|
-
|
41
|
-
sweep_scope.map(&:to_sym).to_set
|
42
|
-
elsif sweep_scope.kind_of?(Hash)
|
43
|
-
sweep_scope.keys.map(&:to_sym).to_set
|
44
|
-
end
|
32
|
+
false
|
45
33
|
end
|
46
34
|
end
|
47
35
|
|
48
36
|
include InventoryRefresh::SaveCollection::Saver::RetentionHelper
|
49
37
|
|
50
|
-
attr_reader :inventory_collection, :refresh_state, :
|
38
|
+
attr_reader :inventory_collection, :refresh_state, :model_class, :primary_key
|
51
39
|
|
52
|
-
|
53
|
-
:inventory_object?,
|
54
|
-
:to => :inventory_collection
|
55
|
-
|
56
|
-
def initialize(inventory_collection, refresh_state, sweep_scope)
|
40
|
+
def initialize(inventory_collection, refresh_state)
|
57
41
|
@inventory_collection = inventory_collection
|
42
|
+
@refresh_state = refresh_state
|
58
43
|
|
59
|
-
@
|
60
|
-
@
|
61
|
-
|
62
|
-
@model_class = inventory_collection.model_class
|
63
|
-
@primary_key = @model_class.primary_key
|
64
|
-
end
|
65
|
-
|
66
|
-
def apply_targeted_sweep_scope(all_entities_query)
|
67
|
-
if sweep_scope.kind_of?(Hash)
|
68
|
-
scope = sweep_scope[inventory_collection.name]
|
69
|
-
return all_entities_query if scope.nil? || scope.empty?
|
70
|
-
|
71
|
-
# Scan the scope to find all references, so we can load them from DB in batches
|
72
|
-
scan_sweep_scope!(scope)
|
73
|
-
|
74
|
-
scope_keys = Set.new
|
75
|
-
conditions = scope.map { |x| InventoryRefresh::InventoryObject.attributes_with_keys(x, inventory_collection, scope_keys) }
|
76
|
-
assert_conditions!(conditions, scope_keys)
|
77
|
-
|
78
|
-
all_entities_query.where(inventory_collection.build_multi_selection_condition(conditions, scope_keys))
|
79
|
-
else
|
80
|
-
all_entities_query
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def loadable?(value)
|
85
|
-
inventory_object_lazy?(value) || inventory_object?(value)
|
86
|
-
end
|
87
|
-
|
88
|
-
def scan_sweep_scope!(scope)
|
89
|
-
scope.each do |sc|
|
90
|
-
sc.each_value do |value|
|
91
|
-
next unless loadable?(value)
|
92
|
-
|
93
|
-
value_inventory_collection = value.inventory_collection
|
94
|
-
value_inventory_collection.add_reference(value.reference, :key => value.key)
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def assert_conditions!(conditions, scope_keys)
|
100
|
-
conditions.each do |cond|
|
101
|
-
assert_uniform_keys!(cond, scope_keys)
|
102
|
-
assert_non_existent_keys!(cond)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
def assert_uniform_keys!(cond, scope_keys)
|
107
|
-
return if (diff = (scope_keys - cond.keys.to_set)).empty?
|
108
|
-
|
109
|
-
raise(InventoryRefresh::Exception::SweeperNonUniformScopeKeyFoundError,
|
110
|
-
"Sweeping scope for #{inventory_collection} contained non uniform keys. All keys for the"\
|
111
|
-
"scope must be the same, it's possible to send multiple sweeps with different key set. Missing keys"\
|
112
|
-
" for a scope were: #{diff.to_a}")
|
113
|
-
end
|
114
|
-
|
115
|
-
def assert_non_existent_keys!(cond)
|
116
|
-
return if (diff = (cond.keys.to_set - inventory_collection.all_column_names)).empty?
|
117
|
-
|
118
|
-
raise(InventoryRefresh::Exception::SweeperNonExistentScopeKeyFoundError,
|
119
|
-
"Sweeping scope for #{inventory_collection} contained keys that are not columns: #{diff.to_a}")
|
44
|
+
@model_class = inventory_collection.model_class
|
45
|
+
@primary_key = @model_class.primary_key
|
120
46
|
end
|
121
47
|
|
122
48
|
def sweep
|
@@ -126,9 +52,7 @@ module InventoryRefresh::SaveCollection
|
|
126
52
|
table = model_class.arel_table
|
127
53
|
date_field = table[:last_seen_at]
|
128
54
|
all_entities_query = inventory_collection.full_collection_for_comparison
|
129
|
-
all_entities_query
|
130
|
-
|
131
|
-
all_entities_query = apply_targeted_sweep_scope(all_entities_query)
|
55
|
+
all_entities_query.active if inventory_collection.retention_strategy == :archive
|
132
56
|
|
133
57
|
query = all_entities_query
|
134
58
|
.where(date_field.lt(refresh_start)).or(all_entities_query.where(:last_seen_at => nil))
|
@@ -17,21 +17,21 @@ module InventoryRefresh::SaveCollection
|
|
17
17
|
|
18
18
|
layers = InventoryRefresh::Graph::TopologicalSort.new(graph).topological_sort
|
19
19
|
|
20
|
-
logger.debug("Saving manager #{ems.
|
20
|
+
logger.debug("Saving manager #{ems.name}...")
|
21
21
|
|
22
|
-
sorted_graph_log = "Topological sorting of manager #{ems.
|
22
|
+
sorted_graph_log = "Topological sorting of manager #{ems.name} resulted in these layers processable in parallel:\n"
|
23
23
|
sorted_graph_log += graph.to_graphviz(:layers => layers)
|
24
24
|
logger.debug(sorted_graph_log)
|
25
25
|
|
26
26
|
layers.each_with_index do |layer, index|
|
27
|
-
logger.debug("Saving manager #{ems.
|
27
|
+
logger.debug("Saving manager #{ems.name} | Layer #{index}")
|
28
28
|
layer.each do |inventory_collection|
|
29
29
|
save_inventory_object_inventory(ems, inventory_collection) unless inventory_collection.saved?
|
30
30
|
end
|
31
|
-
logger.debug("Saved manager #{ems.
|
31
|
+
logger.debug("Saved manager #{ems.name} | Layer #{index}")
|
32
32
|
end
|
33
33
|
|
34
|
-
logger.debug("Saving manager #{ems.
|
34
|
+
logger.debug("Saving manager #{ems.name}...Complete")
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -29,18 +29,11 @@ module InventoryRefresh
|
|
29
29
|
# @param ems [ExtManagementSystem] manager owning the inventory_collections
|
30
30
|
# @param inventory_collections [Array<InventoryRefresh::InventoryCollection>] array of InventoryCollection objects
|
31
31
|
# for sweeping
|
32
|
-
# @param sweep_scope [Array<String, Symbol, Hash>] Array of inventory collection names marking sweep. Or for
|
33
|
-
# targeted sweeping it's array of hashes, where key is inventory collection name pointing to an array of
|
34
|
-
# identifiers of inventory objects we want to target for sweeping.
|
35
32
|
# @param refresh_state [ActiveRecord] Record of :refresh_states
|
36
|
-
def sweep_inactive_records(ems, inventory_collections,
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
logger.info("#{log_header(ems)} Sweeping EMS Inventory with scope #{sweep_scope} and date #{refresh_state.created_at} ...")
|
42
|
-
InventoryRefresh::SaveCollection::Sweeper.sweep(ems, inventory_collections, sweep_scope, refresh_state)
|
43
|
-
logger.info("#{log_header(ems)} Sweeping EMS Inventory with scope #{sweep_scope} and date #{refresh_state.created_at}...Complete")
|
33
|
+
def sweep_inactive_records(ems, inventory_collections, refresh_state)
|
34
|
+
logger.info("#{log_header(ems)} Sweeping EMS Inventory...")
|
35
|
+
InventoryRefresh::SaveCollection::Sweeper.sweep(ems, inventory_collections, refresh_state)
|
36
|
+
logger.info("#{log_header(ems)} Sweeping EMS Inventory...Complete")
|
44
37
|
|
45
38
|
ems
|
46
39
|
end
|
@@ -50,7 +43,7 @@ module InventoryRefresh
|
|
50
43
|
# @param ems [ExtManagementSystem] manager owning the inventory_collections
|
51
44
|
# @return [String] helper string for logging
|
52
45
|
def log_header(ems)
|
53
|
-
"EMS: [#{ems.id}]"
|
46
|
+
"EMS: [#{ems.name}], id: [#{ems.id}]"
|
54
47
|
end
|
55
48
|
end
|
56
49
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module InventoryRefresh
|
2
|
+
class Target
|
3
|
+
attr_reader :association, :manager_ref, :event_id, :options
|
4
|
+
|
5
|
+
# @param association [Symbol] An existing association on Manager, that lists objects represented by a Target, naming
|
6
|
+
# should be the same of association of a counterpart InventoryCollection object
|
7
|
+
# @param manager_ref [Hash] A Hash that can be used to find_by on a given association and returning a unique object.
|
8
|
+
# The keys should be the same as the keys of the counterpart InventoryObject
|
9
|
+
# @param manager [ManageIQ::Providers::BaseManager] The Manager owning the Target
|
10
|
+
# @param manager_id [Integer] A primary key of the Manager owning the Target
|
11
|
+
# @param event_id [Integer] A primary key of the EmsEvent associated with the Target
|
12
|
+
# @param options [Hash] A free form options hash
|
13
|
+
def initialize(association:, manager_ref:, manager: nil, manager_id: nil, event_id: nil, options: {})
|
14
|
+
raise "Provide either :manager or :manager_id argument" if manager.nil? && manager_id.nil?
|
15
|
+
|
16
|
+
@manager = manager
|
17
|
+
@manager_id = manager_id
|
18
|
+
@association = association
|
19
|
+
@manager_ref = manager_ref
|
20
|
+
@event_id = event_id
|
21
|
+
@options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
# A Rails recommended interface for deserializing an object
|
25
|
+
# @return [InventoryRefresh::Target] InventoryRefresh::Target instance
|
26
|
+
def self.load(*args)
|
27
|
+
new(*args)
|
28
|
+
end
|
29
|
+
|
30
|
+
# A Rails recommended interface for serializing an object
|
31
|
+
#
|
32
|
+
# @param obj [InventoryRefresh::Target] InventoryRefresh::Target instance we want to serialize
|
33
|
+
# @return [Hash] serialized object
|
34
|
+
def self.dump(obj)
|
35
|
+
obj.dump
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns a serialized InventoryRefresh::Target object. This can be used to initialize a new object, then the object
|
39
|
+
# target acts the same as the object InventoryRefresh::Target.new(target.serialize)
|
40
|
+
#
|
41
|
+
# @return [Hash] serialized object
|
42
|
+
def dump
|
43
|
+
{
|
44
|
+
:manager_id => manager_id,
|
45
|
+
:association => association,
|
46
|
+
:manager_ref => manager_ref,
|
47
|
+
:event_id => event_id,
|
48
|
+
:options => options
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
alias id dump
|
53
|
+
alias name manager_ref
|
54
|
+
|
55
|
+
# @return [ManageIQ::Providers::BaseManager] The Manager owning the Target
|
56
|
+
def manager
|
57
|
+
@manager || ManageIQ::Providers::BaseManager.find(@manager_id)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Integer] A primary key of the Manager owning the Target
|
61
|
+
def manager_id
|
62
|
+
@manager_id || manager.try(:id)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Loads InventoryRefresh::Target ApplicationRecord representation from our DB, this requires that InventoryRefresh::Target
|
66
|
+
# has been refreshed, otherwise the AR object can be missing.
|
67
|
+
#
|
68
|
+
# @return [ApplicationRecord] A InventoryRefresh::Target loaded from the database as AR object
|
69
|
+
def load_from_db
|
70
|
+
manager.public_send(association).find_by(manager_ref)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|