go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/bugs/activation.go (about) 1 // Copyright 2023 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 bugs 16 17 import ( 18 "time" 19 20 "google.golang.org/protobuf/proto" 21 "google.golang.org/protobuf/types/known/timestamppb" 22 23 "go.chromium.org/luci/analysis/internal/analysis/metrics" 24 bugspb "go.chromium.org/luci/analysis/internal/bugs/proto" 25 configpb "go.chromium.org/luci/analysis/proto/config" 26 ) 27 28 // ActivationThresholds returns the set of thresholds that result 29 // in a policy activating. The returned thresholds should be treated as 30 // an 'OR', i.e. any of the given metric thresholds can result in a policy 31 // activating. 32 func ActivationThresholds(policy *configpb.BugManagementPolicy) []*configpb.ImpactMetricThreshold { 33 var results []*configpb.ImpactMetricThreshold 34 for _, metric := range policy.Metrics { 35 results = append(results, &configpb.ImpactMetricThreshold{ 36 MetricId: metric.MetricId, 37 Threshold: metric.ActivationThreshold, 38 }) 39 } 40 return results 41 } 42 43 // UpdatePolicyActivations updates the active policies for a failure 44 // association rule, given its current state, configured policies 45 // and cluster metrics. 46 // 47 // If no reliable cluster metrics are available, nil should be passed 48 // as clusterMetrics and only pruning of old policies/creation of new 49 // state entries for new policies will occur. 50 // 51 // If any updates need to be made, changed will be true and a new 52 // *bugspb.BugManagementState is returned. Otherwise, the original 53 // state is returned. 54 func UpdatePolicyActivations(state *bugspb.BugManagementState, policies []*configpb.BugManagementPolicy, clusterMetrics ClusterMetrics, now time.Time) (updatedState *bugspb.BugManagementState, changed bool) { 55 // Proto3 serializes nil and empty maps to exactly the same bytes. 56 // For the implementation here, we prefer to deal with the empty 57 // maps, so we coerce them, but it does not represent a semantic 58 // change to the proto. 59 policyState := state.PolicyState 60 if policyState == nil { 61 policyState = make(map[string]*bugspb.BugManagementState_PolicyState) 62 } 63 64 newPolicyState := make(map[string]*bugspb.BugManagementState_PolicyState) 65 66 changed = false 67 for _, policy := range policies { 68 state, ok := policyState[string(policy.Id)] 69 if !ok { 70 // Create a policy state entry for the new policy. 71 state = &bugspb.BugManagementState_PolicyState{} 72 changed = true 73 } 74 // Only update policy activation if cluster metrics are reliable. 75 // During re-clustering, this may not be the case and clusterMetrics will be nil. 76 if clusterMetrics != nil { 77 evaluation := evaluatePolicy(policy, clusterMetrics) 78 if !state.IsActive && evaluation == policyEvaluationActivate { 79 // Transition the policy to active. 80 // Make updates to a copied proto so that the side-effects 81 // do not propogate to the passed proto. 82 state = proto.Clone(state).(*bugspb.BugManagementState_PolicyState) 83 state.IsActive = true 84 state.LastActivationTime = timestamppb.New(now) 85 changed = true 86 } 87 if state.IsActive && evaluation == policyEvaluationDeactivate { 88 // Transition the policy to inactive. 89 // Make updates to a copied proto so that the side-effects 90 // do not propogate to the passed proto. 91 state = proto.Clone(state).(*bugspb.BugManagementState_PolicyState) 92 state.IsActive = false 93 state.LastDeactivationTime = timestamppb.New(now) 94 changed = true 95 } 96 } 97 newPolicyState[string(policy.Id)] = state 98 } 99 for policyID := range policyState { 100 if _, ok := newPolicyState[policyID]; !ok { 101 // We are removing a policy which is no longer configured. 102 changed = true 103 } 104 } 105 106 if changed { 107 return &bugspb.BugManagementState{ 108 RuleAssociationNotified: state.RuleAssociationNotified, 109 PolicyState: newPolicyState, 110 }, true 111 } 112 return state, false 113 } 114 115 // ActivePoliciesPendingNotification returns the set of policies which 116 // are active but for which activation has not been notified to the bug. 117 func ActivePoliciesPendingNotification(state *bugspb.BugManagementState) map[PolicyID]struct{} { 118 policyIDsToNotify := make(map[PolicyID]struct{}) 119 for policyID, policyState := range state.PolicyState { 120 if policyState.IsActive && !policyState.ActivationNotified { 121 policyIDsToNotify[PolicyID(policyID)] = struct{}{} 122 } 123 } 124 return policyIDsToNotify 125 } 126 127 type policyEvaluation int 128 129 const ( 130 // Neither the activation or deactivation criteria was met. The policy 131 // activation should remain unchanged. 132 policyEvaluationUnchanged policyEvaluation = iota 133 // The policy deactivation criteria was met. 134 policyEvaluationDeactivate 135 // The policy activation criteria was met. 136 policyEvaluationActivate 137 ) 138 139 // evaluatePolicy determines whether the policy activation criteria, deactivation criteria or 140 // neither are met. To use this method, clusterMetrics must be non-nil. 141 func evaluatePolicy(policy *configpb.BugManagementPolicy, clusterMetrics ClusterMetrics) policyEvaluation { 142 isDeactivationCriteriaMet := true 143 for _, metric := range policy.Metrics { 144 if clusterMetrics.MeetsThreshold(metrics.ID(metric.MetricId), metric.ActivationThreshold) { 145 // The activation criteria is met on one of the metrics. 146 // The policy should activate. 147 return policyEvaluationActivate 148 } 149 if clusterMetrics.MeetsThreshold(metrics.ID(metric.MetricId), metric.DeactivationThreshold) { 150 // If the deactivation threshold is met on any metric, 151 // deactivation is inhibited. Deactivation only occurs 152 // once all thresholds are unmet. 153 isDeactivationCriteriaMet = false 154 } 155 } 156 if isDeactivationCriteriaMet { 157 return policyEvaluationDeactivate 158 } else { 159 // Apply hysteresis: keep active policies active, and inactive policies inactive. 160 return policyEvaluationUnchanged 161 } 162 } 163 164 // ActivePolicies returns the set of policy IDs active in the given 165 // bug management state. 166 func ActivePolicies(state *bugspb.BugManagementState) map[PolicyID]struct{} { 167 result := make(map[PolicyID]struct{}) 168 for policyID, policyState := range state.PolicyState { 169 if !policyState.IsActive { 170 continue 171 } 172 result[PolicyID(policyID)] = struct{}{} 173 } 174 return result 175 } 176 177 func previouslyActivePolicies(state *bugspb.BugManagementState) map[PolicyID]struct{} { 178 currentActive := ActivePolicies(state) 179 changes := lastPolicyActivationChanges(state) 180 181 result := make(map[PolicyID]struct{}) 182 for policy := range currentActive { 183 result[policy] = struct{}{} 184 } 185 // De-activate the policies which are activated. 186 for activatedPolicyID := range changes.activatedPolicyIDs { 187 delete(result, activatedPolicyID) 188 } 189 // Re-activate the policies which were de-activated. 190 for deactivatedPolicyID := range changes.deactivatedPolicyIDs { 191 result[deactivatedPolicyID] = struct{}{} 192 } 193 return result 194 } 195 196 type activationChanges struct { 197 activatedPolicyIDs map[PolicyID]struct{} 198 deactivatedPolicyIDs map[PolicyID]struct{} 199 } 200 201 func lastPolicyActivationChanges(state *bugspb.BugManagementState) activationChanges { 202 var lastChangeTime time.Time 203 var lastActivations map[PolicyID]struct{} 204 var lastDeactivations map[PolicyID]struct{} 205 206 for policyID, policyState := range state.PolicyState { 207 if policyState.LastActivationTime != nil { 208 time := policyState.LastActivationTime.AsTime() 209 if time.After(lastChangeTime) { 210 lastChangeTime = time 211 lastActivations = make(map[PolicyID]struct{}) 212 lastDeactivations = make(map[PolicyID]struct{}) 213 lastActivations[PolicyID(policyID)] = struct{}{} 214 } else if time.Equal(lastChangeTime) { 215 lastActivations[PolicyID(policyID)] = struct{}{} 216 } 217 } 218 if policyState.LastDeactivationTime != nil { 219 time := policyState.LastDeactivationTime.AsTime() 220 if time.After(lastChangeTime) { 221 lastChangeTime = time 222 lastActivations = make(map[PolicyID]struct{}) 223 lastDeactivations = make(map[PolicyID]struct{}) 224 lastDeactivations[PolicyID(policyID)] = struct{}{} 225 } else if time.Equal(lastChangeTime) { 226 lastDeactivations[PolicyID(policyID)] = struct{}{} 227 } 228 } 229 } 230 return activationChanges{ 231 activatedPolicyIDs: lastActivations, 232 deactivatedPolicyIDs: lastDeactivations, 233 } 234 }