go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/model/tagindex.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 "time" 21 22 "go.chromium.org/luci/buildbucket/protoutil" 23 "go.chromium.org/luci/common/data/rand/mathrand" 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/logging" 26 "go.chromium.org/luci/gae/service/datastore" 27 ) 28 29 // Ensure TagIndexEntry implements datastore.PropertyConverter. 30 var _ datastore.PropertyConverter = &TagIndexEntry{} 31 32 // TagIndexEntry refers to a particular Build entity. 33 type TagIndexEntry struct { 34 // BuildID is the ID of the Build entity this entry refers to. 35 BuildID int64 `json:"build_id"` 36 // <project>/<bucket>. Bucket is in v2 format. 37 // e.g. chromium/try (never chromium/luci.chromium.try). 38 BucketID string `json:"bucket_id"` 39 // CreatedTime is the time this entry was created. 40 CreatedTime time.Time `json:"created_time"` 41 } 42 43 // FromProperty deserializes TagIndexEntries from the datastore. 44 // Implements datastore.PropertyConverter. 45 func (e *TagIndexEntry) FromProperty(p datastore.Property) error { 46 for key, val := range p.Value().(datastore.PropertyMap) { 47 switch key { 48 case "build_id": 49 e.BuildID = val.Slice()[0].Value().(int64) 50 case "bucket_id": 51 e.BucketID = val.Slice()[0].Value().(string) 52 case "created_time": 53 e.CreatedTime = val.Slice()[0].Value().(time.Time) 54 } 55 } 56 return nil 57 } 58 59 // ToProperty serializes TagIndexEntries to datastore format. 60 // Implements datastore.PropertyConverter. 61 func (e *TagIndexEntry) ToProperty() (datastore.Property, error) { 62 p := datastore.Property{} 63 err := p.SetValue(datastore.PropertyMap{ 64 "build_id": datastore.MkProperty(e.BuildID), 65 "bucket_id": datastore.MkProperty(e.BucketID), 66 "created_time": datastore.MkProperty(e.CreatedTime), 67 }, datastore.NoIndex) 68 return p, err 69 } 70 71 // MaxTagIndexEntries is the maximum number of entries that may be associated 72 // with a single TagIndex entity. 73 const MaxTagIndexEntries = 1000 74 75 // TagIndexShardCount is the number of shards used by the TagIndex. 76 const TagIndexShardCount = 16 77 78 // TagIndex is an index used to search Build entities by tag. 79 type TagIndex struct { 80 _kind string `gae:"$kind,TagIndex"` 81 // ID is a "<key>:<value>" or ":<index>:<key>:<value>" string for index > 0. 82 ID string `gae:"$id"` 83 // Incomplete means there are more than MaxTagIndexEntries entities 84 // with the same ID, and therefore the index is incomplete and cannot be 85 // searched. 86 Incomplete bool `gae:"permanently_incomplete,noindex"` 87 // Entries is a slice of TagIndexEntries matching this ID. 88 Entries []TagIndexEntry `gae:"entries,noindex"` 89 } 90 91 // TagIndexIncomplete means the tag index is incomplete and thus cannot be searched. 92 var TagIndexIncomplete = errors.BoolTag{Key: errors.NewTagKey("tag index incomplete")} 93 94 // SearchTagIndex searches the tag index for the given tag. 95 // Returns an error tagged with TagIndexIncomplete if the tag index is 96 // incomplete and thus cannot be searched. 97 func SearchTagIndex(ctx context.Context, key, val string) ([]*TagIndexEntry, error) { 98 shds := make([]TagIndex, TagIndexShardCount) 99 for i := range shds { 100 if i == 0 { 101 shds[i].ID = fmt.Sprintf("%s:%s", key, val) 102 } else { 103 shds[i].ID = fmt.Sprintf(":%d:%s:%s", i, key, val) 104 } 105 } 106 if err := GetIgnoreMissing(ctx, shds); err != nil { 107 return nil, errors.Annotate(err, "error fetching tag index for %q", fmt.Sprintf("%s:%s", key, val)).Err() 108 } 109 var ents []*TagIndexEntry 110 for _, s := range shds { 111 if s.Incomplete { 112 return nil, errors.Reason("tag index incomplete for %q", fmt.Sprintf("%s:%s", key, val)).Tag(TagIndexIncomplete).Err() 113 } 114 for i := range s.Entries { 115 // check in case the tagIndexEntry is corrupted. 116 if _, _, err := protoutil.ParseBucketID(s.Entries[i].BucketID); err != nil { 117 logging.Warningf(ctx, "Bad TagIndexEntry(%+v): %s in TagIndex %s", s.Entries[i], err, s.ID) 118 continue 119 } 120 ents = append(ents, &s.Entries[i]) 121 } 122 } 123 return ents, nil 124 } 125 126 // UpdateTagIndex updates the tag index for the given tag. 127 func UpdateTagIndex(ctx context.Context, tag string, ents []TagIndexEntry) error { 128 if len(ents) == 0 { 129 return nil 130 } 131 return updateTagIndex(ctx, tag, mathrand.Intn(ctx, TagIndexShardCount), ents) 132 } 133 134 // updateTagIndex updates the tag index's specified shard for the given tag. 135 func updateTagIndex(ctx context.Context, tag string, shard int, ents []TagIndexEntry) error { 136 if len(ents) == 0 { 137 return nil 138 } 139 return datastore.RunInTransaction(ctx, func(ctx context.Context) error { 140 shd := &TagIndex{ 141 ID: tag, 142 } 143 if shard > 0 { 144 shd.ID = fmt.Sprintf(":%d:%s", shard, tag) 145 } 146 switch err := datastore.Get(ctx, shd); { 147 case err == datastore.ErrNoSuchEntity: 148 case err != nil: 149 return errors.Annotate(err, "error fetching tag index for %q", shd.ID).Err() 150 case shd.Incomplete: 151 // No point in updating an incomplete index because it cannot be searched. 152 return nil 153 } 154 155 orig := len(shd.Entries) 156 shd.Entries = append(shd.Entries, ents...) 157 if len(shd.Entries) > MaxTagIndexEntries { 158 shd.Entries = nil 159 shd.Incomplete = true 160 logging.Warningf(ctx, "marking tag index incomplete for %q", shd.ID) 161 } else { 162 logging.Debugf(ctx, "updating tag index for %q (entries %d -> %d)", shd.ID, orig, len(ents)) 163 } 164 165 if err := datastore.Put(ctx, shd); err != nil { 166 return errors.Annotate(err, "error updating tag index for %q", shd.ID).Err() 167 } 168 return nil 169 }, nil) 170 }