-
Notifications
You must be signed in to change notification settings - Fork 993
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes #36715 - Speed up host fact retrieval when you only need a subset of facts #9819
Fixes #36715 - Speed up host fact retrieval when you only need a subset of facts #9819
Conversation
Issues: #36715 |
ab6aa1c
to
b2c0037
Compare
app/models/host/base.rb
Outdated
example '@host.facts_with_names([\'dmi::memory::size\', \'net::interface::eth0\']) # => { "dmi::memory::size"=>"16 GB", "net::interface::eth0"=>"00:50:56:8a:5c:3c" }', desc: 'Getting specific facts' | ||
end | ||
def facts_with_names(names_query = [], *args) | ||
filtered_names = Set.new([names_query, *args].flatten.compact) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason the report renderer splats the first argument, turning
host.facts_with_names(['fact1', 'fact2'])
into
host.facts_with_names('fact1', 'fact2')
resulting in an ArgumentError if I don't handle it here.
I 'd recommend this approach instead of facts_with_names. In my benchmark tests it was way faster. diff --git a/app/models/host/base.rb b/app/models/host/base.rb
index 36fbe72..4f1d980 100644
--- a/app/models/host/base.rb
+++ b/app/models/host/base.rb
@@ -221,13 +221,16 @@ module Host
example '@host.facts["uptime"] # => "30 days"', desc: 'Getting specific fact value, +uptime+ in this case'
aliases :facts
end
- def facts_hash
+ def facts_hash(fact_names = [])
hash = {}
- fact_values.includes(:fact_name).collect do |fact|
+ query = fact_values.includes(:fact_name)
+ query = query.where(fact_names: {name: fact_names}) if fact_names.present?
+ query.collect do |fact|
hash[fact.fact_name.name] = fact.value
end
hash
end
For example with my change fact_names = [
'ansible_local::gls_ansible_ctd_posture::_ctd',
'ansible_local::gls_ansible_timestemp::_timestemp',
'ansible_local::gls_ansible_model::_model',
'ansible_local::gls_ansible_mac::_mac_address',
'ansible_local::gls_ansible_ipaddress::_gls_ansible_ipaddress',
'ansible_local::gls_ansible_owner::_owner',
'ansible_local::gls_ansible_lrt::_lab_lrt',
'ansible_local::gls_ansible_hostname::_gls_ansible_hostname'
]
hashes = []
Benchmark.measure do
hashes = Host.all.map {|host| host.facts(fact_names) }
end
=> #<Benchmark::Tms:0x000055fc793fe1b8 @label="", @real=4.964442664990202, @cstime=0.0, @cutime=0.0, @stime=0.068133, @utime=3.543512999999999, @total=3.611645999999999> About 3.6 seconds for 2851 Hosts. vs hashes1 = []
Benchmark.measure do
hashes1 = Host.limit(500).map {|host| host.facts_with_names(fact_names) }
end
=> #<Benchmark::Tms:0x000055fcf3c900b8 @label="", @real=39.416546951048076, @cstime=0.0, @cutime=0.0, @stime=0.7837529999999999, @utime=30.203799, @total=30.987552> 30 seconds for 500 host records |
dae49ec
to
6ed24f7
Compare
@parthaa updated 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to work well for me. APJ
6ed24f7
to
525fd78
Compare
@@ -214,17 +214,20 @@ def set_comment(parser) | |||
desc 'Note that available facts depend on what facts have been uploaded to Foreman, | |||
typical sources are Puppet facter, subscription manager etc. | |||
The facts can be out of date, this macro only provides access to the value stored in the database.' | |||
optional :fact_names, Array, desc: 'A list of fact names to return. If empty all facts are returned' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ofedoren is this also correct documentation for *args
parameters, or is there a specific tag for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agh, my bad for not noticing this earlier :/
Well, it's not as wrong. It's still quite optional :) But for *args
params, I'd use one of list
/splat
/rest
instead of optional
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you submit a follow up PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure: #9850
f7e012c
to
51eebec
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be interested to see some benchmarks of the new code in the latest iteration.
51eebec
to
73fb064
Compare
Squashed |
[test katello] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the improvement (looks like join & pluck shaved off another few seconds), but I wonder if there is test coverage for this new functionality. Can we also assert that it only performs a single DB query?
Did a bit of research and it looks like there is a "bullet" gem that helps with this, but I didn't want to add any new gems. So I added a helper method in test_helper; see what you think. Turns out I also improved
Added tests to cover all the new functionality. |
73fb064
to
eeba8cf
Compare
test/test_helper.rb
Outdated
def assert_sql_queries(num_of_queries) | ||
queries = [] | ||
ActiveSupport::Notifications.subscribe("sql.active_record") do |name, start, finish, id, payload| | ||
queries << payload[:sql] if payload[:sql] =~ /SELECT/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this also catch UPDATE
and DROP
and INSERT
statements?
(but probably out of scope right now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be argued that those are SQL "statements" and not "queries" :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given SELECT
can change data (hello triggers!), I see no difference. But as said, this is probably more than was asked in the original comment by Ewoud :)
I had promised you a review when I am back, but it seems Ewoud did beat me, so we're good :) |
test/test_helper.rb
Outdated
@@ -119,6 +119,16 @@ def after_teardown | |||
Foreman::Plugin.send(:clear, @plugins_backup, @registries_backup) | |||
@clear_plugins = nil | |||
end | |||
|
|||
def assert_sql_queries(num_of_queries) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def assert_sql_queries(num_of_queries) | |
def assert_sql_queries(num_of_queries, match = /SELECT/) |
followed by
queries << payload[:sql] if payload[:sql] =~ match
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Updated 👍
eeba8cf
to
cc6ece6
Compare
Seems this will need another approving review since I added those tests. |
A user with about 3k hosts has a report that runs
host.facts
on each host multiple times. This was causing the report generation time to be over an hour, sincehost.facts
runs a fewincludes
which result in quite a few SQL queries:This PR adds a new argument to host.facts so you can pass specific fact names. It also changes
collect
toeach
in the above code because we don't use the resulting arrays, we just iterate and build the final hash.The report template can look something like this: