go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/algorithms/cluster.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package algorithms
    16  
    17  import (
    18  	"encoding/hex"
    19  	"errors"
    20  
    21  	"go.chromium.org/luci/analysis/internal/clustering"
    22  	"go.chromium.org/luci/analysis/internal/clustering/algorithms/failurereason"
    23  	"go.chromium.org/luci/analysis/internal/clustering/algorithms/rulesalgorithm"
    24  	"go.chromium.org/luci/analysis/internal/clustering/algorithms/testname"
    25  	"go.chromium.org/luci/analysis/internal/clustering/rules"
    26  	"go.chromium.org/luci/analysis/internal/clustering/rules/cache"
    27  	"go.chromium.org/luci/analysis/internal/config"
    28  	"go.chromium.org/luci/analysis/internal/config/compiledcfg"
    29  )
    30  
    31  // Algorithm represents the interface that each clustering algorithm
    32  // generating suggested clusters must implement.
    33  type Algorithm interface {
    34  	// Name returns the identifier of the clustering algorithm.
    35  	Name() string
    36  	// Cluster clusters the given test failure and returns its cluster ID (if
    37  	// it can be clustered) or nil otherwise. THe returned cluster ID must be
    38  	// at most 16 bytes.
    39  	Cluster(config *compiledcfg.ProjectConfig, failure *clustering.Failure) []byte
    40  	// FailureAssociationRule returns a failure association rule that
    41  	// captures the definition of the cluster containing the given example.
    42  	FailureAssociationRule(config *compiledcfg.ProjectConfig, example *clustering.Failure) string
    43  	// ClusterTitle returns the unhashed clustering key which is common
    44  	// across all test results in a cluster. This will be displayed
    45  	// on the cluster page or cluster listing.
    46  	ClusterTitle(config *compiledcfg.ProjectConfig, example *clustering.Failure) string
    47  	// ClusterDescription returns a description of the cluster, for use when
    48  	// filing bugs, with the help of the given example failure.
    49  	ClusterDescription(config *compiledcfg.ProjectConfig, summary *clustering.ClusterSummary) (*clustering.ClusterDescription, error)
    50  }
    51  
    52  // AlgorithmsVersion is the version of the set of algorithms used.
    53  // Changing the set of algorithms below (including add, update or
    54  // deletion of an algorithm) should result in this version being
    55  // incremented.
    56  //
    57  // In case of algorithm deletion, make sure to update this constant
    58  // appropriately to ensure the AlgorithmsVersion still increases
    59  // (I.E. DO NOT simply delete "+ <myalgorithm>.AlgorithmVersion"
    60  // when deleting an algorithm without rolling its value (plus one)
    61  // into the constant.)
    62  const AlgorithmsVersion = 1 + failurereason.AlgorithmVersion +
    63  	testname.AlgorithmVersion + rulesalgorithm.AlgorithmVersion
    64  
    65  // suggestingAlgorithms is the set of clustering algorithms used by
    66  // LUCI Analysis to generate suggested clusters.
    67  // When an algorithm is added or removed from the set,
    68  // or when an algorithm is updated, ensure the AlgorithmsVersion
    69  // above increments.
    70  var suggestingAlgorithms = []Algorithm{
    71  	&failurereason.Algorithm{},
    72  	&testname.Algorithm{},
    73  }
    74  
    75  // rulesAlgorithm is the rules-based clustering algorithm used by
    76  // LUCI Analysis. When this algorithm is changed, ensure the AlgorithmsVersion
    77  // above increments.
    78  var rulesAlgorithm = rulesalgorithm.Algorithm{}
    79  
    80  // The set of all algorithms known by LUCI Analysis.
    81  var algorithmNames map[string]struct{}
    82  
    83  // The set of all suggested algorithms known by LUCI Analysis.
    84  var suggestedAlgorithmNames map[string]struct{}
    85  
    86  func init() {
    87  	algorithmNames = make(map[string]struct{})
    88  	suggestedAlgorithmNames = make(map[string]struct{})
    89  	algorithmNames[rulesalgorithm.AlgorithmName] = struct{}{}
    90  	for _, a := range suggestingAlgorithms {
    91  		algorithmNames[a.Name()] = struct{}{}
    92  		suggestedAlgorithmNames[a.Name()] = struct{}{}
    93  	}
    94  }
    95  
    96  // Cluster performs (incremental re-)clustering of the given test
    97  // failures using all registered clustering algorithms and the
    98  // specified set of failure association rules and config.
    99  //
   100  // If the test results have not been previously clustered, pass
   101  // an existing ClusterResults of NewEmptyClusterResults(...)
   102  // to cluster test results from scratch.
   103  //
   104  // If the test results have been previously clustered, pass the
   105  // ClusterResults returned by the last call to Cluster.
   106  //
   107  // Cluster(...) will always return a set of ClusterResults which
   108  // are as- or more-recent than the existing ClusterResults.
   109  // This is defined as the following postcondition:
   110  //
   111  //	returned.AlgorithmsVersion > existing.AlgorithmsVersion ||
   112  //	(returned.AlgorithmsVersion == existing.AlgorithmsVersion &&
   113  //	  returned.ConfigVersion >= existing.ConfigVersion &&
   114  //	  returned.RulesVersion >= existing.RulesVersion)
   115  func Cluster(config *compiledcfg.ProjectConfig, ruleset *cache.Ruleset, existing clustering.ClusterResults, failures []*clustering.Failure) clustering.ClusterResults {
   116  	if existing.AlgorithmsVersion > AlgorithmsVersion {
   117  		// We are running out-of-date clustering algorithms. Do not
   118  		// try to improve on the existing clustering. This can
   119  		// happen if we are rolling out a new version of LUCI Analysis.
   120  		return existing
   121  	}
   122  
   123  	newSuggestedAlgorithms := false
   124  	for _, alg := range suggestingAlgorithms {
   125  		if _, ok := existing.Algorithms[alg.Name()]; !ok {
   126  			newSuggestedAlgorithms = true
   127  		}
   128  	}
   129  	// We should recycle the previous suggested clusters for performance if:
   130  	// (1) the algorithms to be run are the same (or a subset)
   131  	//     of what was previously run, and
   132  	// (2) the config available to us is not later than
   133  	//     what was available when clustering occurred.
   134  	//
   135  	// Implied is that we may only update to suggested clusters based
   136  	// on an earlier version of config if there are new algorithms.
   137  	reuseSuggestedAlgorithmResults := !newSuggestedAlgorithms &&
   138  		!config.LastUpdated.After(existing.ConfigVersion)
   139  
   140  	// For rule-based clustering.
   141  	_, reuseRuleAlgorithmResults := existing.Algorithms[rulesalgorithm.AlgorithmName]
   142  	existingRulesVersion := existing.RulesVersion
   143  	if !reuseRuleAlgorithmResults {
   144  		// Although we may have previously run rule-based clustering, we did
   145  		// not run the current version of that algorithm. Invalidate all
   146  		// previous analysis; match against all rules again.
   147  		existingRulesVersion = rules.StartingEpoch
   148  	}
   149  
   150  	result := make([][]clustering.ClusterID, len(failures))
   151  	for i, f := range failures {
   152  		newIDs := make([]clustering.ClusterID, 0, len(suggestingAlgorithms)+2)
   153  		ruleIDs := make(map[string]struct{})
   154  
   155  		existingIDs := existing.Clusters[i]
   156  		for _, id := range existingIDs {
   157  			if reuseSuggestedAlgorithmResults {
   158  				if _, ok := suggestedAlgorithmNames[id.Algorithm]; ok {
   159  					// The algorithm was run previously and its results are still valid.
   160  					// Retain its results.
   161  					newIDs = append(newIDs, id)
   162  				}
   163  			}
   164  			if reuseRuleAlgorithmResults && id.Algorithm == rulesalgorithm.AlgorithmName {
   165  				// The rules algorithm was previously run. Record the past results,
   166  				// but separately. Some previously matched rules may have been
   167  				// updated or made inactive since, so we need to treat these
   168  				// separately (and pass them to the rules algorithm to filter
   169  				// through).
   170  				ruleIDs[id.ID] = struct{}{}
   171  			}
   172  		}
   173  
   174  		if !reuseSuggestedAlgorithmResults {
   175  			// Run the suggested clustering algorithms.
   176  			for _, a := range suggestingAlgorithms {
   177  				id := a.Cluster(config, f)
   178  				if id == nil {
   179  					continue
   180  				}
   181  				newIDs = append(newIDs, clustering.ClusterID{
   182  					Algorithm: a.Name(),
   183  					ID:        hex.EncodeToString(id),
   184  				})
   185  			}
   186  		}
   187  
   188  		if ruleset.Version.Predicates.After(existingRulesVersion) {
   189  			// Match against the (incremental) set of rules.
   190  			rulesAlgorithm.Cluster(ruleset, existingRulesVersion, ruleIDs, f)
   191  		}
   192  		// Otherwise test results were already clustered with an equal or later
   193  		// version of rules. This can happen if our cached ruleset is out of date.
   194  		// Re-use the existing analysis in this case; don't try to improve on it.
   195  
   196  		for rID := range ruleIDs {
   197  			id := clustering.ClusterID{
   198  				Algorithm: rulesalgorithm.AlgorithmName,
   199  				ID:        rID,
   200  			}
   201  			newIDs = append(newIDs, id)
   202  		}
   203  
   204  		// Keep the output deterministic by sorting the clusters in the
   205  		// output.
   206  		clustering.SortClusters(newIDs)
   207  		result[i] = newIDs
   208  	}
   209  
   210  	// Base re-clustering on rule predicate changes,
   211  	// as only the rule predicate matters for clustering.
   212  	newRulesVersion := ruleset.Version.Predicates
   213  	if existingRulesVersion.After(newRulesVersion) {
   214  		// If the existing rule-matching is more current than our current
   215  		// ruleset allows, we will have kept its results, and should keep
   216  		// its RulesVersion.
   217  		// This can happen sometimes if our cached ruleset is out of date.
   218  		// This is normal.
   219  		newRulesVersion = existingRulesVersion
   220  	}
   221  	newConfigVersion := existing.ConfigVersion
   222  	if !reuseSuggestedAlgorithmResults {
   223  		// If the we recomputed the suggested clusters, record the version
   224  		// of config we used.
   225  		newConfigVersion = config.LastUpdated
   226  	}
   227  
   228  	return clustering.ClusterResults{
   229  		AlgorithmsVersion: AlgorithmsVersion,
   230  		ConfigVersion:     newConfigVersion,
   231  		RulesVersion:      newRulesVersion,
   232  		Algorithms:        algorithmNames,
   233  		Clusters:          result,
   234  	}
   235  }
   236  
   237  // ErrAlgorithmNotExist is returned if an algorithm with the given
   238  // name does not exist. This may indicate the algorithm
   239  // is newer or older than the current version.
   240  var ErrAlgorithmNotExist = errors.New("algorithm does not exist")
   241  
   242  // SuggestingAlgorithm returns the algorithm for generating
   243  // suggested clusters with the given name. If the algorithm does
   244  // not exist, ErrAlgorithmNotExist is returned.
   245  func SuggestingAlgorithm(algorithm string) (Algorithm, error) {
   246  	for _, a := range suggestingAlgorithms {
   247  		if a.Name() == algorithm {
   248  			return a, nil
   249  		}
   250  	}
   251  	// We may be running old code, or the caller may be asking
   252  	// for an old (version of an) algorithm.
   253  	return nil, ErrAlgorithmNotExist
   254  }
   255  
   256  // NewEmptyClusterResults returns a new ClusterResults for a list of
   257  // test results of length count. The ClusterResults will indicate the
   258  // test results have not been clustered.
   259  func NewEmptyClusterResults(count int) clustering.ClusterResults {
   260  	return clustering.ClusterResults{
   261  		// Algorithms version 0 is the empty set of clustering algorithms.
   262  		AlgorithmsVersion: 0,
   263  		ConfigVersion:     config.StartingEpoch,
   264  		// The RulesVersion StartingEpoch refers to the empty set of rules.
   265  		RulesVersion: rules.StartingEpoch,
   266  		Algorithms:   make(map[string]struct{}),
   267  		Clusters:     make([][]clustering.ClusterID, count),
   268  	}
   269  }