go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/tryjob/requirement/diff.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 requirement 16 17 import ( 18 "bytes" 19 "fmt" 20 "sort" 21 22 "google.golang.org/protobuf/proto" 23 24 "go.chromium.org/luci/common/data/cmpbin" 25 26 "go.chromium.org/luci/cv/internal/tryjob" 27 ) 28 29 // DefinitionMapping is a mapping between two definitions. 30 type DefinitionMapping map[*tryjob.Definition]*tryjob.Definition 31 32 // Has reports whether the given tryjob definition is in the mapping as a key. 33 func (dm DefinitionMapping) Has(def *tryjob.Definition) bool { 34 _, ok := dm[def] 35 return ok 36 } 37 38 // DiffResult contains a diff between two Tryjob requirements. 39 type DiffResult struct { 40 // RemovedDefs contains the definitions in `base` that are no longer present 41 // in `target`. 42 // 43 // Only the keys will be populated, the values will be nil. 44 RemovedDefs DefinitionMapping 45 // ChangedDefs contains mapping between definitions in `base` and `target` 46 // that have changed. 47 // 48 // The categorization of changes may vary across different backend. For 49 // buildbucket tryjob, a definition is considered changed if the main builder 50 // stays the same and other properties like equivalent builder, reuse config 51 // have changed. 52 ChangedDefs DefinitionMapping 53 // ChangedDefsReverse is a reverse mapping of `ChangedDefs` 54 ChangedDefsReverse DefinitionMapping 55 // AddedDefs contains the definitions that are added to `target` but are not 56 // in `base`. 57 // 58 // Only the keys will be populated, the values will be nil. 59 AddedDefs DefinitionMapping 60 // UnchangedDefs is like `ChangeDefs` but contain unchanged definitions. 61 // 62 // This comes handy when trying to locate the corresponding definition in the 63 // `target` when iterating through `base`. 64 UnchangedDefs DefinitionMapping 65 // UnchangedDefsReverse is a reverse mapping of `UnchangedDefs` 66 UnchangedDefsReverse DefinitionMapping 67 68 // RetryConfigChanged indicates the retry configuration has changed. 69 RetryConfigChanged bool 70 } 71 72 // Diff computes the diff between two Tryjob Requirements. 73 func Diff(base, target *tryjob.Requirement) DiffResult { 74 sortedBaseDefs := toSortedTryjobDefs(base.GetDefinitions()) 75 sortedTargetDefs := toSortedTryjobDefs(target.GetDefinitions()) 76 77 res := DiffResult{ 78 AddedDefs: make(DefinitionMapping), 79 ChangedDefs: make(DefinitionMapping), 80 ChangedDefsReverse: make(DefinitionMapping), 81 UnchangedDefs: make(DefinitionMapping), 82 UnchangedDefsReverse: make(DefinitionMapping), 83 RemovedDefs: make(DefinitionMapping), 84 } 85 for len(sortedBaseDefs) > 0 && len(sortedTargetDefs) > 0 { 86 baseDef, targetDef := sortedBaseDefs[0].def, sortedTargetDefs[0].def 87 switch bytes.Compare(sortedBaseDefs[0].sortKey, sortedTargetDefs[0].sortKey) { 88 case 0: 89 if !proto.Equal(baseDef, targetDef) { 90 res.ChangedDefs[baseDef] = targetDef 91 res.ChangedDefsReverse[targetDef] = baseDef 92 } else { 93 res.UnchangedDefs[baseDef] = targetDef 94 res.UnchangedDefsReverse[targetDef] = baseDef 95 } 96 sortedBaseDefs, sortedTargetDefs = sortedBaseDefs[1:], sortedTargetDefs[1:] 97 case -1: 98 // The head of the sortedBaseDefs is lower than the head of , 99 // sortedTargetDefs, this implies that the definition at the head of 100 // sortedBaseDefs has been removed. 101 res.RemovedDefs[baseDef] = nil 102 sortedBaseDefs = sortedBaseDefs[1:] 103 case 1: 104 // Converse case of the above. 105 res.AddedDefs[targetDef] = nil 106 sortedTargetDefs = sortedTargetDefs[1:] 107 } 108 } 109 // Add the remaining definitions. 110 for _, def := range sortedBaseDefs { 111 res.RemovedDefs[def.def] = nil 112 } 113 for _, def := range sortedTargetDefs { 114 res.AddedDefs[def.def] = nil 115 } 116 117 if !proto.Equal(base.GetRetryConfig(), target.GetRetryConfig()) { 118 res.RetryConfigChanged = true 119 } 120 return res 121 } 122 123 type comparableTryjobDef struct { 124 def *tryjob.Definition 125 // sortKey is comparable binary encoding of critical part of definition. 126 // 127 // For example, for Buildbucket tryjob, the key will be encode(host 128 // + fully qualified builder name). 129 sortKey []byte 130 } 131 132 func toSortedTryjobDefs(defs []*tryjob.Definition) []comparableTryjobDef { 133 if len(defs) == 0 { 134 return nil // Optimization: avoid initialize empty slice. 135 } 136 ret := make([]comparableTryjobDef, len(defs)) 137 for i, def := range defs { 138 if def.GetBuildbucket() == nil { 139 panic(fmt.Errorf("only support buildbucket backend; got %T", def.GetBackend())) 140 } 141 buf := &bytes.Buffer{} 142 encodeBuildbucketDef(buf, def.GetBuildbucket()) 143 ret[i] = comparableTryjobDef{ 144 def: def, 145 sortKey: buf.Bytes(), 146 } 147 } 148 sort.Slice(ret, func(i, j int) bool { 149 return bytes.Compare(ret[i].sortKey, ret[j].sortKey) < 0 150 }) 151 return ret 152 } 153 154 func encodeBuildbucketDef(buf cmpbin.WriteableBytesBuffer, bbdef *tryjob.Definition_Buildbucket) { 155 mustCmpbinWriteString(buf, bbdef.GetHost()) 156 builder := bbdef.GetBuilder() 157 mustCmpbinWriteString(buf, builder.GetProject()) 158 mustCmpbinWriteString(buf, builder.GetBucket()) 159 mustCmpbinWriteString(buf, builder.GetBuilder()) 160 } 161 162 func mustCmpbinWriteString(buf cmpbin.WriteableBytesBuffer, str string) { 163 _, err := cmpbin.WriteString(buf, str) 164 if err != nil { 165 panic(err) 166 } 167 }