go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/deploy/service/model/history.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 model
    16  
    17  import (
    18  	"context"
    19  
    20  	"go.chromium.org/luci/deploy/api/modelpb"
    21  	"go.chromium.org/luci/gae/service/datastore"
    22  )
    23  
    24  // shouldRecordHistory returns true if the new history entry should be recorded.
    25  //
    26  // It is skipped if it is not sufficiently interesting compared to the last
    27  // committed entry.
    28  //
    29  // Note that skipping a historical record also skips sending any notifications
    30  // related to it (notifications need an AssetHistory record to link to to be
    31  // useful).
    32  func shouldRecordHistory(next, prev *modelpb.AssetHistory) bool {
    33  	nextDecision := next.Decision.Decision
    34  	prevDecision := prev.Decision.Decision
    35  	switch {
    36  	case nextDecision == modelpb.ActuationDecision_SKIP_UPTODATE && IsActuateDecision(prevDecision):
    37  		return false // this particular transition is very common and not interesting
    38  	case nextDecision != prevDecision:
    39  		return true // other changes are always interesting
    40  	case IsActuateDecision(nextDecision):
    41  		return true // active actuations are also always interesting
    42  	case nextDecision == modelpb.ActuationDecision_SKIP_UPTODATE:
    43  		return false // repeating UPTODATE decisions are boring, it is steady state
    44  	case nextDecision == modelpb.ActuationDecision_SKIP_DISABLED:
    45  		return false // repeating DISABLED decisions are also boring
    46  	case nextDecision == modelpb.ActuationDecision_SKIP_LOCKED:
    47  		return !sameLocks(next.Decision.Locks, prev.Decision.Locks)
    48  	case nextDecision == modelpb.ActuationDecision_SKIP_BROKEN:
    49  		return true // errors are always interesting (for retries and alerts)
    50  	default:
    51  		panic("unreachable")
    52  	}
    53  }
    54  
    55  func sameLocks(a, b []*modelpb.ActuationLock) bool {
    56  	if len(a) != len(b) {
    57  		return false
    58  	}
    59  	for i := range a {
    60  		if a[i].Id != b[i].Id {
    61  			return false
    62  		}
    63  	}
    64  	return true
    65  }
    66  
    67  // historyRecorder collects AssetHistory records emitted by an actuation to
    68  // store them and send notifications based on them.
    69  type historyRecorder struct {
    70  	actuation *modelpb.Actuation
    71  	entries   []*modelpb.AssetHistory
    72  }
    73  
    74  // recordAndNotify emits a notification and records the historical entry.
    75  func (h *historyRecorder) recordAndNotify(e *modelpb.AssetHistory) {
    76  	h.entries = append(h.entries, e)
    77  	h.notifyOnly(e)
    78  }
    79  
    80  // notifyOnly emits the notification without updating the history.
    81  //
    82  // Useful for sending notifications pertaining to ongoing actuations.
    83  func (h *historyRecorder) notifyOnly(e *modelpb.AssetHistory) {
    84  	// TODO
    85  }
    86  
    87  // commit prepares the history entries for commit and emits TQ tasks.
    88  //
    89  // Must be called inside a transaction. Returns a list of entities to
    90  // transactionally store.
    91  func (h *historyRecorder) commit(ctx context.Context) ([]any, error) {
    92  	// TODO: Emit TQ tasks.
    93  	toPut := make([]any, len(h.entries))
    94  	for idx, entry := range h.entries {
    95  		toPut[idx] = &AssetHistory{
    96  			ID:      entry.HistoryId,
    97  			Parent:  datastore.NewKey(ctx, "Asset", entry.AssetId, 0, nil),
    98  			Entry:   entry,
    99  			Created: asTime(entry.Actuation.Created),
   100  		}
   101  	}
   102  	return toPut, nil
   103  }