go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/model/builder.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 model 16 17 import ( 18 "context" 19 "fmt" 20 "math/rand" 21 "strings" 22 "time" 23 24 "go.chromium.org/luci/common/data/stringset" 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/gae/service/datastore" 27 28 pb "go.chromium.org/luci/buildbucket/proto" 29 ) 30 31 // BuilderKind is the kind of the Builder entity. 32 const BuilderKind = "Bucket.Builder" 33 34 // BuilderStatKind is the kind of the BuilderStat entity. 35 const BuilderStatKind = "Builder" 36 37 // BuilderExpirationDuration is the maximum duration a builder can go without 38 // having a build scheduled before its BuilderStat may be deleted. 39 const BuilderExpirationDuration = 4 * 7 * 24 * time.Hour // 4 weeks 40 41 // BuilderStatZombieDuration is the maximum duration for which a zombie 42 // BuilderStat that can exist without having a build scheduled before it may be 43 // deleted. 44 // 45 // Zombie BuilderStat is a BuilderStat entity of which Builder entity doesn't 46 // exist. 47 const BuilderStatZombieDuration = 6 * time.Hour 48 49 // Builder is a Datastore entity that stores builder configuration. 50 // It is a child of Bucket entity. 51 // 52 // Builder entities are updated together with their parents, in a cron job. 53 type Builder struct { 54 _kind string `gae:"$kind,Bucket.Builder"` 55 56 // ID is the builder name, e.g. "linux-rel". 57 ID string `gae:"$id"` 58 59 // Parent is the key of the parent Bucket. 60 Parent *datastore.Key `gae:"$parent"` 61 62 // Config is the builder configuration feched from luci-config. 63 Config *pb.BuilderConfig `gae:"config,legacy"` 64 65 // ConfigHash is used for fast deduplication of configs. 66 ConfigHash string `gae:"config_hash"` 67 68 // Metadata is the builder owner and health information. 69 Metadata *pb.BuilderMetadata `gae:"builder_metadata,legacy"` 70 } 71 72 // FullBuilderName return the builder name in the format of "<project>.<bucket>.<builder>". 73 func (b *Builder) FullBuilderName() string { 74 return fmt.Sprintf("%s.%s.%s", b.Parent.Parent().StringID(), b.Parent.StringID(), b.ID) 75 } 76 77 // BuilderKey returns a datastore key of a builder. 78 func BuilderKey(ctx context.Context, project, bucket, builder string) *datastore.Key { 79 return datastore.KeyForObj(ctx, &Builder{ 80 ID: builder, 81 Parent: BucketKey(ctx, project, bucket), 82 }) 83 } 84 85 // BuilderStat represents a builder Datastore entity which is used internally for metrics. 86 // 87 // The builder will be registered automatically by scheduling a build, 88 // and unregistered automatically by not scheduling builds for BuilderExpirationDuration. 89 // 90 // Note: due to the historical reason, the entity kind is Builder. 91 type BuilderStat struct { 92 _kind string `gae:"$kind,Builder"` 93 94 // ID is a string with format "{project}:{bucket}:{builder}". 95 ID string `gae:"$id"` 96 97 // LastScheduled is the last time we received a valid build scheduling request 98 // for this builder. Probabilistically update when scheduling a build. 99 LastScheduled time.Time `gae:"last_scheduled,noindex"` 100 } 101 102 // BuilderKey returns a datastore key for the Builder that a given BuilderStat 103 // references. 104 // 105 // Panics if the ID of the BuilderStat is invalid. 106 func (s *BuilderStat) BuilderKey(ctx context.Context) *datastore.Key { 107 parts := strings.Split(s.ID, ":") 108 if len(parts) != 3 { 109 panic(fmt.Errorf("invalid BuilderStatID: %s", s.ID)) 110 } 111 return BuilderKey(ctx, parts[0], parts[1], parts[2]) 112 } 113 114 // BuilderStatKey returns a datastore key for a given Builder. 115 func BuilderStatKey(ctx context.Context, project, bucket, builder string) *datastore.Key { 116 return datastore.KeyForObj(ctx, &BuilderStat{ 117 ID: fmt.Sprintf("%s:%s:%s", project, bucket, builder), 118 }) 119 } 120 121 // UpdateBuilderStat updates or creates datastore BuilderStat entities. 122 func UpdateBuilderStat(ctx context.Context, builds []*Build, scheduledTime time.Time) error { 123 seen := stringset.New(len(builds)) 124 builderStats := make([]*BuilderStat, 0, len(builds)) 125 for _, b := range builds { 126 if b.Proto.Builder == nil { 127 panic("Build.Proto.Builder isn't initialized") 128 } 129 id := fmt.Sprintf("%s:%s:%s", b.Proto.Builder.Project, b.Proto.Builder.Bucket, b.Proto.Builder.Builder) 130 if seen.Add(id) { 131 builderStats = append(builderStats, &BuilderStat{ 132 ID: id, 133 }) 134 } 135 } 136 137 if err := GetIgnoreMissing(ctx, builderStats); err != nil { 138 return errors.Annotate(err, "error fetching BuilderStat").Err() 139 } 140 141 var toPut []*BuilderStat 142 for _, s := range builderStats { 143 if s.LastScheduled.IsZero() { 144 s.LastScheduled = scheduledTime 145 toPut = append(toPut, s) 146 } else { 147 // Probabilistically update BuilderStat entities to avoid high contention. 148 // The longer an entity isn't updated, the greater its probability. 149 sinceLastUpdate := scheduledTime.Sub(s.LastScheduled) 150 updateProbability := sinceLastUpdate.Seconds() / 3600.0 151 if rand.Float64() < updateProbability { 152 s.LastScheduled = scheduledTime 153 toPut = append(toPut, s) 154 } 155 } 156 } 157 if len(toPut) == 0 { 158 return nil 159 } 160 if err := datastore.Put(ctx, toPut); err != nil { 161 return errors.Annotate(err, "error putting BuilderStat").Err() 162 } 163 return nil 164 }