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  }