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 }