go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/common/run.go (about) 1 // Copyright 2020 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 common 16 17 import ( 18 "context" 19 "encoding/hex" 20 "fmt" 21 "sort" 22 "strings" 23 "time" 24 25 "go.chromium.org/luci/auth/identity" 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/config" 29 "go.chromium.org/luci/server/auth" 30 ) 31 32 // RunKind is the Datastore entity kind for Run. 33 const RunKind = "Run" 34 35 // MaxRunTotalDuration is the max total duration of the Run. 36 // 37 // Total duration means end time - create time. Run will be cancelled after 38 // the total duration is reached. 39 const MaxRunTotalDuration = 10 * 24 * time.Hour // 10 days 40 41 // RunID is an unique RunID to identify a Run in CV. 42 // 43 // RunID is string like `luciProject/inverseTS-1-hexHashDigest` consisting of 44 // 7 parts: 45 // 1. The LUCI Project that this Run belongs to. 46 // Purpose: separates load on Datastore from different projects. 47 // 2. `/` separator. 48 // 3. InverseTS, defined as (`endOfTheWorld` - CreateTime) in ms precision, 49 // left-padded with zeros to 13 digits. See `Run.CreateTime` Doc. 50 // Purpose: ensures queries by default orders runs of the same project by 51 // most recent first. 52 // 4. `-` separator. 53 // 5. Digest version (see part 7). 54 // 6. `-` separator. 55 // 7. A hex digest string uniquely identifying the set of CLs involved in 56 // this Run. 57 // Purpose: ensures two simultaneously started Runs in the same project 58 // won't have the same RunID. 59 type RunID string 60 61 // CV will be dead on ~292.3 years after first LUCI design doc was created. 62 // 63 // Computed as https://play.golang.com/p/hDQ-EhlSLu5 64 // 65 // luci := time.Date(2014, time.May, 9, 1, 26, 0, 0, time.UTC) 66 // endOfTheWorld := luci.Add(time.Duration(1<<63 - 1)) 67 var endOfTheWorld = time.Date(2306, time.August, 19, 1, 13, 16, 854775807, time.UTC) 68 69 func MakeRunID(luciProject string, createTime time.Time, digestVersion int, clsDigest []byte) RunID { 70 if endOfTheWorld.Sub(createTime) == 1<<63-1 { 71 panic(fmt.Errorf("overflow")) 72 } 73 ms := endOfTheWorld.Sub(createTime).Milliseconds() 74 if ms < 0 { 75 panic(fmt.Errorf("Can't create run at %s which is after endOfTheWorld %s", createTime, endOfTheWorld)) 76 } 77 id := fmt.Sprintf("%s/%013d-%d-%s", luciProject, ms, digestVersion, hex.EncodeToString(clsDigest)) 78 return RunID(id) 79 } 80 81 // Validate returns an error if Run ID is not valid. 82 // 83 // If validate returns nil, 84 // - it means all other methods on RunID will work fine instead of panicking, 85 // - it doesn't mean Run ID is possible to generate using the MakeRunID. 86 // This is especially relevant in CV tests, where specifying short Run IDs is 87 // useful. 88 func (id RunID) Validate() (err error) { 89 defer func() { 90 if err != nil { 91 err = errors.Annotate(err, "malformed RunID %q", id).Err() 92 } 93 }() 94 95 allDigits := func(digits string) bool { 96 for _, r := range digits { 97 if r < '0' || r > '9' { 98 return false 99 } 100 } 101 return true 102 } 103 104 s := string(id) 105 i := strings.IndexRune(s, '/') 106 if i < 1 { 107 return fmt.Errorf("lacks LUCI project") 108 } 109 if err := config.ValidateProjectName(s[:i]); err != nil { 110 return fmt.Errorf("invalid LUCI project part: %s", err) 111 } 112 s = s[i+1:] 113 114 i = strings.IndexRune(s, '-') 115 if i < 1 { 116 return fmt.Errorf("lacks InverseTS part") 117 } 118 if !allDigits(s[:i]) { 119 return fmt.Errorf("invalid InverseTS") 120 } 121 122 s = s[i+1:] 123 i = strings.IndexRune(s, '-') 124 if i < 1 { 125 return fmt.Errorf("lacks version") 126 } 127 if !allDigits(s[:i]) { 128 return fmt.Errorf("invalid version") 129 } 130 131 s = s[i+1:] 132 if len(s) == 0 { 133 return fmt.Errorf("lacks digest") 134 } 135 return nil 136 } 137 138 // LUCIProject this Run belongs to. 139 func (id RunID) LUCIProject() string { 140 pos := strings.IndexRune(string(id), '/') 141 if pos == -1 { 142 panic(fmt.Errorf("invalid run ID %q", id)) 143 } 144 return string(id[:pos]) 145 } 146 147 // Inner is the part after "<LUCIProject>/" for use in UI. 148 func (id RunID) Inner() string { 149 pos := strings.IndexRune(string(id), '/') 150 if pos == -1 { 151 panic(fmt.Errorf("invalid run ID %q", id)) 152 } 153 return string(id[pos+1:]) 154 } 155 156 // InverseTS of this Run. See RunID doc. 157 func (id RunID) InverseTS() string { 158 s := string(id) 159 posSlash := strings.IndexRune(s, '/') 160 if posSlash == -1 { 161 panic(fmt.Errorf("invalid run ID %q", id)) 162 } 163 s = s[posSlash+1:] 164 posDash := strings.IndexRune(s, '-') 165 return s[:posDash] 166 } 167 168 // PublicID returns the public representation of the RunID. 169 // 170 // The format of a public ID is `projects/$luci-project/runs/$id`, where 171 // - luci-project is the name of the LUCI project the Run belongs to 172 // - id is an opaque key unique in the LUCI project. 173 func (id RunID) PublicID() string { 174 prj := id.LUCIProject() 175 return fmt.Sprintf("projects/%s/runs/%s", prj, string(id[len(prj)+1:])) 176 } 177 178 // FromPublicRunID is the inverse of RunID.PublicID(). 179 func FromPublicRunID(id string) (RunID, error) { 180 parts := strings.Split(id, "/") 181 if len(parts) == 4 && parts[0] == "projects" && parts[2] == "runs" { 182 return RunID(parts[1] + "/" + parts[3]), nil 183 } 184 return "", errors.Reason(`Run ID must be in the form "projects/$luci-project/runs/$id", but %q given"`, id).Err() 185 } 186 187 // AttemptKey returns CQDaemon attempt key. 188 func (id RunID) AttemptKey() string { 189 i := strings.LastIndexByte(string(id), '-') 190 if i == -1 || i == len(id)-1 { 191 panic(fmt.Errorf("invalid run ID %q", id)) 192 } 193 return string(id[i+1:]) 194 } 195 196 // RunIDs is a convenience type to facilitate handling of run RunIDs. 197 type RunIDs []RunID 198 199 // sort.Interface copy-pasta. 200 func (ids RunIDs) Less(i, j int) bool { return ids[i] < ids[j] } 201 func (ids RunIDs) Len() int { return len(ids) } 202 func (ids RunIDs) Swap(i, j int) { ids[i], ids[j] = ids[j], ids[i] } 203 204 // WithoutSorted returns a subsequence of IDs without excluded IDs. 205 // 206 // Both this and the excluded slices must be sorted. 207 // 208 // If this and excluded IDs are disjoint, return this slice. 209 // Otherwise, returns a copy without excluded IDs. 210 func (ids RunIDs) WithoutSorted(exclude RunIDs) RunIDs { 211 remaining := ids 212 ret := ids 213 mutated := false 214 for { 215 switch { 216 case len(remaining) == 0: 217 return ret 218 case len(exclude) == 0: 219 if mutated { 220 ret = append(ret, remaining...) 221 } 222 return ret 223 case remaining[0] < exclude[0]: 224 if mutated { 225 ret = append(ret, remaining[0]) 226 } 227 remaining = remaining[1:] 228 case remaining[0] > exclude[0]: 229 exclude = exclude[1:] 230 default: 231 if !mutated { 232 // Must copy all IDs that were skipped. 233 mutated = true 234 n := len(ids) - len(remaining) 235 ret = make(RunIDs, n, len(ids)-1) 236 copy(ret, ids) // copies len(ret) == n elements. 237 } 238 remaining = remaining[1:] 239 exclude = exclude[1:] 240 } 241 } 242 } 243 244 // InsertSorted adds given ID if not yet exists to the list keeping list sorted. 245 // 246 // InsertSorted is a pointer receiver method, because it modifies slice itself. 247 func (p *RunIDs) InsertSorted(id RunID) { 248 ids := *p 249 switch i := sort.Search(len(ids), func(i int) bool { return ids[i] >= id }); { 250 case i == len(ids): 251 *p = append(ids, id) 252 case ids[i] > id: 253 // Insert new ID at position i and shift the rest of slice to the right. 254 toInsert := id 255 for ; i < len(ids); i++ { 256 ids[i], toInsert = toInsert, ids[i] 257 } 258 *p = append(ids, toInsert) 259 } 260 } 261 262 // DelSorted removes the given ID if it exists. 263 // 264 // DelSorted is a pointer receiver method, because it modifies slice itself. 265 func (p *RunIDs) DelSorted(id RunID) bool { 266 ids := *p 267 i := sort.Search(len(ids), func(i int) bool { return ids[i] >= id }) 268 if i == len(ids) || ids[i] != id { 269 return false 270 } 271 272 copy(ids[i:], ids[i+1:]) 273 ids[len(ids)-1] = "" 274 *p = ids[:len(ids)-1] 275 return true 276 } 277 278 // ContainsSorted returns true if ids contain the given one. 279 func (ids RunIDs) ContainsSorted(id RunID) bool { 280 i := sort.Search(len(ids), func(i int) bool { return ids[i] >= id }) 281 return i < len(ids) && ids[i] == id 282 } 283 284 // DifferenceSorted returns all IDs in this slice and not the other one. 285 // 286 // Both slices must be sorted. Doesn't modify input slices. 287 func (a RunIDs) DifferenceSorted(b RunIDs) RunIDs { 288 var diff RunIDs 289 for { 290 if len(b) == 0 { 291 return append(diff, a...) 292 } 293 if len(a) == 0 { 294 return diff 295 } 296 x, y := a[0], b[0] 297 switch { 298 case x == y: 299 a, b = a[1:], b[1:] 300 case x < y: 301 diff = append(diff, x) 302 a = a[1:] 303 default: 304 b = b[1:] 305 } 306 } 307 } 308 309 // Index returns the index of the first instance of the provided id. 310 // 311 // Returns -1 if the provided id isn't present. 312 func (ids RunIDs) Index(target RunID) int { 313 for i, id := range ids { 314 if id == target { 315 return i 316 } 317 } 318 return -1 319 } 320 321 // Equal checks if two ids are equal. 322 func (ids RunIDs) Equal(other RunIDs) bool { 323 if len(ids) != len(other) { 324 return false 325 } 326 for i, id := range ids { 327 if id != other[i] { 328 return false 329 } 330 } 331 return true 332 } 333 334 // Set returns a new set of run IDs. 335 func (ids RunIDs) Set() map[RunID]struct{} { 336 r := make(map[RunID]struct{}, len(ids)) 337 for _, id := range ids { 338 r[id] = struct{}{} 339 } 340 return r 341 } 342 343 // MakeRunIDs returns RunIDs from list of strings. 344 func MakeRunIDs(ids ...string) RunIDs { 345 ret := make(RunIDs, len(ids)) 346 for i, id := range ids { 347 ret[i] = RunID(id) 348 } 349 return ret 350 } 351 352 // MCEDogfooderGroup is a CrIA group who signed up for dogfooding MCE. 353 const MCEDogfooderGroup = "luci-cv-mce-dogfooders" 354 355 // IsMCEDogfooder returns true if the user is an MCE dogfooder. 356 // 357 // TODO(ddoman): remove this function, once MCE dogfood is done. 358 func IsMCEDogfooder(ctx context.Context, id identity.Identity) bool { 359 // if it fails to retrieve the authDB, then log the error and return false. 360 // this function will be removed, anyways. 361 ret, err := auth.GetState(ctx).DB().IsMember(ctx, id, []string{MCEDogfooderGroup}) 362 if err != nil { 363 logging.Errorf(ctx, "IsMCEDogfooder: auth.IsMember: %s", err) 364 } 365 return ret 366 } 367 368 // InstantTriggerDogfooderGroup is the CrIA group who signed up for dogfooding 369 // cros instant trigger. 370 const InstantTriggerDogfooderGroup = "luci-cv-instant-trigger-dogfooders" 371 372 // IsInstantTriggerDogfooder returns true if the given user participate in 373 // the cros instant trigger dogfood. 374 // 375 // TODO(yiwzhang): remove this function, once cros instant trigger dogfood is 376 // done. 377 func IsInstantTriggerDogfooder(ctx context.Context, id identity.Identity) bool { 378 // if it fails to retrieve the authDB, then log the error and return false. 379 // this function will be removed, anyways. 380 ret, err := auth.GetState(ctx).DB().IsMember(ctx, id, []string{InstantTriggerDogfooderGroup}) 381 if err != nil { 382 logging.Errorf(ctx, "IsInstantTriggerDogfooder: auth.IsMember: %s", err) 383 return false 384 } 385 return ret 386 }