From d6c5158f86a7c5bff1766251dfab5222712cd0c9 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..680a41541 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