From 4d6e563c029e8e0b9a219ae351dbfc4adae30035 Mon Sep 17 00:00:00 2001 From: Sean Millichamp Date: Wed, 3 Apr 2024 00:12:51 +0000 Subject: [PATCH] Optimize for exact-match target name lookups Performing wildcard target name resolution is fairly costly, having to search through all group names, target names, and target aliases, calling File.fnmatch on each one in order to find matches. In the case of the targets being non-wildcard strings much faster exact matching can be performed resulting in a significant performance improvement. !feature * **Optimize get_targets performance for exact-match cases** Attempt exact target name matches for strings passed to get_targets and only attempt the slower wildcard match if the string contains a valid glob wildcard character. --- lib/bolt/inventory/inventory.rb | 37 ++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/bolt/inventory/inventory.rb b/lib/bolt/inventory/inventory.rb index 54d67b387..b2bd75cf4 100644 --- a/lib/bolt/inventory/inventory.rb +++ b/lib/bolt/inventory/inventory.rb @@ -14,6 +14,10 @@ class Inventory EXTENDED_TARGET_REGEX = /[[:space:],]+(?=[^\]}]*(?:[\[{]|$))/.freeze TARGET_REGEX = /[[:space:],]+/.freeze + # Pattern which looks for indicators that glob-based target name matching + # should be used. + GLOB_MATCH_REGEX = /[*?\[\]{}]/.freeze + class WildcardError < Bolt::Error def initialize(target) super("Found 0 targets matching wildcard pattern #{target}", 'bolt.inventory/wildcard-error') @@ -125,12 +129,30 @@ def match_wildcard?(wildcard, target_name, ext_glob: false) # If target is a group name, expand it to the members of that group. # Else match against groups and targets in inventory by name or alias. - # If a wildcard string, error if no matches are found. + # Attempt exact matches for groups, targets, and aliases first for speed. + # If no exact match and the string contains wildcard characters, then check + # and see if the target string might be a URI, if it parses as a URI with + # a scheme then return as-is, otherwise look for a wildcard match and + # error if no matches are found. # Else fall back to [target] if no matches are found. def resolve_name(target, ext_glob: false) if (group = group_lookup[target]) group.all_targets.to_a - else + elsif @targets.key?(target) + [target] + elsif (real_target = groups.target_aliases[target]) + [real_target] + elsif GLOB_MATCH_REGEX.match?(target) + # URIs and glob wildcards have some overlapping characters. If the target + # being resolved parses as a valid target URI and has a scheme defined then + # return it as-is and do not try to do further wildcard matching: + uri = begin + Bolt::Inventory::Target.parse_uri(target) + rescue Bolt::ParseError + nil + end + return [target] if uri&.scheme + targets = [] # Find groups that match the glob @@ -147,12 +169,11 @@ def resolve_name(target, ext_glob: false) .select { |tgt_alias, _| match_wildcard?(target, tgt_alias, ext_glob: ext_glob) } .values - if targets.empty? - raise(WildcardError, target) if target.include?('*') - [target] - else - targets.uniq - end + raise(WildcardError, target) if targets.empty? + + targets.uniq + else # rubocop:disable Lint/DuplicateBranch + [target] end end private :resolve_name