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 }